Skip to content

Commit

Permalink
Adding CORS support
Browse files Browse the repository at this point in the history
Change-Id: I894473994cdfea0996ad16e7619aff421f604abc
  • Loading branch information
sasimpson committed Oct 23, 2012
1 parent 8cacf5a commit 74b27d5
Show file tree
Hide file tree
Showing 10 changed files with 341 additions and 68 deletions.
7 changes: 7 additions & 0 deletions doc/source/deployment_guide.rst
Expand Up @@ -529,6 +529,13 @@ cert_file Path to the ssl .crt. This
key_file Path to the ssl .key. This
should be enabled for testing
purposes only.
cors_allow_origin This is a list of hosts that
are included with any CORS
request by default and
returned with the
Access-Control-Allow-Origin
header in addition to what
the container has set.
============================ =============== =============================

[proxy-server]
Expand Down
9 changes: 9 additions & 0 deletions doc/source/development_auth.rst
Expand Up @@ -482,3 +482,12 @@ folks a start on their own code if they want to use repoze.what::
authenticators=[('devauth', DevAuthenticator(conf))],
challengers=[('devauth', DevChallenger(conf))])
return auth_filter

-----------------------
Allowing CORS with Auth
-----------------------

Cross Origin RequestS require that the auth system allow the OPTIONS method to
pass through without a token. The preflight request will make an OPTIONS call
against the object or container and will not work if the auth system stops it.
See TempAuth for an example of how OPTIONS requests are handled.
32 changes: 32 additions & 0 deletions doc/source/misc.rst
Expand Up @@ -171,3 +171,35 @@ Proxy Logging
.. automodule:: swift.common.middleware.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.
2 changes: 2 additions & 0 deletions doc/source/overview_auth.rst
Expand Up @@ -39,6 +39,8 @@ Additionally, if the auth system sets the request environ's swift_owner key to
True, the proxy will return additional header information in some requests,
such as the X-Container-Sync-Key for a container GET or HEAD.

TempAuth will now allow OPTIONS requests to go through without a token.

The user starts a session by sending a ReST request to the auth system to
receive the auth token and a URL to the Swift system.

Expand Down
2 changes: 2 additions & 0 deletions etc/proxy-server.conf-sample
Expand Up @@ -27,6 +27,8 @@
# log_statsd_port = 8125
# log_statsd_default_sample_rate = 1
# log_statsd_metric_prefix =
# Use a comma separated list of full url (http://foo.bar:1234,https://foo.bar)
# cors_allow_origin =

