Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.8k
Fixes #52 - Allow query-string authentication (aka, signing URLs).#56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| import base64 | ||
| import datetime | ||
| import httplib2 | ||
| import json | ||
| import time | ||
| import urllib | ||
| from Crypto.Hash import SHA256 | ||
| from Crypto.PublicKey import RSA | ||
| from Crypto.Signature import PKCS1_v1_5 | ||
| from OpenSSL import crypto | ||
| import pytz | ||
| from gcloud import connection | ||
| from gcloud.storage import exceptions | ||
| @@ -57,6 +65,8 @@ class Connection(connection.Connection): | ||
| API_URL_TEMPLATE = '{api_base_url}/storage/{api_version}{path}' | ||
| """A template used to craft the URL pointing toward a particular API call.""" | ||
| API_ACCESS_ENDPOINT = 'https://storage.googleapis.com' | ||
| def __init__(self, project_name, *args, **kwargs): | ||
| """ | ||
| :type project_name: string | ||
| @@ -400,3 +410,82 @@ def new_bucket(self, bucket): | ||
| return Bucket(connection=self, name=bucket) | ||
| raise TypeError('Invalid bucket: %s' % bucket) | ||
| def generate_signed_url(self, resource, expiration, method='GET', content_md5=None, content_type=None): | ||
| """Generate a signed URL to provide query-string authentication to a resource. | ||
| :type resource: string | ||
| :param resource: A pointer to a specific resource | ||
| (typically, ``/bucket-name/path/to/key.txt``). | ||
| :type expiration: int, long, datetime.datetime, datetime.timedelta | ||
| :param expiration: When the signed URL should expire. | ||
| :type method: string | ||
| :param method: The HTTP verb that will be used when requesting the URL. | ||
| :type content_md5: string | ||
| :param content_md5: The MD5 hash of the object referenced by ``resource``. | ||
| :type content_type: string | ||
| :param content_type: The content type of the object referenced by | ||
| ``resource``. | ||
| :rtype: string | ||
| :returns: A signed URL you can use to access the resource until expiration. | ||
| """ | ||
| # expiration can be an absolute timestamp (int, long), | ||
| # an absolute time (datetime.datetime), | ||
| # or a relative time (datetime.timedelta). | ||
| # We should convert all of these into an absolute timestamp. | ||
| # If it's a timedelta, add it to `now` in UTC. | ||
| if isinstance(expiration, datetime.timedelta): | ||
| now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) | ||
| expiration = now + expiration | ||
| # If it's a datetime, convert to a timestamp. | ||
| if isinstance(expiration, datetime.datetime): | ||
| # Make sure the timezone on the value is UTC | ||
| # (either by converting or replacing the value). | ||
| if expiration.tzinfo: | ||
| expiration = expiration.astimezone(pytz.utc) | ||
| else: | ||
| expiration = expiration.replace(tzinfo=pytz.utc) | ||
| # Turn the datetime into a timestamp (seconds, not microseconds). | ||
| expiration = int(time.mktime(expiration.timetuple())) | ||
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page.
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page. | ||
| if not isinstance(expiration, (int, long)): | ||
| raise ValueError('Expected an integer timestamp, datetime, or timedelta. ' | ||
| 'Got %s' % type(expiration)) | ||
| # Generate the string to sign. | ||
| signature_string = '\n'.join([ | ||
| method, | ||
| content_md5 or '', | ||
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page.
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page. | ||
| content_type or '', | ||
| str(expiration), | ||
| resource]) | ||
| # Take our PKCS12 (.p12) key and make it into a RSA key we can use... | ||
| pkcs12 = crypto.load_pkcs12(base64.b64decode(self.credentials.private_key), 'notasecret') | ||
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page.
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page. | ||
| pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs12.get_privatekey()) | ||
| pem_key = RSA.importKey(pem) | ||
| # Sign the string with the RSA key. | ||
| signer = PKCS1_v1_5.new(pem_key) | ||
| signature_hash = SHA256.new(signature_string) | ||
| signature_bytes = signer.sign(signature_hash) | ||
| signature = base64.b64encode(signature_bytes) | ||
| # Set the right query parameters. | ||
| query_params = {'GoogleAccessId': self.credentials.service_account_name, | ||
| 'Expires': str(expiration), | ||
| 'Signature': signature} | ||
| # Return the built URL. | ||
| return '{endpoint}{resource}?{querystring}'.format( | ||
| endpoint=self.API_ACCESS_ENDPOINT, resource=resource, | ||
| querystring=urllib.urlencode(query_params)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -64,6 +64,19 @@ def __repr__(self): | ||
| return '<Key: %s, %s>' % (bucket_name, self.name) | ||
| @property | ||
| def connection(self): | ||
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page.
This comment was marked as spam.Sorry, something went wrong. Uh oh!There was an error while loading. Please reload this page. | ||
| """Getter property for the connection to use with this Key. | ||
| :rtype: :class:`gcloud.storage.connection.Connection` or None | ||
| :returns: The connection to use, or None if no connection is set. | ||
| """ | ||
| # TODO: If a bucket isn't defined, this is basically useless. | ||
| # Where do we throw an error? | ||
| if self.bucket and self.bucket.connection: | ||
| return self.bucket.connection | ||
| @property | ||
| def path(self): | ||
| """Getter property for the URL path to this Key. | ||
| @@ -84,18 +97,31 @@ def public_url(self): | ||
| return '{storage_base_url}/{self.bucket.name}/{self.name}'.format( | ||
| storage_base_url='http://commondatastorage.googleapis.com', self=self) | ||
| @property | ||
| def connection(self): | ||
| """Getter property for the connection to use with this Key. | ||
| def generate_signed_url(self, expiration, method='GET'): | ||
| """Generates a signed URL for this key. | ||
| :rtype: :class:`gcloud.storage.connection.Connection` or None | ||
| :returns: The connection to use, or None if no connection is set. | ||
| If you have a key that you want to allow access to | ||
| for a set amount of time, | ||
| you can use this method to generate a URL | ||
| that is only valid within a certain time period. | ||
| This is particularly useful if you don't want publicly accessible keys, | ||
| but don't want to require users to explicitly log in. | ||
| :type expiration: int, long, datetime.datetime, datetime.timedelta | ||
| :param expiration: When the signed URL should expire. | ||
| :type method: string | ||
| :param method: The HTTP verb that will be used when requesting the URL. | ||
| :rtype: string | ||
| :returns: A signed URL you can use to access the resource until expiration. | ||
| """ | ||
| # TODO: If a bucket isn't defined, this is basically useless. | ||
| # Where do we throw an error? | ||
| if self.bucket and self.bucket.connection: | ||
| return self.bucket.connection | ||
| resource = '/{self.bucket.name}/{self.name}'.format(self=self) | ||
| return self.connection.generate_signed_url(resource=resource, | ||
| expiration=expiration, | ||
| method=method) | ||
| def exists(self): | ||
| """Determines whether or not this key exists. | ||
This comment was marked as spam.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as spam.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.