Skip to content

Commit

Permalink
Add handler for CORS "actual requests"
Browse files Browse the repository at this point in the history
Fix for bug 1095130

* Added a wrapper function around public methods to handle
  CORS actual requests. These requests need to return some
  extra headers to be valid responses to a CORS request.
  Access-Control-Expose-Headers and Access-Control-Allow-Origin.

* Added support for the CORS header Access-Control-Expose-Headers.

* Some refactoring of the OPTIONS method so the
  "is_origin_allowed" logic can be reused.

* Added a little extra detail to the CORS documentation.

DocImpact

Change-Id: I68538e472a900775427f21a8a59e738a83dcc8bc
  • Loading branch information
Adrian Smith committed Jan 23, 2013
1 parent 6c5fc3c commit 89ee10b
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 64 deletions.
151 changes: 151 additions & 0 deletions doc/source/cors.rst
@@ -0,0 +1,151 @@
====
CORS
====

CORS_ is a mechanisim to allow code running in a browser (Javascript for
example) make requests to a domain other then the one from where it originated.

Swift supports CORS requests to containers and objects.

CORS metadata is held on the container only. The values given apply to the
container itself and all objects within it.

The supported headers are,

+---------------------------------------------+-------------------------------+
|Metadata | Use |
+==============================================+==============================+
|X-Container-Meta-Access-Control-Allow-Origin | Origins to be allowed to |
| | make Cross Origin Requests, |
| | space separated. |
+----------------------------------------------+------------------------------+
|X-Container-Meta-Access-Control-Max-Age | Max age for the Origin to |
| | hold the preflight results. |
+----------------------------------------------+------------------------------+
|X-Container-Meta-Access-Control-Allow-Headers | Headers to be allowed in |
| | actual request by browser, |
| | space seperated. |
+----------------------------------------------+------------------------------+
|X-Container-Meta-Access-Control-Expose-Headers| Headers exposed to the user |
| | agent (e.g. browser) in the |
| | the actual request response. |
| | Space seperated. |
+----------------------------------------------+------------------------------+

Before a browser issues an actual request it may issue a `preflight request`_.
The preflight request is an OPTIONS call to verify the Origin is allowed to
make the request. The sequence of events are,

* Browser makes OPTIONS request to Swift
* Swift returns 200/401 to browser based on allowed origins
* If 200, browser makes the "actual request" to Swift, i.e. PUT, POST, DELETE,
HEAD, GET

When a browser receives a response to an actual request it only exposes those
headers listed in the ``Access-Control-Expose-Headers`` header. By default Swift
returns the following values for this header,

* "simple response headers" as listed on
http://www.w3.org/TR/cors/#simple-response-header
* the headers ``etag``, ``x-timestamp``, ``x-trans-id``
* all metadata headers (``X-Container-Meta-*`` for containers and
``X-Object-Meta-*`` for objects)
* headers listed in ``X-Container-Meta-Access-Control-Expose-Headers``


-----------------
Sample Javascript
-----------------

To see some CORS Javascript in action download the `test CORS page`_ (source
below). Host it on a webserver and take note of the protocol and hostname
(origin) you'll be using to request the page, e.g. http://localhost.

Locate a container you'd like to query. Needless to say the Swift cluster
hosting this container should have CORS support. Append the origin of the
test page to the container's ``X-Container-Meta-Access-Control-Allow-Origin``
header,::

curl -X POST -H 'X-Auth-Token: xxx' \
-H 'X-Container-Meta-Access-Control-Allow-Origin: http://localhost' \
http://192.168.56.3:8080/v1/AUTH_test/cont1

At this point the container is now accessable to CORS clients hosted on
http://localhost. Open the test CORS page in your browser.

#. Populate the Token field
#. Populate the URL field with the URL of either a container or object
#. Select the request method
#. Hit Submit

Assuming the request succeeds you should see the response header and body. If
something went wrong the response status will be 0.

.. _test CORS page:

Test CORS Page
--------------

::

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test CORS</title>
</head>
<body>

Token<br><input id="token" type="text" size="64"><br><br>

Method<br>
<select id="method">
<option value="GET">GET</option>
<option value="HEAD">HEAD</option>
<option value="POST">POST</option>
<option value="DELETE">DELETE</option>
<option value="PUT">PUT</option>
</select><br><br>

