Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gcloud/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ def __init__(self, credentials=None):

self._credentials = credentials

@property
def credentials(self):
return self._credentials

@property
def http(self):
"""A getter for the HTTP transport used in talking to the API.
Expand Down
89 changes: 89 additions & 0 deletions gcloud/storage/connection.py
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

This comment was marked as spam.

This comment was marked as spam.

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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

This comment was marked as spam.


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.

This comment was marked as spam.

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.

This comment was marked as spam.

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))
44 changes: 35 additions & 9 deletions gcloud/storage/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

This comment was marked as spam.

"""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.
Expand All @@ -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.
Expand Down