Index: boto/cloudfront/distribution.py |
diff --git a/boto/cloudfront/distribution.py b/boto/cloudfront/distribution.py |
index ed245cbc4e951e0a510366991f9025736b83edb7..01ceed490ea760fab9b504695ba91453e8858e50 100644 |
--- a/boto/cloudfront/distribution.py |
+++ b/boto/cloudfront/distribution.py |
@@ -20,6 +20,8 @@ |
# IN THE SOFTWARE. |
import uuid |
+import base64 |
+import json |
from boto.cloudfront.identity import OriginAccessIdentity |
from boto.cloudfront.object import Object, StreamingObject |
from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners |
@@ -286,6 +288,7 @@ class Distribution: |
self.id = id |
self.last_modified_time = last_modified_time |
self.status = status |
+ self.in_progress_invalidation_batches = 0 |
self.active_signers = None |
self.etag = None |
self._bucket = None |
@@ -308,6 +311,8 @@ class Distribution: |
self.last_modified_time = value |
elif name == 'Status': |
self.status = value |
+ elif name == 'InProgressInvalidationBatches': |
+ self.in_progress_invalidation_batches = int(value) |
elif name == 'DomainName': |
self.domain_name = value |
else: |
@@ -316,12 +321,18 @@ class Distribution: |
def update(self, enabled=None, cnames=None, comment=None): |
""" |
Update the configuration of the Distribution. The only values |
- of the DistributionConfig that can be updated are: |
+ of the DistributionConfig that can be directly updated are: |
* CNAMES |
* Comment |
* Whether the Distribution is enabled or not |
+ Any changes to the ``trusted_signers`` or ``origin`` properties of |
+ this distribution's current config object will also be included in |
+ the update. Therefore, to set the origin access identity for this |
+ distribution, set ``Distribution.config.origin.origin_access_identity`` |
+ before calling this update method. |
+ |
:type enabled: bool |
:param enabled: Whether the Distribution is active or not. |
@@ -371,19 +382,23 @@ class Distribution: |
self.connection.delete_distribution(self.id, self.etag) |
def _get_bucket(self): |
- if not self._bucket: |
- bucket_name = self.config.origin.replace('.s3.amazonaws.com', '') |
- from boto.s3.connection import S3Connection |
- s3 = S3Connection(self.connection.aws_access_key_id, |
- self.connection.aws_secret_access_key, |
- proxy=self.connection.proxy, |
- proxy_port=self.connection.proxy_port, |
- proxy_user=self.connection.proxy_user, |
- proxy_pass=self.connection.proxy_pass) |
- self._bucket = s3.get_bucket(bucket_name) |
- self._bucket.distribution = self |
- self._bucket.set_key_class(self._object_class) |
- return self._bucket |
+ if isinstance(self.config.origin, S3Origin): |
+ if not self._bucket: |
+ bucket_dns_name = self.config.origin.dns_name |
+ bucket_name = bucket_dns_name.replace('.s3.amazonaws.com', '') |
+ from boto.s3.connection import S3Connection |
+ s3 = S3Connection(self.connection.aws_access_key_id, |
+ self.connection.aws_secret_access_key, |
+ proxy=self.connection.proxy, |
+ proxy_port=self.connection.proxy_port, |
+ proxy_user=self.connection.proxy_user, |
+ proxy_pass=self.connection.proxy_pass) |
+ self._bucket = s3.get_bucket(bucket_name) |
+ self._bucket.distribution = self |
+ self._bucket.set_key_class(self._object_class) |
+ return self._bucket |
+ else: |
+ raise NotImplementedError('Unable to get_objects on CustomOrigin') |
def get_objects(self): |
""" |
@@ -469,17 +484,198 @@ class Distribution: |
:rtype: :class:`boto.cloudfront.object.Object` |
:return: The newly created object. |
""" |
- if self.config.origin_access_identity: |
+ if self.config.origin.origin_access_identity: |
policy = 'private' |
else: |
policy = 'public-read' |
bucket = self._get_bucket() |
object = bucket.new_key(name) |
object.set_contents_from_file(content, headers=headers, policy=policy) |
- if self.config.origin_access_identity: |
+ if self.config.origin.origin_access_identity: |
self.set_permissions(object, replace) |
return object |
- |
+ |
+ def create_signed_url(self, url, keypair_id, |
+ expire_time=None, valid_after_time=None, |
+ ip_address=None, policy_url=None, |
+ private_key_file=None, private_key_string=None): |
+ """ |
+ Creates a signed CloudFront URL that is only valid within the specified |
+ parameters. |
+ |
+ :type url: str |
+ :param url: The URL of the protected object. |
+ |
+ :type keypair_id: str |
+ :param keypair_id: The keypair ID of the Amazon KeyPair used to sign |
+ theURL. This ID MUST correspond to the private key |
+ specified with private_key_file or |
+ private_key_string. |
+ |
+ :type expire_time: int |
+ :param expire_time: The expiry time of the URL. If provided, the URL |
+ will expire after the time has passed. If not |
+ provided the URL will never expire. Format is a |
+ unix epoch. Use time.time() + duration_in_sec. |
+ |
+ :type valid_after_time: int |
+ :param valid_after_time: If provided, the URL will not be valid until |
+ after valid_after_time. Format is a unix |
+ epoch. Use time.time() + secs_until_valid. |
+ |
+ :type ip_address: str |
+ :param ip_address: If provided, only allows access from the specified |
+ IP address. Use '192.168.0.10' for a single IP or |
+ use '192.168.0.0/24' CIDR notation for a subnet. |
+ |
+ :type policy_url: str |
+ :param policy_url: If provided, allows the signature to contain |
+ wildcard globs in the URL. For example, you could |
+ provide: 'http://example.com/media/*' and the policy |
+ and signature would allow access to all contents of |
+ the media subdirectory. If not specified, only |
+ allow access to the exact url provided in 'url'. |
+ |
+ :type private_key_file: str or file object. |
+ :param private_key_file: If provided, contains the filename of the |
+ private key file used for signing or an open |
+ file object containing the private key |
+ contents. Only one of private_key_file or |
+ private_key_string can be provided. |
+ |
+ :type private_key_string: str |
+ :param private_key_string: If provided, contains the private key string |
+ used for signing. Only one of |
+ private_key_file or private_key_string can |
+ be provided. |
+ |
+ :rtype: str |
+ :return: The signed URL. |
+ """ |
+ # Get the required parameters |
+ params = self._create_signing_params( |
+ url=url, keypair_id=keypair_id, expire_time=expire_time, |
+ valid_after_time=valid_after_time, ip_address=ip_address, |
+ policy_url=policy_url, private_key_file=private_key_file, |
+ private_key_string=private_key_string) |
+ |
+ #combine these into a full url |
+ if "?" in url: |
+ sep = "&" |
+ else: |
+ sep = "?" |
+ signed_url_params = [] |
+ for key in ["Expires", "Policy", "Signature", "Key-Pair-Id"]: |
+ if key in params: |
+ param = "%s=%s" % (key, params[key]) |
+ signed_url_params.append(param) |
+ signed_url = url + sep + "&".join(signed_url_params) |
+ return signed_url |
+ |
+ def _create_signing_params(self, url, keypair_id, |
+ expire_time=None, valid_after_time=None, |
+ ip_address=None, policy_url=None, |
+ private_key_file=None, private_key_string=None): |
+ """ |
+ Creates the required URL parameters for a signed URL. |
+ """ |
+ params = {} |
+ # Check if we can use a canned policy |
+ if expire_time and not valid_after_time and not ip_address and not policy_url: |
+ # we manually construct this policy string to ensure formatting |
+ # matches signature |
+ policy = self._canned_policy(url, expire_time) |
+ params["Expires"] = str(expire_time) |
+ else: |
+ # If no policy_url is specified, default to the full url. |
+ if policy_url is None: |
+ policy_url = url |
+ # Can't use canned policy |
+ policy = self._custom_policy(policy_url, expires=None, |
+ valid_after=None, |
+ ip_address=None) |
+ encoded_policy = self._url_base64_encode(policy) |
+ params["Policy"] = encoded_policy |
+ #sign the policy |
+ signature = self._sign_string(policy, private_key_file, private_key_string) |
+ #now base64 encode the signature (URL safe as well) |
+ encoded_signature = self._url_base64_encode(signature) |
+ params["Signature"] = encoded_signature |
+ params["Key-Pair-Id"] = keypair_id |
+ return params |
+ |
+ @staticmethod |
+ def _canned_policy(resource, expires): |
+ """ |
+ Creates a canned policy string. |
+ """ |
+ policy = ('{"Statement":[{"Resource":"%(resource)s",' |
+ '"Condition":{"DateLessThan":{"AWS:EpochTime":' |
+ '%(expires)s}}}]}' % locals()) |
+ return policy |
+ |
+ @staticmethod |
+ def _custom_policy(resource, expires=None, valid_after=None, ip_address=None): |
+ """ |
+ Creates a custom policy string based on the supplied parameters. |
+ """ |
+ condition = {} |
+ if expires: |
+ condition["DateLessThan"] = {"AWS:EpochTime": expires} |
+ if valid_after: |
+ condition["DateGreaterThan"] = {"AWS:EpochTime": valid_after} |
+ if ip_address: |
+ if '/' not in ip_address: |
+ ip_address += "/32" |
+ condition["IpAddress"] = {"AWS:SourceIp": ip_address} |
+ policy = {"Statement": [{ |
+ "Resource": resource, |
+ "Condition": condition}]} |
+ return json.dumps(policy, separators=(",", ":")) |
+ |
+ @staticmethod |
+ def _sign_string(message, private_key_file=None, private_key_string=None): |
+ """ |
+ Signs a string for use with Amazon CloudFront. Requires the M2Crypto |
+ library be installed. |
+ """ |
+ try: |
+ from M2Crypto import EVP |
+ except ImportError: |
+ raise NotImplementedError("Boto depends on the python M2Crypto " |
+ "library to generate signed URLs for " |
+ "CloudFront") |
+ # Make sure only one of private_key_file and private_key_string is set |
+ if private_key_file and private_key_string: |
+ raise ValueError("Only specify the private_key_file or the private_key_string not both") |
+ if not private_key_file and not private_key_string: |
+ raise ValueError("You must specify one of private_key_file or private_key_string") |
+ # if private_key_file is a file object read the key string from there |
+ if isinstance(private_key_file, file): |
+ private_key_string = private_key_file.read() |
+ # Now load key and calculate signature |
+ if private_key_string: |
+ key = EVP.load_key_string(private_key_string) |
+ else: |
+ key = EVP.load_key(private_key_file) |
+ key.reset_context(md='sha1') |
+ key.sign_init() |
+ key.sign_update(str(message)) |
+ signature = key.sign_final() |
+ return signature |
+ |
+ @staticmethod |
+ def _url_base64_encode(msg): |
+ """ |
+ Base64 encodes a string using the URL-safe characters specified by |
+ Amazon. |
+ """ |
+ msg_base64 = base64.b64encode(msg) |
+ msg_base64 = msg_base64.replace('+', '-') |
+ msg_base64 = msg_base64.replace('=', '_') |
+ msg_base64 = msg_base64.replace('/', '~') |
+ return msg_base64 |
+ |
class StreamingDistribution(Distribution): |
def __init__(self, connection=None, config=None, domain_name='', |
@@ -498,12 +694,19 @@ class StreamingDistribution(Distribution): |
def update(self, enabled=None, cnames=None, comment=None): |
""" |
Update the configuration of the StreamingDistribution. The only values |
- of the StreamingDistributionConfig that can be updated are: |
+ of the StreamingDistributionConfig that can be directly updated are: |
* CNAMES |
* Comment |
* Whether the Distribution is enabled or not |
+ Any changes to the ``trusted_signers`` or ``origin`` properties of |
+ this distribution's current config object will also be included in |
+ the update. Therefore, to set the origin access identity for this |
+ distribution, set |
+ ``StreamingDistribution.config.origin.origin_access_identity`` |
+ before calling this update method. |
+ |
:type enabled: bool |
:param enabled: Whether the StreamingDistribution is active or not. |