| 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.
|
|
|
|
|