OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ |
| 2 # |
| 3 # Permission is hereby granted, free of charge, to any person obtaining a |
| 4 # copy of this software and associated documentation files (the |
| 5 # "Software"), to deal in the Software without restriction, including |
| 6 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 7 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 8 # persons to whom the Software is furnished to do so, subject to the fol- |
| 9 # lowing conditions: |
| 10 # |
| 11 # The above copyright notice and this permission notice shall be included |
| 12 # in all copies or substantial portions of the Software. |
| 13 # |
| 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 17 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 18 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 20 # IN THE SOFTWARE. |
| 21 |
| 22 import uuid |
| 23 import base64 |
| 24 import time |
| 25 from boto.compat import json |
| 26 from boto.cloudfront.identity import OriginAccessIdentity |
| 27 from boto.cloudfront.object import Object, StreamingObject |
| 28 from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners |
| 29 from boto.cloudfront.logging import LoggingInfo |
| 30 from boto.cloudfront.origin import S3Origin, CustomOrigin |
| 31 from boto.s3.acl import ACL |
| 32 |
| 33 class DistributionConfig: |
| 34 |
| 35 def __init__(self, connection=None, origin=None, enabled=False, |
| 36 caller_reference='', cnames=None, comment='', |
| 37 trusted_signers=None, default_root_object=None, |
| 38 logging=None): |
| 39 """ |
| 40 :param origin: Origin information to associate with the |
| 41 distribution. If your distribution will use |
| 42 an Amazon S3 origin, then this should be an |
| 43 S3Origin object. If your distribution will use |
| 44 a custom origin (non Amazon S3), then this |
| 45 should be a CustomOrigin object. |
| 46 :type origin: :class:`boto.cloudfront.origin.S3Origin` or |
| 47 :class:`boto.cloudfront.origin.CustomOrigin` |
| 48 |
| 49 :param enabled: Whether the distribution is enabled to accept |
| 50 end user requests for content. |
| 51 :type enabled: bool |
| 52 |
| 53 :param caller_reference: A unique number that ensures the |
| 54 request can't be replayed. If no |
| 55 caller_reference is provided, boto |
| 56 will generate a type 4 UUID for use |
| 57 as the caller reference. |
| 58 :type enabled: str |
| 59 |
| 60 :param cnames: A CNAME alias you want to associate with this |
| 61 distribution. You can have up to 10 CNAME aliases |
| 62 per distribution. |
| 63 :type enabled: array of str |
| 64 |
| 65 :param comment: Any comments you want to include about the |
| 66 distribution. |
| 67 :type comment: str |
| 68 |
| 69 :param trusted_signers: Specifies any AWS accounts you want to |
| 70 permit to create signed URLs for private |
| 71 content. If you want the distribution to |
| 72 use signed URLs, this should contain a |
| 73 TrustedSigners object; if you want the |
| 74 distribution to use basic URLs, leave |
| 75 this None. |
| 76 :type trusted_signers: :class`boto.cloudfront.signers.TrustedSigners` |
| 77 |
| 78 :param default_root_object: Designates a default root object. |
| 79 Only include a DefaultRootObject value |
| 80 if you are going to assign a default |
| 81 root object for the distribution. |
| 82 :type comment: str |
| 83 |
| 84 :param logging: Controls whether access logs are written for the |
| 85 distribution. If you want to turn on access logs, |
| 86 this should contain a LoggingInfo object; otherwise |
| 87 it should contain None. |
| 88 :type logging: :class`boto.cloudfront.logging.LoggingInfo` |
| 89 |
| 90 """ |
| 91 self.connection = connection |
| 92 self.origin = origin |
| 93 self.enabled = enabled |
| 94 if caller_reference: |
| 95 self.caller_reference = caller_reference |
| 96 else: |
| 97 self.caller_reference = str(uuid.uuid4()) |
| 98 self.cnames = [] |
| 99 if cnames: |
| 100 self.cnames = cnames |
| 101 self.comment = comment |
| 102 self.trusted_signers = trusted_signers |
| 103 self.logging = None |
| 104 self.default_root_object = default_root_object |
| 105 |
| 106 def to_xml(self): |
| 107 s = '<?xml version="1.0" encoding="UTF-8"?>\n' |
| 108 s += '<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/201
0-07-15/">\n' |
| 109 if self.origin: |
| 110 s += self.origin.to_xml() |
| 111 s += ' <CallerReference>%s</CallerReference>\n' % self.caller_reference |
| 112 for cname in self.cnames: |
| 113 s += ' <CNAME>%s</CNAME>\n' % cname |
| 114 if self.comment: |
| 115 s += ' <Comment>%s</Comment>\n' % self.comment |
| 116 s += ' <Enabled>' |
| 117 if self.enabled: |
| 118 s += 'true' |
| 119 else: |
| 120 s += 'false' |
| 121 s += '</Enabled>\n' |
| 122 if self.trusted_signers: |
| 123 s += '<TrustedSigners>\n' |
| 124 for signer in self.trusted_signers: |
| 125 if signer == 'Self': |
| 126 s += ' <Self></Self>\n' |
| 127 else: |
| 128 s += ' <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer |
| 129 s += '</TrustedSigners>\n' |
| 130 if self.logging: |
| 131 s += '<Logging>\n' |
| 132 s += ' <Bucket>%s</Bucket>\n' % self.logging.bucket |
| 133 s += ' <Prefix>%s</Prefix>\n' % self.logging.prefix |
| 134 s += '</Logging>\n' |
| 135 if self.default_root_object: |
| 136 dro = self.default_root_object |
| 137 s += '<DefaultRootObject>%s</DefaultRootObject>\n' % dro |
| 138 s += '</DistributionConfig>\n' |
| 139 return s |
| 140 |
| 141 def startElement(self, name, attrs, connection): |
| 142 if name == 'TrustedSigners': |
| 143 self.trusted_signers = TrustedSigners() |
| 144 return self.trusted_signers |
| 145 elif name == 'Logging': |
| 146 self.logging = LoggingInfo() |
| 147 return self.logging |
| 148 elif name == 'S3Origin': |
| 149 self.origin = S3Origin() |
| 150 return self.origin |
| 151 elif name == 'CustomOrigin': |
| 152 self.origin = CustomOrigin() |
| 153 return self.origin |
| 154 else: |
| 155 return None |
| 156 |
| 157 def endElement(self, name, value, connection): |
| 158 if name == 'CNAME': |
| 159 self.cnames.append(value) |
| 160 elif name == 'Comment': |
| 161 self.comment = value |
| 162 elif name == 'Enabled': |
| 163 if value.lower() == 'true': |
| 164 self.enabled = True |
| 165 else: |
| 166 self.enabled = False |
| 167 elif name == 'CallerReference': |
| 168 self.caller_reference = value |
| 169 elif name == 'DefaultRootObject': |
| 170 self.default_root_object = value |
| 171 else: |
| 172 setattr(self, name, value) |
| 173 |
| 174 class StreamingDistributionConfig(DistributionConfig): |
| 175 |
| 176 def __init__(self, connection=None, origin='', enabled=False, |
| 177 caller_reference='', cnames=None, comment='', |
| 178 trusted_signers=None, logging=None): |
| 179 DistributionConfig.__init__(self, connection=connection, |
| 180 origin=origin, enabled=enabled, |
| 181 caller_reference=caller_reference, |
| 182 cnames=cnames, comment=comment, |
| 183 trusted_signers=trusted_signers, |
| 184 logging=logging) |
| 185 def to_xml(self): |
| 186 s = '<?xml version="1.0" encoding="UTF-8"?>\n' |
| 187 s += '<StreamingDistributionConfig xmlns="http://cloudfront.amazonaws.co
m/doc/2010-07-15/">\n' |
| 188 if self.origin: |
| 189 s += self.origin.to_xml() |
| 190 s += ' <CallerReference>%s</CallerReference>\n' % self.caller_reference |
| 191 for cname in self.cnames: |
| 192 s += ' <CNAME>%s</CNAME>\n' % cname |
| 193 if self.comment: |
| 194 s += ' <Comment>%s</Comment>\n' % self.comment |
| 195 s += ' <Enabled>' |
| 196 if self.enabled: |
| 197 s += 'true' |
| 198 else: |
| 199 s += 'false' |
| 200 s += '</Enabled>\n' |
| 201 if self.trusted_signers: |
| 202 s += '<TrustedSigners>\n' |
| 203 for signer in self.trusted_signers: |
| 204 if signer == 'Self': |
| 205 s += ' <Self/>\n' |
| 206 else: |
| 207 s += ' <AwsAccountNumber>%s</AwsAccountNumber>\n' % signer |
| 208 s += '</TrustedSigners>\n' |
| 209 if self.logging: |
| 210 s += '<Logging>\n' |
| 211 s += ' <Bucket>%s</Bucket>\n' % self.logging.bucket |
| 212 s += ' <Prefix>%s</Prefix>\n' % self.logging.prefix |
| 213 s += '</Logging>\n' |
| 214 s += '</StreamingDistributionConfig>\n' |
| 215 return s |
| 216 |
| 217 class DistributionSummary: |
| 218 |
| 219 def __init__(self, connection=None, domain_name='', id='', |
| 220 last_modified_time=None, status='', origin=None, |
| 221 cname='', comment='', enabled=False): |
| 222 self.connection = connection |
| 223 self.domain_name = domain_name |
| 224 self.id = id |
| 225 self.last_modified_time = last_modified_time |
| 226 self.status = status |
| 227 self.origin = origin |
| 228 self.enabled = enabled |
| 229 self.cnames = [] |
| 230 if cname: |
| 231 self.cnames.append(cname) |
| 232 self.comment = comment |
| 233 self.trusted_signers = None |
| 234 self.etag = None |
| 235 self.streaming = False |
| 236 |
| 237 def startElement(self, name, attrs, connection): |
| 238 if name == 'TrustedSigners': |
| 239 self.trusted_signers = TrustedSigners() |
| 240 return self.trusted_signers |
| 241 elif name == 'S3Origin': |
| 242 self.origin = S3Origin() |
| 243 return self.origin |
| 244 elif name == 'CustomOrigin': |
| 245 self.origin = CustomOrigin() |
| 246 return self.origin |
| 247 return None |
| 248 |
| 249 def endElement(self, name, value, connection): |
| 250 if name == 'Id': |
| 251 self.id = value |
| 252 elif name == 'Status': |
| 253 self.status = value |
| 254 elif name == 'LastModifiedTime': |
| 255 self.last_modified_time = value |
| 256 elif name == 'DomainName': |
| 257 self.domain_name = value |
| 258 elif name == 'Origin': |
| 259 self.origin = value |
| 260 elif name == 'CNAME': |
| 261 self.cnames.append(value) |
| 262 elif name == 'Comment': |
| 263 self.comment = value |
| 264 elif name == 'Enabled': |
| 265 if value.lower() == 'true': |
| 266 self.enabled = True |
| 267 else: |
| 268 self.enabled = False |
| 269 elif name == 'StreamingDistributionSummary': |
| 270 self.streaming = True |
| 271 else: |
| 272 setattr(self, name, value) |
| 273 |
| 274 def get_distribution(self): |
| 275 return self.connection.get_distribution_info(self.id) |
| 276 |
| 277 class StreamingDistributionSummary(DistributionSummary): |
| 278 |
| 279 def get_distribution(self): |
| 280 return self.connection.get_streaming_distribution_info(self.id) |
| 281 |
| 282 class Distribution: |
| 283 |
| 284 def __init__(self, connection=None, config=None, domain_name='', |
| 285 id='', last_modified_time=None, status=''): |
| 286 self.connection = connection |
| 287 self.config = config |
| 288 self.domain_name = domain_name |
| 289 self.id = id |
| 290 self.last_modified_time = last_modified_time |
| 291 self.status = status |
| 292 self.in_progress_invalidation_batches = 0 |
| 293 self.active_signers = None |
| 294 self.etag = None |
| 295 self._bucket = None |
| 296 self._object_class = Object |
| 297 |
| 298 def startElement(self, name, attrs, connection): |
| 299 if name == 'DistributionConfig': |
| 300 self.config = DistributionConfig() |
| 301 return self.config |
| 302 elif name == 'ActiveTrustedSigners': |
| 303 self.active_signers = ActiveTrustedSigners() |
| 304 return self.active_signers |
| 305 else: |
| 306 return None |
| 307 |
| 308 def endElement(self, name, value, connection): |
| 309 if name == 'Id': |
| 310 self.id = value |
| 311 elif name == 'LastModifiedTime': |
| 312 self.last_modified_time = value |
| 313 elif name == 'Status': |
| 314 self.status = value |
| 315 elif name == 'InProgressInvalidationBatches': |
| 316 self.in_progress_invalidation_batches = int(value) |
| 317 elif name == 'DomainName': |
| 318 self.domain_name = value |
| 319 else: |
| 320 setattr(self, name, value) |
| 321 |
| 322 def update(self, enabled=None, cnames=None, comment=None): |
| 323 """ |
| 324 Update the configuration of the Distribution. The only values |
| 325 of the DistributionConfig that can be directly updated are: |
| 326 |
| 327 * CNAMES |
| 328 * Comment |
| 329 * Whether the Distribution is enabled or not |
| 330 |
| 331 Any changes to the ``trusted_signers`` or ``origin`` properties of |
| 332 this distribution's current config object will also be included in |
| 333 the update. Therefore, to set the origin access identity for this |
| 334 distribution, set ``Distribution.config.origin.origin_access_identity`` |
| 335 before calling this update method. |
| 336 |
| 337 :type enabled: bool |
| 338 :param enabled: Whether the Distribution is active or not. |
| 339 |
| 340 :type cnames: list of str |
| 341 :param cnames: The DNS CNAME's associated with this |
| 342 Distribution. Maximum of 10 values. |
| 343 |
| 344 :type comment: str or unicode |
| 345 :param comment: The comment associated with the Distribution. |
| 346 |
| 347 """ |
| 348 new_config = DistributionConfig(self.connection, self.config.origin, |
| 349 self.config.enabled, self.config.caller_
reference, |
| 350 self.config.cnames, self.config.comment, |
| 351 self.config.trusted_signers, |
| 352 self.config.default_root_object) |
| 353 if enabled != None: |
| 354 new_config.enabled = enabled |
| 355 if cnames != None: |
| 356 new_config.cnames = cnames |
| 357 if comment != None: |
| 358 new_config.comment = comment |
| 359 self.etag = self.connection.set_distribution_config(self.id, self.etag,
new_config) |
| 360 self.config = new_config |
| 361 self._object_class = Object |
| 362 |
| 363 def enable(self): |
| 364 """ |
| 365 Deactivate the Distribution. A convenience wrapper around |
| 366 the update method. |
| 367 """ |
| 368 self.update(enabled=True) |
| 369 |
| 370 def disable(self): |
| 371 """ |
| 372 Activate the Distribution. A convenience wrapper around |
| 373 the update method. |
| 374 """ |
| 375 self.update(enabled=False) |
| 376 |
| 377 def delete(self): |
| 378 """ |
| 379 Delete this CloudFront Distribution. The content |
| 380 associated with the Distribution is not deleted from |
| 381 the underlying Origin bucket in S3. |
| 382 """ |
| 383 self.connection.delete_distribution(self.id, self.etag) |
| 384 |
| 385 def _get_bucket(self): |
| 386 if isinstance(self.config.origin, S3Origin): |
| 387 if not self._bucket: |
| 388 bucket_dns_name = self.config.origin.dns_name |
| 389 bucket_name = bucket_dns_name.replace('.s3.amazonaws.com', '') |
| 390 from boto.s3.connection import S3Connection |
| 391 s3 = S3Connection(self.connection.aws_access_key_id, |
| 392 self.connection.aws_secret_access_key, |
| 393 proxy=self.connection.proxy, |
| 394 proxy_port=self.connection.proxy_port, |
| 395 proxy_user=self.connection.proxy_user, |
| 396 proxy_pass=self.connection.proxy_pass) |
| 397 self._bucket = s3.get_bucket(bucket_name) |
| 398 self._bucket.distribution = self |
| 399 self._bucket.set_key_class(self._object_class) |
| 400 return self._bucket |
| 401 else: |
| 402 raise NotImplementedError('Unable to get_objects on CustomOrigin') |
| 403 |
| 404 def get_objects(self): |
| 405 """ |
| 406 Return a list of all content objects in this distribution. |
| 407 |
| 408 :rtype: list of :class:`boto.cloudfront.object.Object` |
| 409 :return: The content objects |
| 410 """ |
| 411 bucket = self._get_bucket() |
| 412 objs = [] |
| 413 for key in bucket: |
| 414 objs.append(key) |
| 415 return objs |
| 416 |
| 417 def set_permissions(self, object, replace=False): |
| 418 """ |
| 419 Sets the S3 ACL grants for the given object to the appropriate |
| 420 value based on the type of Distribution. If the Distribution |
| 421 is serving private content the ACL will be set to include the |
| 422 Origin Access Identity associated with the Distribution. If |
| 423 the Distribution is serving public content the content will |
| 424 be set up with "public-read". |
| 425 |
| 426 :type object: :class:`boto.cloudfront.object.Object` |
| 427 :param enabled: The Object whose ACL is being set |
| 428 |
| 429 :type replace: bool |
| 430 :param replace: If False, the Origin Access Identity will be |
| 431 appended to the existing ACL for the object. |
| 432 If True, the ACL for the object will be |
| 433 completely replaced with one that grants |
| 434 READ permission to the Origin Access Identity. |
| 435 |
| 436 """ |
| 437 if isinstance(self.config.origin, S3Origin): |
| 438 if self.config.origin.origin_access_identity: |
| 439 id = self.config.origin.origin_access_identity.split('/')[-1] |
| 440 oai = self.connection.get_origin_access_identity_info(id) |
| 441 policy = object.get_acl() |
| 442 if replace: |
| 443 policy.acl = ACL() |
| 444 policy.acl.add_user_grant('READ', oai.s3_user_id) |
| 445 object.set_acl(policy) |
| 446 else: |
| 447 object.set_canned_acl('public-read') |
| 448 |
| 449 def set_permissions_all(self, replace=False): |
| 450 """ |
| 451 Sets the S3 ACL grants for all objects in the Distribution |
| 452 to the appropriate value based on the type of Distribution. |
| 453 |
| 454 :type replace: bool |
| 455 :param replace: If False, the Origin Access Identity will be |
| 456 appended to the existing ACL for the object. |
| 457 If True, the ACL for the object will be |
| 458 completely replaced with one that grants |
| 459 READ permission to the Origin Access Identity. |
| 460 |
| 461 """ |
| 462 bucket = self._get_bucket() |
| 463 for key in bucket: |
| 464 self.set_permissions(key, replace) |
| 465 |
| 466 def add_object(self, name, content, headers=None, replace=True): |
| 467 """ |
| 468 Adds a new content object to the Distribution. The content |
| 469 for the object will be copied to a new Key in the S3 Bucket |
| 470 and the permissions will be set appropriately for the type |
| 471 of Distribution. |
| 472 |
| 473 :type name: str or unicode |
| 474 :param name: The name or key of the new object. |
| 475 |
| 476 :type content: file-like object |
| 477 :param content: A file-like object that contains the content |
| 478 for the new object. |
| 479 |
| 480 :type headers: dict |
| 481 :param headers: A dictionary containing additional headers |
| 482 you would like associated with the new |
| 483 object in S3. |
| 484 |
| 485 :rtype: :class:`boto.cloudfront.object.Object` |
| 486 :return: The newly created object. |
| 487 """ |
| 488 if self.config.origin.origin_access_identity: |
| 489 policy = 'private' |
| 490 else: |
| 491 policy = 'public-read' |
| 492 bucket = self._get_bucket() |
| 493 object = bucket.new_key(name) |
| 494 object.set_contents_from_file(content, headers=headers, policy=policy) |
| 495 if self.config.origin.origin_access_identity: |
| 496 self.set_permissions(object, replace) |
| 497 return object |
| 498 |
| 499 def create_signed_url(self, url, keypair_id, |
| 500 expire_time=None, valid_after_time=None, |
| 501 ip_address=None, policy_url=None, |
| 502 private_key_file=None, private_key_string=None): |
| 503 """ |
| 504 Creates a signed CloudFront URL that is only valid within the specified |
| 505 parameters. |
| 506 |
| 507 :type url: str |
| 508 :param url: The URL of the protected object. |
| 509 |
| 510 :type keypair_id: str |
| 511 :param keypair_id: The keypair ID of the Amazon KeyPair used to sign |
| 512 theURL. This ID MUST correspond to the private key |
| 513 specified with private_key_file or private_key_string. |
| 514 |
| 515 :type expire_time: int |
| 516 :param expire_time: The expiry time of the URL. If provided, the URL |
| 517 will expire after the time has passed. If not provided the URL will |
| 518 never expire. Format is a unix epoch. |
| 519 Use time.time() + duration_in_sec. |
| 520 |
| 521 :type valid_after_time: int |
| 522 :param valid_after_time: If provided, the URL will not be valid until |
| 523 after valid_after_time. Format is a unix epoch. |
| 524 Use time.time() + secs_until_valid. |
| 525 |
| 526 :type ip_address: str |
| 527 :param ip_address: If provided, only allows access from the specified |
| 528 IP address. Use '192.168.0.10' for a single IP or |
| 529 use '192.168.0.0/24' CIDR notation for a subnet. |
| 530 |
| 531 :type policy_url: str |
| 532 :param policy_url: If provided, allows the signature to contain |
| 533 wildcard globs in the URL. For example, you could |
| 534 provide: 'http://example.com/media/\*' and the policy |
| 535 and signature would allow access to all contents of |
| 536 the media subdirectory. If not specified, only |
| 537 allow access to the exact url provided in 'url'. |
| 538 |
| 539 :type private_key_file: str or file object. |
| 540 :param private_key_file: If provided, contains the filename of the |
| 541 private key file used for signing or an open |
| 542 file object containing the private key |
| 543 contents. Only one of private_key_file or |
| 544 private_key_string can be provided. |
| 545 |
| 546 :type private_key_string: str |
| 547 :param private_key_string: If provided, contains the private key string |
| 548 used for signing. Only one of private_key_file or |
| 549 private_key_string can be provided. |
| 550 |
| 551 :rtype: str |
| 552 :return: The signed URL. |
| 553 """ |
| 554 # Get the required parameters |
| 555 params = self._create_signing_params( |
| 556 url=url, keypair_id=keypair_id, expire_time=expire_time, |
| 557 valid_after_time=valid_after_time, ip_address=ip_address, |
| 558 policy_url=policy_url, private_key_file=private_key_file, |
| 559 private_key_string=private_key_string) |
| 560 |
| 561 #combine these into a full url |
| 562 if "?" in url: |
| 563 sep = "&" |
| 564 else: |
| 565 sep = "?" |
| 566 signed_url_params = [] |
| 567 for key in ["Expires", "Policy", "Signature", "Key-Pair-Id"]: |
| 568 if key in params: |
| 569 param = "%s=%s" % (key, params[key]) |
| 570 signed_url_params.append(param) |
| 571 signed_url = url + sep + "&".join(signed_url_params) |
| 572 return signed_url |
| 573 |
| 574 def _create_signing_params(self, url, keypair_id, |
| 575 expire_time=None, valid_after_time=None, |
| 576 ip_address=None, policy_url=None, |
| 577 private_key_file=None, private_key_string=None): |
| 578 """ |
| 579 Creates the required URL parameters for a signed URL. |
| 580 """ |
| 581 params = {} |
| 582 # Check if we can use a canned policy |
| 583 if expire_time and not valid_after_time and not ip_address and not polic
y_url: |
| 584 # we manually construct this policy string to ensure formatting |
| 585 # matches signature |
| 586 policy = self._canned_policy(url, expire_time) |
| 587 params["Expires"] = str(expire_time) |
| 588 else: |
| 589 # If no policy_url is specified, default to the full url. |
| 590 if policy_url is None: |
| 591 policy_url = url |
| 592 # Can't use canned policy |
| 593 policy = self._custom_policy(policy_url, expires=expire_time, |
| 594 valid_after=valid_after_time, |
| 595 ip_address=ip_address) |
| 596 |
| 597 encoded_policy = self._url_base64_encode(policy) |
| 598 params["Policy"] = encoded_policy |
| 599 #sign the policy |
| 600 signature = self._sign_string(policy, private_key_file, private_key_stri
ng) |
| 601 #now base64 encode the signature (URL safe as well) |
| 602 encoded_signature = self._url_base64_encode(signature) |
| 603 params["Signature"] = encoded_signature |
| 604 params["Key-Pair-Id"] = keypair_id |
| 605 return params |
| 606 |
| 607 @staticmethod |
| 608 def _canned_policy(resource, expires): |
| 609 """ |
| 610 Creates a canned policy string. |
| 611 """ |
| 612 policy = ('{"Statement":[{"Resource":"%(resource)s",' |
| 613 '"Condition":{"DateLessThan":{"AWS:EpochTime":' |
| 614 '%(expires)s}}}]}' % locals()) |
| 615 return policy |
| 616 |
| 617 @staticmethod |
| 618 def _custom_policy(resource, expires=None, valid_after=None, ip_address=None
): |
| 619 """ |
| 620 Creates a custom policy string based on the supplied parameters. |
| 621 """ |
| 622 condition = {} |
| 623 # SEE: http://docs.amazonwebservices.com/AmazonCloudFront/latest/Develop
erGuide/RestrictingAccessPrivateContent.html#CustomPolicy |
| 624 # The 'DateLessThan' property is required. |
| 625 if not expires: |
| 626 # Defaults to ONE day |
| 627 expires = int(time.time()) + 86400 |
| 628 condition["DateLessThan"] = {"AWS:EpochTime": expires} |
| 629 if valid_after: |
| 630 condition["DateGreaterThan"] = {"AWS:EpochTime": valid_after} |
| 631 if ip_address: |
| 632 if '/' not in ip_address: |
| 633 ip_address += "/32" |
| 634 condition["IpAddress"] = {"AWS:SourceIp": ip_address} |
| 635 policy = {"Statement": [{ |
| 636 "Resource": resource, |
| 637 "Condition": condition}]} |
| 638 return json.dumps(policy, separators=(",", ":")) |
| 639 |
| 640 @staticmethod |
| 641 def _sign_string(message, private_key_file=None, private_key_string=None): |
| 642 """ |
| 643 Signs a string for use with Amazon CloudFront. Requires the M2Crypto |
| 644 library be installed. |
| 645 """ |
| 646 try: |
| 647 from M2Crypto import EVP |
| 648 except ImportError: |
| 649 raise NotImplementedError("Boto depends on the python M2Crypto " |
| 650 "library to generate signed URLs for " |
| 651 "CloudFront") |
| 652 # Make sure only one of private_key_file and private_key_string is set |
| 653 if private_key_file and private_key_string: |
| 654 raise ValueError("Only specify the private_key_file or the private_k
ey_string not both") |
| 655 if not private_key_file and not private_key_string: |
| 656 raise ValueError("You must specify one of private_key_file or privat
e_key_string") |
| 657 # if private_key_file is a file object read the key string from there |
| 658 if isinstance(private_key_file, file): |
| 659 private_key_string = private_key_file.read() |
| 660 # Now load key and calculate signature |
| 661 if private_key_string: |
| 662 key = EVP.load_key_string(private_key_string) |
| 663 else: |
| 664 key = EVP.load_key(private_key_file) |
| 665 key.reset_context(md='sha1') |
| 666 key.sign_init() |
| 667 key.sign_update(str(message)) |
| 668 signature = key.sign_final() |
| 669 return signature |
| 670 |
| 671 @staticmethod |
| 672 def _url_base64_encode(msg): |
| 673 """ |
| 674 Base64 encodes a string using the URL-safe characters specified by |
| 675 Amazon. |
| 676 """ |
| 677 msg_base64 = base64.b64encode(msg) |
| 678 msg_base64 = msg_base64.replace('+', '-') |
| 679 msg_base64 = msg_base64.replace('=', '_') |
| 680 msg_base64 = msg_base64.replace('/', '~') |
| 681 return msg_base64 |
| 682 |
| 683 class StreamingDistribution(Distribution): |
| 684 |
| 685 def __init__(self, connection=None, config=None, domain_name='', |
| 686 id='', last_modified_time=None, status=''): |
| 687 Distribution.__init__(self, connection, config, domain_name, |
| 688 id, last_modified_time, status) |
| 689 self._object_class = StreamingObject |
| 690 |
| 691 def startElement(self, name, attrs, connection): |
| 692 if name == 'StreamingDistributionConfig': |
| 693 self.config = StreamingDistributionConfig() |
| 694 return self.config |
| 695 else: |
| 696 return Distribution.startElement(self, name, attrs, connection) |
| 697 |
| 698 def update(self, enabled=None, cnames=None, comment=None): |
| 699 """ |
| 700 Update the configuration of the StreamingDistribution. The only values |
| 701 of the StreamingDistributionConfig that can be directly updated are: |
| 702 |
| 703 * CNAMES |
| 704 * Comment |
| 705 * Whether the Distribution is enabled or not |
| 706 |
| 707 Any changes to the ``trusted_signers`` or ``origin`` properties of |
| 708 this distribution's current config object will also be included in |
| 709 the update. Therefore, to set the origin access identity for this |
| 710 distribution, set |
| 711 ``StreamingDistribution.config.origin.origin_access_identity`` |
| 712 before calling this update method. |
| 713 |
| 714 :type enabled: bool |
| 715 :param enabled: Whether the StreamingDistribution is active or not. |
| 716 |
| 717 :type cnames: list of str |
| 718 :param cnames: The DNS CNAME's associated with this |
| 719 Distribution. Maximum of 10 values. |
| 720 |
| 721 :type comment: str or unicode |
| 722 :param comment: The comment associated with the Distribution. |
| 723 |
| 724 """ |
| 725 new_config = StreamingDistributionConfig(self.connection, |
| 726 self.config.origin, |
| 727 self.config.enabled, |
| 728 self.config.caller_reference, |
| 729 self.config.cnames, |
| 730 self.config.comment, |
| 731 self.config.trusted_signers) |
| 732 if enabled != None: |
| 733 new_config.enabled = enabled |
| 734 if cnames != None: |
| 735 new_config.cnames = cnames |
| 736 if comment != None: |
| 737 new_config.comment = comment |
| 738 self.etag = self.connection.set_streaming_distribution_config(self.id, |
| 739 self.etag, |
| 740 new_config
) |
| 741 self.config = new_config |
| 742 self._object_class = StreamingObject |
| 743 |
| 744 def delete(self): |
| 745 self.connection.delete_streaming_distribution(self.id, self.etag) |
| 746 |
| 747 |
OLD | NEW |