[pipeline:main]
pipeline = catch_errors healthcheck cache ratelimit tempauth proxy-logging proxy-server
Expand Down
25 changes: 18 additions & 7 deletions swift/common/middleware/tempauth.py
Expand Up @@ -84,7 +84,8 @@ def __init__(self, app, conf):
if self.auth_prefix[-1] != '/':
self.auth_prefix += '/'
self.token_life = int(conf.get('token_life', 86400))
self.allowed_sync_hosts = [h.strip()
self.allowed_sync_hosts = [
h.strip()
for h in conf.get('allowed_sync_hosts', '127.0.0.1').split(',')
if h.strip()]
self.allow_overrides = \
Expand Down Expand Up @@ -244,6 +245,7 @@ def authorize(self, req):
Returns None if the request is authorized to continue or a standard
WSGI response callable if not.
"""

try:
version, account, container, obj = split_path(req.path, 1, 4, True)
except ValueError:
Expand All @@ -270,6 +272,9 @@ def authorize(self, req):
(req.remote_addr in self.allowed_sync_hosts or
get_remote_client(req) in self.allowed_sync_hosts)):
return None
if req.method == 'OPTIONS':
#allow OPTIONS requests to proceed as normal
return None
referrers, groups = parse_acl(getattr(req, 'acl', None))
if referrer_allowed(req.referer, referrers):
if obj or '.rlistings' in groups:
Expand Down Expand Up @@ -341,8 +346,11 @@ def handle_request(self, req):
req.start_time = time()
handler = None
try:
version, account, user, _junk = split_path(req.path_info,
minsegs=1, maxsegs=4, rest_with_last=True)
version, account, user, _junk = split_path(
req.path_info,
minsegs=1,
maxsegs=4,
rest_with_last=True)
except ValueError:
self.logger.increment('errors')
return HTTPNotFound(request=req)
Expand Down Expand Up @@ -464,8 +472,10 @@ def handle_get_token(self, req):
memcache_client.set(memcache_user_key, token,
timeout=float(expires - time()))
return Response(request=req,
headers={'x-auth-token': token, 'x-storage-token': token,
'x-storage-url': self.users[account_user]['url']})
headers={
'x-auth-token': token,
'x-storage-token': token,
'x-storage-url': self.users[account_user]['url']})

def posthooklogger(self, env, req):
if not req.path.startswith(self.auth_prefix):
Expand All @@ -490,12 +500,13 @@ def posthooklogger(self, env, req):
if getattr(req, 'client_disconnect', False) or \
getattr(response, 'client_disconnect', False):
status_int = HTTP_CLIENT_CLOSED_REQUEST
self.logger.info(' '.join(quote(str(x)) for x in (client or '-',
self.logger.info(
' '.join(quote(str(x)) for x in (client or '-',
req.remote_addr or '-', strftime('%d/%b/%Y/%H/%M/%S', gmtime()),
req.method, the_request, req.environ['SERVER_PROTOCOL'],
status_int, req.referer or '-', req.user_agent or '-',
req.headers.get('x-auth-token',
req.headers.get('x-auth-admin-user', '-')),
req.headers.get('x-auth-admin-user', '-')),
getattr(req, 'bytes_transferred', 0) or '-',
getattr(response, 'bytes_transferred', 0) or '-',
req.headers.get('etag', '-'),
Expand Down
64 changes: 56 additions & 8 deletions swift/proxy/controllers/base.py
Expand Up @@ -278,6 +278,25 @@ def account_info(self, account, autocreate=False):
return partition, nodes, container_count
return None, None, None

def headers_to_container_info(self, headers):
headers = dict(headers)
return {
'read_acl': headers.get('x-container-read'),
'write_acl': headers.get('x-container-write'),
'sync_key': headers.get('x-container-sync-key'),
'count': headers.get('x-container-object-count'),
'bytes': headers.get('x-container-bytes-used'),
'versions': headers.get('x-versions-location'),
'cors': {
'allow_origin': headers.get(
'x-container-meta-access-control-allow-origin'),
'allow_headers': headers.get(
'x-container-meta-access-control-allow-headers'),
'max_age': headers.get(
'x-container-meta-access-control-max-age')
}
}

def container_info(self, account, container, account_autocreate=False):
"""
Get container information and thusly verify container existance.
Expand Down Expand Up @@ -324,14 +343,9 @@ def container_info(self, account, container, account_autocreate=False):
resp = conn.getresponse()
body = resp.read()
if is_success(resp.status):
container_info.update({
'status': HTTP_OK,
'read_acl': resp.getheader('x-container-read'),
'write_acl': resp.getheader('x-container-write'),
'sync_key': resp.getheader('x-container-sync-key'),
'count': resp.getheader('x-container-object-count'),
'bytes': resp.getheader('x-container-bytes-used'),
'versions': resp.getheader('x-versions-location')})
container_info.update(
self.headers_to_container_info(resp.getheaders()))
container_info['status'] = HTTP_OK
break
elif resp.status == HTTP_NOT_FOUND:
container_info['status'] = HTTP_NOT_FOUND
Expand Down Expand Up @@ -661,3 +675,37 @@ def GETorHEAD_base(self, req, server_type, partition, nodes, path,
return res
return self.best_response(req, statuses, reasons, bodies,
'%s %s' % (server_type, req.method))

def OPTIONS_base(self, req):
"""
Base handler for OPTIONS requests
:param req: swob.Request object
:returns: swob.Response object
"""
container_info = \
self.container_info(self.account_name, self.container_name)
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 not allowed_origins:
return Response(status=401, request=req)
headers = {}
if req.headers.get('Origin') in allowed_origins \
or '*' in allowed_origins:
headers['access-control-allow-origin'] = ' '.join(allowed_origins)
headers['access-control-max-age'] = cors.get('max_age')
headers['access-control-allow-methods'] = \
'GET, POST, PUT, DELETE, HEAD'
headers['access-control-allow-headers'] = \
cors.get('allow_headers')
return Response(status=200, headers=headers, request=req)
else:
return Response(status=401, request=req)

@public
def OPTIONS(self, req):
return self.OPTIONS_base(req)
22 changes: 14 additions & 8 deletions swift/proxy/server.py
Expand Up @@ -79,9 +79,9 @@ def __init__(self, conf, memcache=None, logger=None, account_ring=None,
self.resellers_conf.read(os.path.join(swift_dir, 'resellers.conf'))
self.object_ring = object_ring or Ring(swift_dir, ring_name='object')
self.container_ring = container_ring or Ring(swift_dir,
ring_name='container')
ring_name='container')
self.account_ring = account_ring or Ring(swift_dir,
ring_name='account')
ring_name='account')
self.memcache = memcache
mimetypes.init(mimetypes.knownfiles +
[os.path.join(swift_dir, 'mime.types')])
Expand All @@ -94,17 +94,23 @@ def __init__(self, conf, memcache=None, logger=None, account_ring=None,
int(conf.get('expiring_objects_container_divisor') or 86400)
self.max_containers_per_account = \
int(conf.get('max_containers_per_account') or 0)
self.max_containers_whitelist = [a.strip()
self.max_containers_whitelist = [
a.strip()
for a in conf.get('max_containers_whitelist', '').split(',')
if a.strip()]
self.deny_host_headers = [host.strip() for host in
self.deny_host_headers = [
host.strip() for host in
conf.get('deny_host_headers', '').split(',') if host.strip()]
self.rate_limit_after_segment = \
int(conf.get('rate_limit_after_segment', 10))
self.rate_limit_segments_per_sec = \
int(conf.get('rate_limit_segments_per_sec', 1))
self.log_handoffs = \
conf.get('log_handoffs', 'true').lower() in TRUE_VALUES
self.cors_allow_origin = [
a.strip()
for a in conf.get('cors_allow_origin', '').split(',')
if a.strip()]

def get_controller(self, path):
"""
Expand All @@ -117,9 +123,9 @@ def get_controller(self, path):
"""
version, account, container, obj = split_path(path, 1, 4, True)
d = dict(version=version,
account_name=account,
container_name=container,
object_name=obj)
account_name=account,
container_name=container,
object_name=obj)
if obj and container and account:
return ObjectController, d
elif container and account:
Expand All @@ -146,7 +152,7 @@ def __call__(self, env, start_response):
return err(env, start_response)
except (Exception, Timeout):
start_response('500 Server Error',
[('Content-Type', 'text/plain')])
[('Content-Type', 'text/plain')])
return ['Internal server error.\n']

def update_request(self, req):
Expand Down

0 comments on commit 74b27d5

Please sign in to comment.