Permalink
Browse files

Completed work on CORS for S3. Fixes #954.

  • Loading branch information...
1 parent af70749 commit f64bccb165c257a7f7ae98f52b434d7ade3cbfbe @garnaat garnaat committed Sep 1, 2012
Showing with 304 additions and 87 deletions.
  1. +36 −26 boto/s3/bucket.py
  2. +113 −61 boto/s3/cors.py
  3. +78 −0 tests/integration/s3/test_cors.py
  4. +77 −0 tests/unit/s3/test_cors_configuration.py
View
@@ -92,16 +92,6 @@ class Bucket(object):
WebsiteErrorFragment = """<ErrorDocument><Key>%s</Key></ErrorDocument>"""
- CORSBody = """<?xml version="1.0" encoding="UTF-8"?>
- <CORSConfiguration
- <CORSRule>
- <AllowedOrigin>%s</AllowedOrigin>
- <AllowedMethod>%s</AllowedMethod>
- <MaxAgeSeconds>%d</MaxAgeSeconds>
- <ExposeHeader>%s</ExposeHeader>
- </CORSRule>
- </CORSConfiguration>"""
-
VersionRE = '<Status>([A-Za-z]+)</Status>'
MFADeleteRE = '<MfaDelete>([A-Za-z]+)</MfaDelete>'
@@ -1366,15 +1356,16 @@ def delete_policy(self, headers=None):
raise self.connection.provider.storage_response_error(
response.status, response.reason, body)
- def configure_cors(self, cors_config, headers=None):
+ def set_cors_xml(self, cors_xml, headers=None):
"""
- Configure CORS for this bucket.
+ Set the CORS (Cross-Origin Resource Sharing) for a bucket.
- :type cors_config: :class:`boto.s3.cors.CORSConfiguration`
- :param cors_config: The CORS configuration you want
- to configure for this bucket.
+ :type cors_xml: str
+ :param cors_xml: The XML document describing your desired
+ CORS configuration. See the S3 documentation for details
+ of the exact syntax required.
"""
- fp = StringIO.StringIO(cors_config.to_xml())
+ fp = StringIO.StringIO(cors_xml)
md5 = boto.utils.compute_md5(fp)
if headers is None:
headers = {}
@@ -1391,28 +1382,47 @@ def configure_cors(self, cors_config, headers=None):
raise self.connection.provider.storage_response_error(
response.status, response.reason, body)
- def get_cors_config(self, headers=None):
+ def set_cors(self, cors_config, headers=None):
"""
- Returns the current CORS configuration on the bucket.
+ Set the CORS for this bucket given a boto CORSConfiguration
+ object.
- :rtype: :class:`boto.s3.cors.CORSConfiguration`
- :returns: A CORSConfiguration object that describes all current
- CORS rules in effect for the bucket.
+ :type cors_config: :class:`boto.s3.cors.CORSConfiguration`
+ :param cors_config: The CORS configuration you want
+ to configure for this bucket.
+ """
+ return self.set_cors_xml(cors_config.to_xml())
+
+ def get_cors_xml(self, headers=None):
+ """
+ Returns the current CORS configuration on the bucket as an
+ XML document.
"""
response = self.connection.make_request('GET', self.name,
query_args='cors', headers=headers)
body = response.read()
boto.log.debug(body)
if response.status == 200:
- cors = CORSConfiguration()
- h = handler.XmlHandler(cors, self)
- xml.sax.parseString(body, h)
- return cors
+ return body
else:
raise self.connection.provider.storage_response_error(
response.status, response.reason, body)
- def delete_cors_configuration(self, headers=None):
+ def get_cors(self, headers=None):
+ """
+ Returns the current CORS configuration on the bucket.
+
+ :rtype: :class:`boto.s3.cors.CORSConfiguration`
+ :returns: A CORSConfiguration object that describes all current
+ CORS rules in effect for the bucket.
+ """
+ body = self.get_cors_xml(headers)
+ cors = CORSConfiguration()
+ h = handler.XmlHandler(cors, self)
+ xml.sax.parseString(body, h)
+ return cors
+
+ def delete_cors(self, headers=None):
"""
Removes all CORS configuration from the bucket.
"""
View
@@ -21,46 +21,61 @@
# IN THE SOFTWARE.
#
+
class CORSRule(object):
"""
CORS rule for a bucket.
- :ivar allowed_method: The HTTP method.
-
- :ivar allowed_origin: A single wildcarded domain name or a list
- of non-wildcarded domain names.
-
- :ivar id: Optional unique identifier for the rule. The value
- cannot be longer than 255 characters.
-
- :ivar max_age_seconds: An integer which alters the caching
- behavior for the pre-flight request.
-
- :ivar expose_header: Enables teh browser to read this header.
-
- :ivar allowed_header: Unsed in response to a preflight request
- to indicate which HTTP headers can be used when making the
- actual request.
- """
-
- ValidMethods = ('GET', 'PUT', 'HEAD', 'POST', 'DELETE')
+ :ivar id: A unique identifier for the rule. The ID value can be
+ up to 255 characters long. The IDs help you find a rule in
+ the configuration.
+
+ :ivar allowed_methods: An HTTP method that you want to allow the
+ origin to execute. Each CORSRule must identify at least one
+ origin and one method. Valid values are:
+ GET|PUT|HEAD|POST|DELETE
+
+ :ivar allowed_origin: An origin that you want to allow cross-domain
+ requests from. This can contain at most one * wild character.
+ Each CORSRule must identify at least one origin and one method.
+ The origin value can include at most one '*' wild character.
+ For example, "http://*.example.com". You can also specify
+ only * as the origin value allowing all origins cross-domain access.
+
+ :ivar allowed_header: Specifies which headers are allowed in a
+ pre-flight OPTIONS request via the
+ Access-Control-Request-Headers header. Each header name
+ specified in the Access-Control-Request-Headers header must
+ have a corresponding entry in the rule. Amazon S3 will send
+ only the allowed headers in a response that were requested.
+ This can contain at most one * wild character.
+
+ :ivar max_age_seconds: The time in seconds that your browser is to
+ cache the preflight response for the specified resource.
+
+ :ivar expose_header: One or more headers in the response that you
+ want customers to be able to access from their applications
+ (for example, from a JavaScript XMLHttpRequest object). You
+ add one ExposeHeader element in the rule for each header.
+ """
- def __init__(self, allowed_method, allowed_origin,
- id=None, max_age_seconds=None, expose_header=None,
- allowed_header=None):
- if allowed_method not in self.ValidMethods:
- msg = 'allowed_method must be one of: %s' % self.ValidMethods
- raise ValueError(msg)
- if not isinstance(allowed_origin, (list, tuple)):
- if allowed_origin is None:
- allowed_origin = []
- else:
- allowed_origin = [allowed_origin]
+ def __init__(self, allowed_method=None, allowed_origin=None,
+ id=None, allowed_header=None, max_age_seconds=None,
+ expose_header=None):
+ if allowed_method is None:
+ allowed_method = []
+ self.allowed_method = allowed_method
+ if allowed_origin is None:
+ allowed_origin = []
self.allowed_origin = allowed_origin
self.id = id
+ if allowed_header is None:
+ allowed_header = []
+ self.allowed_header = allowed_header
self.max_age_seconds = max_age_seconds
+ if expose_header is None:
+ expose_header = []
self.expose_header = expose_header
- self.allowed_header = allowed_header
def __repr__(self):
return '<Rule: %s>' % self.id
@@ -72,29 +87,33 @@ def endElement(self, name, value, connection):
if name == 'ID':
self.id = value
elif name == 'AllowedMethod':
- self.allowed_method = value
+ self.allowed_method.append(value)
elif name == 'AllowedOrigin':
self.allowed_origin.append(value)
elif name == 'AllowedHeader':
- self.allowed_header = value
+ self.allowed_header.append(value)
elif name == 'MaxAgeSeconds':
self.max_age_seconds = int(value)
elif name == 'ExposeHeader':
- self.expose_header = value
+ self.expose_header.append(value)
else:
setattr(self, name, value)
def to_xml(self):
s = '<CORSRule>'
+ for allowed_method in self.allowed_method:
+ s += '<AllowedMethod>%s</AllowedMethod>' % allowed_method
for allowed_origin in self.allowed_origin:
s += '<AllowedOrigin>%s</AllowedOrigin>' % allowed_origin
- s += '<AllowedMethod>%s</AllowedMethod>' % self.allowed_method
+ for allowed_header in self.allowed_header:
+ s += '<AllowedHeader>%s</AllowedHeader>' % allowed_header
+ for expose_header in self.expose_header:
+ s += '<ExposeHeader>%s</ExposeHeader>' % expose_header
if self.max_age_seconds:
s += '<MaxAgeSeconds>%d</MaxAgeSeconds>' % self.max_age_seconds
- if self.expose_header:
- s += '<ExposeHeader>%s</ExposeHeader>' % self.expose_header
- if self.allow_header:
- s += '<AllowHeader>%s</AllowHeader>' % self.allow_header
+ if self.id:
+ s += '<ID>%s</ID>' % self.id
+ s += '</CORSRule>'
return s
@@ -124,35 +143,68 @@ def to_xml(self):
s += '</CORSConfiguration>'
return s
- def add_rule(self, id, prefix, status, expiration):
+ def add_rule(self, allowed_method, allowed_origin,
+ id=None, allowed_header=None, max_age_seconds=None,
+ expose_header=None):
"""
Add a rule to this CORS configuration. This only adds
the rule to the local copy. To install the new rule(s) on
the bucket, you need to pass this CORS config object
- to the configure_cors method of the Bucket object.
-
- :type allowed_method: str
- :param allowed_method: The HTTP method.
-
- :type allowed_origin: str or list of str
- :param allowed_origin: A single wildcarded domain name or a list
- of non-wildcarded domain names.
+ to the set_cors method of the Bucket object.
+
+ :type allowed_methods: list of str
+ :param allowed_methods: An HTTP method that you want to allow the
+ origin to execute. Each CORSRule must identify at least one
+ origin and one method. Valid values are:
+ GET|PUT|HEAD|POST|DELETE
+
+ :type allowed_origin: list of str
+ :param allowed_origin: An origin that you want to allow cross-domain
+ requests from. This can contain at most one * wild character.
+ Each CORSRule must identify at least one origin and one method.
+ The origin value can include at most one '*' wild character.
+ For example, "http://*.example.com". You can also specify
+ only * as the origin value allowing all origins
+ cross-domain access.
:type id: str
- :iparam id: Optional unique identifier for the rule. The value
- cannot be longer than 255 characters.
+ :param id: A unique identifier for the rule. The ID value can be
+ up to 255 characters long. The IDs help you find a rule in
+ the configuration.
+
+ :type allowed_header: list of str
+ :param allowed_header: Specifies which headers are allowed in a
+ pre-flight OPTIONS request via the
+ Access-Control-Request-Headers header. Each header name
+ specified in the Access-Control-Request-Headers header must
+ have a corresponding entry in the rule. Amazon S3 will send
+ only the allowed headers in a response that were requested.
+ This can contain at most one * wild character.
:type max_age_seconds: int
- :param max_age_seconds: An integer which alters the caching
- behavior for the pre-flight request.
-
- :type expose_header: str
- :param expose_header: Enables the browser to read this header.
-
- :type allowed_header: str
- :param allowed_header: Unsed in response to a preflight request
- to indicate which HTTP headers can be used when making the
- actual request.
+ :param max_age_seconds: The time in seconds that your browser is to
+ cache the preflight response for the specified resource.
+
+ :type expose_header: list of str
+ :param expose_header: One or more headers in the response that you
+ want customers to be able to access from their applications
+ (for example, from a JavaScript XMLHttpRequest object). You
+ add one ExposeHeader element in the rule for each header.
"""
- rule = CORSRule(id, prefix, status, expiration)
+ if not isinstance(allowed_method, (list, tuple)):
+ allowed_method = [allowed_method]
+ if not isinstance(allowed_origin, (list, tuple)):
+ allowed_origin = [allowed_origin]
+ if not isinstance(allowed_origin, (list, tuple)):
+ if allowed_origin is None:
+ allowed_origin = []
+ else:
+ allowed_origin = [allowed_origin]
+ if not isinstance(expose_header, (list, tuple)):
+ if expose_header is None:
+ expose_header = []
+ else:
+ expose_header = [expose_header]
+ rule = CORSRule(allowed_method, allowed_origin, id, allowed_header,
+ max_age_seconds, expose_header)
self.append(rule)
Oops, something went wrong.

0 comments on commit f64bccb

Please sign in to comment.