|
|
@@ -1,5 +1,12 @@
|
|
|
+import binascii
|
|
|
+import collections
|
|
|
+import datetime
|
|
|
+import hashlib
|
|
|
import re
|
|
|
|
|
|
+import six
|
|
|
+from six.moves.urllib.parse import quote
|
|
|
+
|
|
|
from googleapiclient.errors import HttpError
|
|
|
|
|
|
import tenacity
|
|
|
@@ -186,3 +193,87 @@ def change_label(resource, key, value, res_att, request):
|
|
|
response, zone=getattr(resource, 'zone_name', None))
|
|
|
finally:
|
|
|
resource.refresh()
|
|
|
+
|
|
|
+
|
|
|
+# https://cloud.google.com/storage/docs/access-control/signing-urls-manually#python-sample
|
|
|
+def generate_signed_url(credentials, bucket_name, object_name,
|
|
|
+ subresource=None, expiration=604800, http_method='GET',
|
|
|
+ query_parameters=None, headers=None):
|
|
|
+
|
|
|
+ if expiration > 604800:
|
|
|
+ # max allowed expiration time is 7 days
|
|
|
+ expiration = 604800
|
|
|
+
|
|
|
+ escaped_object_name = quote(six.ensure_binary(object_name), safe=b'/~')
|
|
|
+ canonical_uri = '/{}'.format(escaped_object_name)
|
|
|
+
|
|
|
+ datetime_now = datetime.datetime.utcnow()
|
|
|
+ request_timestamp = datetime_now.strftime('%Y%m%dT%H%M%SZ')
|
|
|
+ datestamp = datetime_now.strftime('%Y%m%d')
|
|
|
+
|
|
|
+ client_email = credentials.service_account_email
|
|
|
+ credential_scope = '{}/auto/storage/goog4_request'.format(datestamp)
|
|
|
+ credential = '{}/{}'.format(client_email, credential_scope)
|
|
|
+
|
|
|
+ if headers is None:
|
|
|
+ headers = dict()
|
|
|
+ host = '{}.storage.googleapis.com'.format(bucket_name)
|
|
|
+ headers['host'] = host
|
|
|
+
|
|
|
+ canonical_headers = ''
|
|
|
+ ordered_headers = collections.OrderedDict(sorted(headers.items()))
|
|
|
+ for k, v in ordered_headers.items():
|
|
|
+ lower_k = str(k).lower()
|
|
|
+ strip_v = str(v).lower()
|
|
|
+ canonical_headers += '{}:{}\n'.format(lower_k, strip_v)
|
|
|
+
|
|
|
+ signed_headers = ''
|
|
|
+ for k, _ in ordered_headers.items():
|
|
|
+ lower_k = str(k).lower()
|
|
|
+ signed_headers += '{};'.format(lower_k)
|
|
|
+ signed_headers = signed_headers[:-1] # remove trailing ';'
|
|
|
+
|
|
|
+ if query_parameters is None:
|
|
|
+ query_parameters = dict()
|
|
|
+ query_parameters['X-Goog-Algorithm'] = 'GOOG4-RSA-SHA256'
|
|
|
+ query_parameters['X-Goog-Credential'] = credential
|
|
|
+ query_parameters['X-Goog-Date'] = request_timestamp
|
|
|
+ query_parameters['X-Goog-Expires'] = expiration
|
|
|
+ query_parameters['X-Goog-SignedHeaders'] = signed_headers
|
|
|
+ if subresource:
|
|
|
+ query_parameters[subresource] = ''
|
|
|
+
|
|
|
+ canonical_query_string = ''
|
|
|
+ ordered_query_parameters = collections.OrderedDict(
|
|
|
+ sorted(query_parameters.items()))
|
|
|
+ for k, v in ordered_query_parameters.items():
|
|
|
+ encoded_k = quote(str(k), safe='')
|
|
|
+ encoded_v = quote(str(v), safe='')
|
|
|
+ canonical_query_string += '{}={}&'.format(encoded_k, encoded_v)
|
|
|
+ canonical_query_string = canonical_query_string[:-1] # remove trailing '&'
|
|
|
+
|
|
|
+ canonical_request = '\n'.join([http_method,
|
|
|
+ canonical_uri,
|
|
|
+ canonical_query_string,
|
|
|
+ canonical_headers,
|
|
|
+ signed_headers,
|
|
|
+ 'UNSIGNED-PAYLOAD'])
|
|
|
+
|
|
|
+ canonical_request_hash = hashlib.sha256(
|
|
|
+ canonical_request.encode()).hexdigest()
|
|
|
+
|
|
|
+ string_to_sign = '\n'.join(['GOOG4-RSA-SHA256',
|
|
|
+ request_timestamp,
|
|
|
+ credential_scope,
|
|
|
+ canonical_request_hash])
|
|
|
+
|
|
|
+ # signer.sign() signs using RSA-SHA256 with PKCS1v15 padding
|
|
|
+ signature = binascii.hexlify(
|
|
|
+ credentials.signer.sign(string_to_sign)
|
|
|
+ ).decode()
|
|
|
+
|
|
|
+ scheme_and_host = '{}://{}'.format('https', host)
|
|
|
+ signed_url = '{}{}?{}&x-goog-signature={}'.format(
|
|
|
+ scheme_and_host, canonical_uri, canonical_query_string, signature)
|
|
|
+
|
|
|
+ return signed_url
|