URL (Container or Object)<br><input id="url" size="64" type="text"><br><br>

<input id="submit" type="button" value="Submit" onclick="submit(); return false;">

<pre id="response_headers"></pre>
<p>
<hr>
<pre id="response_body"></pre>

<script type="text/javascript">
function submit() {
var token = document.getElementById('token').value;
var method = document.getElementById('method').value;
var url = document.getElementById('url').value;

document.getElementById('response_headers').textContent = null;
document.getElementById('response_body').textContent = null;

var request = new XMLHttpRequest();

request.onreadystatechange = function (oEvent) {
if (request.readyState == 4) {
responseHeaders = 'Status: ' + request.status;
responseHeaders = responseHeaders + '\nStatus Text: ' + request.statusText;
responseHeaders = responseHeaders + '\n\n' + request.getAllResponseHeaders();
document.getElementById('response_headers').textContent = responseHeaders;
document.getElementById('response_body').textContent = request.responseText;
}
}

request.open(method, url);
request.setRequestHeader('X-Auth-Token', token);
request.send(null);
}
</script>

</body>
</html>

.. _CORS: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
.. _preflight request: https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Preflighted_requests

1 change: 1 addition & 0 deletions doc/source/index.rst
Expand Up @@ -52,6 +52,7 @@ Overview and Concepts
overview_object_versioning
overview_container_sync
overview_expiring_objects
cors
associated_projects

Developer Documentation
Expand Down
31 changes: 0 additions & 31 deletions doc/source/misc.rst
Expand Up @@ -172,34 +172,3 @@ Proxy Logging
:members:
:show-inheritance:

CORS Headers
============

Cross Origin RequestS or CORS allows the browser to make requests against
Swift from another origin via the browser. This enables the use of HTML5
forms and javascript uploads to swift. The owner of a container can set
three headers:

+---------------------------------------------+-------------------------------+
|Metadata | Use |
+=============================================+===============================+
|X-Container-Meta-Access-Control-Allow-Origin | Origins to be allowed to |
| | make Cross Origin Requests, |
| | space separated |
+---------------------------------------------+-------------------------------+
|X-Container-Meta-Access-Control-Max-Age | Max age for the Origin to |
| | hold the preflight results. |
+---------------------------------------------+-------------------------------+
|X-Container-Meta-Access-Control-Allow-Headers| Headers to be allowed in |
| | actual request by browser. |
+---------------------------------------------+-------------------------------+

When the browser does a request it can issue a preflight request. The
preflight request is the OPTIONS call that verifies the Origin is allowed
to make the request.

* Browser makes OPTIONS request to Swift
* Swift returns 200/401 to browser based on allowed origins
* If 200, browser makes PUT, POST, DELETE, HEAD, GET request to Swift

CORS should be used in conjunction with TempURL and FormPost.
137 changes: 115 additions & 22 deletions swift/proxy/controllers/base.py
Expand Up @@ -113,6 +113,8 @@ def headers_to_container_info(headers, status_int=HTTP_OK):
'x-container-meta-access-control-allow-origin'),
'allow_headers': headers.get(
'x-container-meta-access-control-allow-headers'),
'expose_headers': headers.get(
'x-container-meta-access-control-expose-headers'),
'max_age': headers.get(
'x-container-meta-access-control-max-age')
},
Expand All @@ -122,6 +124,70 @@ def headers_to_container_info(headers, status_int=HTTP_OK):
}


def cors_validation(func):
"""
Decorator to check if the request is a CORS request and if so, if it's
valid.
:param func: function to check
"""
@functools.wraps(func)
def wrapped(*a, **kw):
controller = a[0]
req = a[1]

# The logic here was interpreted from
# http://www.w3.org/TR/cors/#resource-requests

# Is this a CORS request?
req_origin = req.headers.get('Origin', None)
if req_origin:
# Yes, this is a CORS request so test if the origin is allowed
container_info = \
controller.container_info(controller.account_name,
controller.container_name)
cors_info = container_info.get('cors', {})
if not controller.is_origin_allowed(cors_info, req_origin):
# invalid CORS request
return Response(status=HTTP_UNAUTHORIZED)

# Call through to the decorated method
resp = func(*a, **kw)

