| OLD | NEW |
| 1 # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ | 1 # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/ |
| 2 # | 2 # |
| 3 # Permission is hereby granted, free of charge, to any person obtaining a | 3 # Permission is hereby granted, free of charge, to any person obtaining a |
| 4 # copy of this software and associated documentation files (the | 4 # copy of this software and associated documentation files (the |
| 5 # "Software"), to deal in the Software without restriction, including | 5 # "Software"), to deal in the Software without restriction, including |
| 6 # without limitation the rights to use, copy, modify, merge, publish, dis- | 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 | 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- | 8 # persons to whom the Software is furnished to do so, subject to the fol- |
| 9 # lowing conditions: | 9 # lowing conditions: |
| 10 # | 10 # |
| 11 # The above copyright notice and this permission notice shall be included | 11 # The above copyright notice and this permission notice shall be included |
| 12 # in all copies or substantial portions of the Software. | 12 # in all copies or substantial portions of the Software. |
| 13 # | 13 # |
| 14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | 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- | 15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 16 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT | 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, | 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, | 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 | 19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 20 # IN THE SOFTWARE. | 20 # IN THE SOFTWARE. |
| 21 | 21 |
| 22 import uuid | 22 import uuid |
| 23 import base64 |
| 24 import json |
| 23 from boto.cloudfront.identity import OriginAccessIdentity | 25 from boto.cloudfront.identity import OriginAccessIdentity |
| 24 from boto.cloudfront.object import Object, StreamingObject | 26 from boto.cloudfront.object import Object, StreamingObject |
| 25 from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners | 27 from boto.cloudfront.signers import ActiveTrustedSigners, TrustedSigners |
| 26 from boto.cloudfront.logging import LoggingInfo | 28 from boto.cloudfront.logging import LoggingInfo |
| 27 from boto.cloudfront.origin import S3Origin, CustomOrigin | 29 from boto.cloudfront.origin import S3Origin, CustomOrigin |
| 28 from boto.s3.acl import ACL | 30 from boto.s3.acl import ACL |
| 29 | 31 |
| 30 class DistributionConfig: | 32 class DistributionConfig: |
| 31 | 33 |
| 32 def __init__(self, connection=None, origin=None, enabled=False, | 34 def __init__(self, connection=None, origin=None, enabled=False, |
| (...skipping 246 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 279 class Distribution: | 281 class Distribution: |
| 280 | 282 |
| 281 def __init__(self, connection=None, config=None, domain_name='', | 283 def __init__(self, connection=None, config=None, domain_name='', |
| 282 id='', last_modified_time=None, status=''): | 284 id='', last_modified_time=None, status=''): |
| 283 self.connection = connection | 285 self.connection = connection |
| 284 self.config = config | 286 self.config = config |
| 285 self.domain_name = domain_name | 287 self.domain_name = domain_name |
| 286 self.id = id | 288 self.id = id |
| 287 self.last_modified_time = last_modified_time | 289 self.last_modified_time = last_modified_time |
| 288 self.status = status | 290 self.status = status |
| 291 self.in_progress_invalidation_batches = 0 |
| 289 self.active_signers = None | 292 self.active_signers = None |
| 290 self.etag = None | 293 self.etag = None |
| 291 self._bucket = None | 294 self._bucket = None |
| 292 self._object_class = Object | 295 self._object_class = Object |
| 293 | 296 |
| 294 def startElement(self, name, attrs, connection): | 297 def startElement(self, name, attrs, connection): |
| 295 if name == 'DistributionConfig': | 298 if name == 'DistributionConfig': |
| 296 self.config = DistributionConfig() | 299 self.config = DistributionConfig() |
| 297 return self.config | 300 return self.config |
| 298 elif name == 'ActiveTrustedSigners': | 301 elif name == 'ActiveTrustedSigners': |
| 299 self.active_signers = ActiveTrustedSigners() | 302 self.active_signers = ActiveTrustedSigners() |
| 300 return self.active_signers | 303 return self.active_signers |
| 301 else: | 304 else: |
| 302 return None | 305 return None |
| 303 | 306 |
| 304 def endElement(self, name, value, connection): | 307 def endElement(self, name, value, connection): |
| 305 if name == 'Id': | 308 if name == 'Id': |
| 306 self.id = value | 309 self.id = value |
| 307 elif name == 'LastModifiedTime': | 310 elif name == 'LastModifiedTime': |
| 308 self.last_modified_time = value | 311 self.last_modified_time = value |
| 309 elif name == 'Status': | 312 elif name == 'Status': |
| 310 self.status = value | 313 self.status = value |
| 314 elif name == 'InProgressInvalidationBatches': |
| 315 self.in_progress_invalidation_batches = int(value) |
| 311 elif name == 'DomainName': | 316 elif name == 'DomainName': |
| 312 self.domain_name = value | 317 self.domain_name = value |
| 313 else: | 318 else: |
| 314 setattr(self, name, value) | 319 setattr(self, name, value) |
| 315 | 320 |
| 316 def update(self, enabled=None, cnames=None, comment=None): | 321 def update(self, enabled=None, cnames=None, comment=None): |
| 317 """ | 322 """ |
| 318 Update the configuration of the Distribution. The only values | 323 Update the configuration of the Distribution. The only values |
| 319 of the DistributionConfig that can be updated are: | 324 of the DistributionConfig that can be directly updated are: |
| 320 | 325 |
| 321 * CNAMES | 326 * CNAMES |
| 322 * Comment | 327 * Comment |
| 323 * Whether the Distribution is enabled or not | 328 * Whether the Distribution is enabled or not |
| 324 | 329 |
| 330 Any changes to the ``trusted_signers`` or ``origin`` properties of |
| 331 this distribution's current config object will also be included in |
| 332 the update. Therefore, to set the origin access identity for this |
| 333 distribution, set ``Distribution.config.origin.origin_access_identity`` |
| 334 before calling this update method. |
| 335 |
| 325 :type enabled: bool | 336 :type enabled: bool |
| 326 :param enabled: Whether the Distribution is active or not. | 337 :param enabled: Whether the Distribution is active or not. |
| 327 | 338 |
| 328 :type cnames: list of str | 339 :type cnames: list of str |
| 329 :param cnames: The DNS CNAME's associated with this | 340 :param cnames: The DNS CNAME's associated with this |
| 330 Distribution. Maximum of 10 values. | 341 Distribution. Maximum of 10 values. |
| 331 | 342 |
| 332 :type comment: str or unicode | 343 :type comment: str or unicode |
| 333 :param comment: The comment associated with the Distribution. | 344 :param comment: The comment associated with the Distribution. |
| 334 | 345 |
| (...skipping 29 matching lines...) Expand all Loading... |
| 364 | 375 |
| 365 def delete(self): | 376 def delete(self): |
| 366 """ | 377 """ |
| 367 Delete this CloudFront Distribution. The content | 378 Delete this CloudFront Distribution. The content |
| 368 associated with the Distribution is not deleted from | 379 associated with the Distribution is not deleted from |
| 369 the underlying Origin bucket in S3. | 380 the underlying Origin bucket in S3. |
| 370 """ | 381 """ |
| 371 self.connection.delete_distribution(self.id, self.etag) | 382 self.connection.delete_distribution(self.id, self.etag) |
| 372 | 383 |
| 373 def _get_bucket(self): | 384 def _get_bucket(self): |
| 374 if not self._bucket: | 385 if isinstance(self.config.origin, S3Origin): |
| 375 bucket_name = self.config.origin.replace('.s3.amazonaws.com', '') | 386 if not self._bucket: |
| 376 from boto.s3.connection import S3Connection | 387 bucket_dns_name = self.config.origin.dns_name |
| 377 s3 = S3Connection(self.connection.aws_access_key_id, | 388 bucket_name = bucket_dns_name.replace('.s3.amazonaws.com', '') |
| 378 self.connection.aws_secret_access_key, | 389 from boto.s3.connection import S3Connection |
| 379 proxy=self.connection.proxy, | 390 s3 = S3Connection(self.connection.aws_access_key_id, |
| 380 proxy_port=self.connection.proxy_port, | 391 self.connection.aws_secret_access_key, |
| 381 proxy_user=self.connection.proxy_user, | 392 proxy=self.connection.proxy, |
| 382 proxy_pass=self.connection.proxy_pass) | 393 proxy_port=self.connection.proxy_port, |
| 383 self._bucket = s3.get_bucket(bucket_name) | 394 proxy_user=self.connection.proxy_user, |
| 384 self._bucket.distribution = self | 395 proxy_pass=self.connection.proxy_pass) |
| 385 self._bucket.set_key_class(self._object_class) | 396 self._bucket = s3.get_bucket(bucket_name) |
| 386 return self._bucket | 397 self._bucket.distribution = self |
| 398 self._bucket.set_key_class(self._object_class) |
| 399 return self._bucket |
| 400 else: |
| 401 raise NotImplementedError('Unable to get_objects on CustomOrigin') |
| 387 | 402 |
| 388 def get_objects(self): | 403 def get_objects(self): |
| 389 """ | 404 """ |
| 390 Return a list of all content objects in this distribution. | 405 Return a list of all content objects in this distribution. |
| 391 | 406 |
| 392 :rtype: list of :class:`boto.cloudfront.object.Object` | 407 :rtype: list of :class:`boto.cloudfront.object.Object` |
| 393 :return: The content objects | 408 :return: The content objects |
| 394 """ | 409 """ |
| 395 bucket = self._get_bucket() | 410 bucket = self._get_bucket() |
| 396 objs = [] | 411 objs = [] |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 462 for the new object. | 477 for the new object. |
| 463 | 478 |
| 464 :type headers: dict | 479 :type headers: dict |
| 465 :param headers: A dictionary containing additional headers | 480 :param headers: A dictionary containing additional headers |
| 466 you would like associated with the new | 481 you would like associated with the new |
| 467 object in S3. | 482 object in S3. |
| 468 | 483 |
| 469 :rtype: :class:`boto.cloudfront.object.Object` | 484 :rtype: :class:`boto.cloudfront.object.Object` |
| 470 :return: The newly created object. | 485 :return: The newly created object. |
| 471 """ | 486 """ |
| 472 if self.config.origin_access_identity: | 487 if self.config.origin.origin_access_identity: |
| 473 policy = 'private' | 488 policy = 'private' |
| 474 else: | 489 else: |
| 475 policy = 'public-read' | 490 policy = 'public-read' |
| 476 bucket = self._get_bucket() | 491 bucket = self._get_bucket() |
| 477 object = bucket.new_key(name) | 492 object = bucket.new_key(name) |
| 478 object.set_contents_from_file(content, headers=headers, policy=policy) | 493 object.set_contents_from_file(content, headers=headers, policy=policy) |
| 479 if self.config.origin_access_identity: | 494 if self.config.origin.origin_access_identity: |
| 480 self.set_permissions(object, replace) | 495 self.set_permissions(object, replace) |
| 481 return object | 496 return object |
| 482 | 497 |
| 498 def create_signed_url(self, url, keypair_id, |
| 499 expire_time=None, valid_after_time=None, |
| 500 ip_address=None, policy_url=None, |
| 501 private_key_file=None, private_key_string=None): |
| 502 """ |
| 503 Creates a signed CloudFront URL that is only valid within the specified |
| 504 parameters. |
| 505 |
| 506 :type url: str |
| 507 :param url: The URL of the protected object. |
| 508 |
| 509 :type keypair_id: str |
| 510 :param keypair_id: The keypair ID of the Amazon KeyPair used to sign |
| 511 theURL. This ID MUST correspond to the private key |
| 512 specified with private_key_file or |
| 513 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 |
| 518 provided the URL will never expire. Format is a |
| 519 unix epoch. 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 |
| 524 epoch. 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 |
| 549 private_key_file or private_key_string can |
| 550 be provided. |
| 551 |
| 552 :rtype: str |
| 553 :return: The signed URL. |
| 554 """ |
| 555 # Get the required parameters |
| 556 params = self._create_signing_params( |
| 557 url=url, keypair_id=keypair_id, expire_time=expire_time, |
| 558 valid_after_time=valid_after_time, ip_address=ip_address, |
| 559 policy_url=policy_url, private_key_file=private_key_file, |
| 560 private_key_string=private_key_string) |
| 561 |
| 562 #combine these into a full url |
| 563 if "?" in url: |
| 564 sep = "&" |
| 565 else: |
| 566 sep = "?" |
| 567 signed_url_params = [] |
| 568 for key in ["Expires", "Policy", "Signature", "Key-Pair-Id"]: |
| 569 if key in params: |
| 570 param = "%s=%s" % (key, params[key]) |
| 571 signed_url_params.append(param) |
| 572 signed_url = url + sep + "&".join(signed_url_params) |
| 573 return signed_url |
| 574 |
| 575 def _create_signing_params(self, url, keypair_id, |
| 576 expire_time=None, valid_after_time=None, |
| 577 ip_address=None, policy_url=None, |
| 578 private_key_file=None, private_key_string=None): |
| 579 """ |
| 580 Creates the required URL parameters for a signed URL. |
| 581 """ |
| 582 params = {} |
| 583 # Check if we can use a canned policy |
| 584 if expire_time and not valid_after_time and not ip_address and not polic
y_url: |
| 585 # we manually construct this policy string to ensure formatting |
| 586 # matches signature |
| 587 policy = self._canned_policy(url, expire_time) |
| 588 params["Expires"] = str(expire_time) |
| 589 else: |
| 590 # If no policy_url is specified, default to the full url. |
| 591 if policy_url is None: |
| 592 policy_url = url |
| 593 # Can't use canned policy |
| 594 policy = self._custom_policy(policy_url, expires=None, |
| 595 valid_after=None, |
| 596 ip_address=None) |
| 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 if expires: |
| 624 condition["DateLessThan"] = {"AWS:EpochTime": expires} |
| 625 if valid_after: |
| 626 condition["DateGreaterThan"] = {"AWS:EpochTime": valid_after} |
| 627 if ip_address: |
| 628 if '/' not in ip_address: |
| 629 ip_address += "/32" |
| 630 condition["IpAddress"] = {"AWS:SourceIp": ip_address} |
| 631 policy = {"Statement": [{ |
| 632 "Resource": resource, |
| 633 "Condition": condition}]} |
| 634 return json.dumps(policy, separators=(",", ":")) |
| 635 |
| 636 @staticmethod |
| 637 def _sign_string(message, private_key_file=None, private_key_string=None): |
| 638 """ |
| 639 Signs a string for use with Amazon CloudFront. Requires the M2Crypto |
| 640 library be installed. |
| 641 """ |
| 642 try: |
| 643 from M2Crypto import EVP |
| 644 except ImportError: |
| 645 raise NotImplementedError("Boto depends on the python M2Crypto " |
| 646 "library to generate signed URLs for " |
| 647 "CloudFront") |
| 648 # Make sure only one of private_key_file and private_key_string is set |
| 649 if private_key_file and private_key_string: |
| 650 raise ValueError("Only specify the private_key_file or the private_k
ey_string not both") |
| 651 if not private_key_file and not private_key_string: |
| 652 raise ValueError("You must specify one of private_key_file or privat
e_key_string") |
| 653 # if private_key_file is a file object read the key string from there |
| 654 if isinstance(private_key_file, file): |
| 655 private_key_string = private_key_file.read() |
| 656 # Now load key and calculate signature |
| 657 if private_key_string: |
| 658 key = EVP.load_key_string(private_key_string) |
| 659 else: |
| 660 key = EVP.load_key(private_key_file) |
| 661 key.reset_context(md='sha1') |
| 662 key.sign_init() |
| 663 key.sign_update(str(message)) |
| 664 signature = key.sign_final() |
| 665 return signature |
| 666 |
| 667 @staticmethod |
| 668 def _url_base64_encode(msg): |
| 669 """ |
| 670 Base64 encodes a string using the URL-safe characters specified by |
| 671 Amazon. |
| 672 """ |
| 673 msg_base64 = base64.b64encode(msg) |
| 674 msg_base64 = msg_base64.replace('+', '-') |
| 675 msg_base64 = msg_base64.replace('=', '_') |
| 676 msg_base64 = msg_base64.replace('/', '~') |
| 677 return msg_base64 |
| 678 |
| 483 class StreamingDistribution(Distribution): | 679 class StreamingDistribution(Distribution): |
| 484 | 680 |
| 485 def __init__(self, connection=None, config=None, domain_name='', | 681 def __init__(self, connection=None, config=None, domain_name='', |
| 486 id='', last_modified_time=None, status=''): | 682 id='', last_modified_time=None, status=''): |
| 487 Distribution.__init__(self, connection, config, domain_name, | 683 Distribution.__init__(self, connection, config, domain_name, |
| 488 id, last_modified_time, status) | 684 id, last_modified_time, status) |
| 489 self._object_class = StreamingObject | 685 self._object_class = StreamingObject |
| 490 | 686 |
| 491 def startElement(self, name, attrs, connection): | 687 def startElement(self, name, attrs, connection): |
| 492 if name == 'StreamingDistributionConfig': | 688 if name == 'StreamingDistributionConfig': |
| 493 self.config = StreamingDistributionConfig() | 689 self.config = StreamingDistributionConfig() |
| 494 return self.config | 690 return self.config |
| 495 else: | 691 else: |
| 496 return Distribution.startElement(self, name, attrs, connection) | 692 return Distribution.startElement(self, name, attrs, connection) |
| 497 | 693 |
| 498 def update(self, enabled=None, cnames=None, comment=None): | 694 def update(self, enabled=None, cnames=None, comment=None): |
| 499 """ | 695 """ |
| 500 Update the configuration of the StreamingDistribution. The only values | 696 Update the configuration of the StreamingDistribution. The only values |
| 501 of the StreamingDistributionConfig that can be updated are: | 697 of the StreamingDistributionConfig that can be directly updated are: |
| 502 | 698 |
| 503 * CNAMES | 699 * CNAMES |
| 504 * Comment | 700 * Comment |
| 505 * Whether the Distribution is enabled or not | 701 * Whether the Distribution is enabled or not |
| 506 | 702 |
| 703 Any changes to the ``trusted_signers`` or ``origin`` properties of |
| 704 this distribution's current config object will also be included in |
| 705 the update. Therefore, to set the origin access identity for this |
| 706 distribution, set |
| 707 ``StreamingDistribution.config.origin.origin_access_identity`` |
| 708 before calling this update method. |
| 709 |
| 507 :type enabled: bool | 710 :type enabled: bool |
| 508 :param enabled: Whether the StreamingDistribution is active or not. | 711 :param enabled: Whether the StreamingDistribution is active or not. |
| 509 | 712 |
| 510 :type cnames: list of str | 713 :type cnames: list of str |
| 511 :param cnames: The DNS CNAME's associated with this | 714 :param cnames: The DNS CNAME's associated with this |
| 512 Distribution. Maximum of 10 values. | 715 Distribution. Maximum of 10 values. |
| 513 | 716 |
| 514 :type comment: str or unicode | 717 :type comment: str or unicode |
| 515 :param comment: The comment associated with the Distribution. | 718 :param comment: The comment associated with the Distribution. |
| 516 | 719 |
| (...skipping 14 matching lines...) Expand all Loading... |
| 531 self.etag = self.connection.set_streaming_distribution_config(self.id, | 734 self.etag = self.connection.set_streaming_distribution_config(self.id, |
| 532 self.etag, | 735 self.etag, |
| 533 new_config
) | 736 new_config
) |
| 534 self.config = new_config | 737 self.config = new_config |
| 535 self._object_class = StreamingObject | 738 self._object_class = StreamingObject |
| 536 | 739 |
| 537 def delete(self): | 740 def delete(self): |
| 538 self.connection.delete_streaming_distribution(self.id, self.etag) | 741 self.connection.delete_streaming_distribution(self.id, self.etag) |
| 539 | 742 |
| 540 | 743 |
| OLD | NEW |