|
| 1 | +import base64 |
| 2 | +import datetime |
1 | 3 | import httplib2 |
2 | 4 | import json |
| 5 | +import time |
3 | 6 | import urllib |
4 | 7 |
|
| 8 | +from Crypto.Hash import SHA256 |
| 9 | +from Crypto.PublicKey import RSA |
| 10 | +from Crypto.Signature import PKCS1_v1_5 |
| 11 | +from OpenSSL import crypto |
| 12 | +import pytz |
5 | 13 |
|
6 | 14 | from gcloud import connection |
7 | 15 | from gcloud.storage import exceptions |
@@ -57,6 +65,8 @@ class Connection(connection.Connection): |
57 | 65 | API_URL_TEMPLATE = '{api_base_url}/storage/{api_version}{path}' |
58 | 66 | """A template used to craft the URL pointing toward a particular API call.""" |
59 | 67 |
|
| 68 | + API_ACCESS_ENDPOINT = 'https://storage.googleapis.com' |
| 69 | + |
60 | 70 | def __init__(self, project_name, *args, **kwargs): |
61 | 71 | """ |
62 | 72 | :type project_name: string |
@@ -400,3 +410,82 @@ def new_bucket(self, bucket): |
400 | 410 | return Bucket(connection=self, name=bucket) |
401 | 411 |
|
402 | 412 | raise TypeError('Invalid bucket: %s' % bucket) |
| 413 | + |
| 414 | + def generate_signed_url(self, resource, expiration, method='GET', content_md5=None, content_type=None): |
| 415 | + """Generate a signed URL to provide query-string authentication to a resource. |
| 416 | +
|
| 417 | + :type resource: string |
| 418 | + :param resource: A pointer to a specific resource |
| 419 | + (typically, ``/bucket-name/path/to/key.txt``). |
| 420 | +
|
| 421 | + :type expiration: int, long, datetime.datetime, datetime.timedelta |
| 422 | + :param expiration: When the signed URL should expire. |
| 423 | +
|
| 424 | + :type method: string |
| 425 | + :param method: The HTTP verb that will be used when requesting the URL. |
| 426 | +
|
| 427 | + :type content_md5: string |
| 428 | + :param content_md5: The MD5 hash of the object referenced by ``resource``. |
| 429 | +
|
| 430 | + :type content_type: string |
| 431 | + :param content_type: The content type of the object referenced by |
| 432 | + ``resource``. |
| 433 | +
|
| 434 | + :rtype: string |
| 435 | + :returns: A signed URL you can use to access the resource until expiration. |
| 436 | + """ |
| 437 | + |
| 438 | + # expiration can be an absolute timestamp (int, long), |
| 439 | + # an absolute time (datetime.datetime), |
| 440 | + # or a relative time (datetime.timedelta). |
| 441 | + # We should convert all of these into an absolute timestamp. |
| 442 | + |
| 443 | + # If it's a timedelta, add it to `now` in UTC. |
| 444 | + if isinstance(expiration, datetime.timedelta): |
| 445 | + now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) |
| 446 | + expiration = now + expiration |
| 447 | + |
| 448 | + # If it's a datetime, convert to a timestamp. |
| 449 | + if isinstance(expiration, datetime.datetime): |
| 450 | + # Make sure the timezone on the value is UTC |
| 451 | + # (either by converting or replacing the value). |
| 452 | + if expiration.tzinfo: |
| 453 | + expiration = expiration.astimezone(pytz.utc) |
| 454 | + else: |
| 455 | + expiration = expiration.replace(tzinfo=pytz.utc) |
| 456 | + |
| 457 | + # Turn the datetime into a timestamp (seconds, not microseconds). |
| 458 | + expiration = int(time.mktime(expiration.timetuple())) |
| 459 | + |
| 460 | + if not isinstance(expiration, (int, long)): |
| 461 | + raise ValueError('Expected an integer timestamp, datetime, or timedelta. ' |
| 462 | + 'Got %s' % type(expiration)) |
| 463 | + |
| 464 | + # Generate the string to sign. |
| 465 | + signature_string = '\n'.join([ |
| 466 | + method, |
| 467 | + content_md5 or '', |
| 468 | + content_type or '', |
| 469 | + str(expiration), |
| 470 | + resource]) |
| 471 | + |
| 472 | + # Take our PKCS12 (.p12) key and make it into a RSA key we can use... |
| 473 | + pkcs12 = crypto.load_pkcs12(base64.b64decode(self.credentials.private_key), 'notasecret') |
| 474 | + pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkcs12.get_privatekey()) |
| 475 | + pem_key = RSA.importKey(pem) |
| 476 | + |
| 477 | + # Sign the string with the RSA key. |
| 478 | + signer = PKCS1_v1_5.new(pem_key) |
| 479 | + signature_hash = SHA256.new(signature_string) |
| 480 | + signature_bytes = signer.sign(signature_hash) |
| 481 | + signature = base64.b64encode(signature_bytes) |
| 482 | + |
| 483 | + # Set the right query parameters. |
| 484 | + query_params = {'GoogleAccessId': self.credentials.service_account_name, |
| 485 | + 'Expires': str(expiration), |
| 486 | + 'Signature': signature} |
| 487 | + |
| 488 | + # Return the built URL. |
| 489 | + return '{endpoint}{resource}?{querystring}'.format( |
| 490 | + endpoint=self.API_ACCESS_ENDPOINT, resource=resource, |
| 491 | + querystring=urllib.urlencode(query_params)) |
0 commit comments