# Expose,
# - simple response headers,
# http://www.w3.org/TR/cors/#simple-response-header
# - swift specific: etag, x-timestamp, x-trans-id
# - user metadata headers
# - headers provided by the user in
# x-container-meta-access-control-expose-headers
expose_headers = ['cache-control', 'content-language',
'content-type', 'expires', 'last-modified',
'pragma', 'etag', 'x-timestamp', 'x-trans-id']
for header in resp.headers:
if header.startswith('x-container-meta') or \
header.startswith('x-object-meta'):
expose_headers.append(header.lower())
if cors_info.get('expose_headers'):
expose_headers.extend(
[a.strip()
for a in cors_info['expose_headers'].split(' ')
if a.strip()])
resp.headers['Access-Control-Expose-Headers'] = \
', '.join(expose_headers)

# The user agent won't process the response if the Allow-Origin
# header isn't included
resp.headers['Access-Control-Allow-Origin'] = req_origin

return resp
else:
# Not a CORS request so make the call as normal
return func(*a, **kw)

return wrapped


class Controller(object):
"""Base WSGI controller class for the proxy"""
server_type = 'Base'
Expand Down Expand Up @@ -694,49 +760,76 @@ def GETorHEAD_base(self, req, server_type, partition, nodes, path,
return self.best_response(req, statuses, reasons, bodies,
'%s %s' % (server_type, req.method))

def OPTIONS_base(self, req):
def is_origin_allowed(self, cors_info, origin):
"""
Is the given Origin allowed to make requests to this resource
:param cors_info: the resource's CORS related metadata headers
:param origin: the origin making the request
:return: True or False
"""
allowed_origins = set()
if cors_info.get('allow_origin'):
allowed_origins.update(
[a.strip()
for a in cors_info['allow_origin'].split(' ')
if a.strip()])
if self.app.cors_allow_origin:
allowed_origins.update(self.app.cors_allow_origin)
return origin in allowed_origins or '*' in allowed_origins

@public
def OPTIONS(self, req):
"""
Base handler for OPTIONS requests
:param req: swob.Request object
:returns: swob.Response object
"""
# Prepare the default response
headers = {'Allow': ', '.join(self.allowed_methods)}
resp = Response(status=200, request=req,
headers=headers)
resp = Response(status=200, request=req, headers=headers)

# If this isn't a CORS pre-flight request then return now
req_origin_value = req.headers.get('Origin', None)
if not req_origin_value:
# NOT a CORS request
return resp

# CORS preflight request
# This is a CORS preflight request so check it's allowed
try:
container_info = \
self.container_info(self.account_name, self.container_name)
except AttributeError:
container_info = {}
# This should only happen for requests to the Account. A future
# change could allow CORS requests to the Account level as well.
return resp

cors = container_info.get('cors', {})
allowed_origins = set()
if cors.get('allow_origin'):
allowed_origins.update(cors['allow_origin'].split(' '))
if self.app.cors_allow_origin:
allowed_origins.update(self.app.cors_allow_origin)
if (req_origin_value not in allowed_origins and
'*' not in allowed_origins) or (

# If the CORS origin isn't allowed return a 401
if not self.is_origin_allowed(cors, req_origin_value) or (
req.headers.get('Access-Control-Request-Method') not in
self.allowed_methods):
resp.status = HTTP_UNAUTHORIZED
return resp # CORS preflight request that isn't valid
return resp

# Always allow the x-auth-token header. This ensures
# clients can always make a request to the resource.
allow_headers = set()
if cors.get('allow_headers'):
allow_headers.update(
[a.strip()
for a in cors['allow_headers'].split(' ')
if a.strip()])
allow_headers.add('x-auth-token')

# Populate the response with the CORS preflight headers
headers['access-control-allow-origin'] = req_origin_value
if cors.get('max_age') is not None:
headers['access-control-max-age'] = cors.get('max_age')
headers['access-control-allow-methods'] = ', '.join(
self.allowed_methods)
if cors.get('allow_headers'):
headers['access-control-allow-headers'] = cors.get('allow_headers')
headers['access-control-allow-methods'] = \
', '.join(self.allowed_methods)
headers['access-control-allow-headers'] = ', '.join(allow_headers)
resp.headers = headers
return resp

@public
def OPTIONS(self, req):
return self.OPTIONS_base(req)
return resp

0 comments on commit 89ee10b

Please sign in to comment.