Merge "Add Vary: headers for CORS responses"

This commit is contained in:
Jenkins 2017-02-23 01:45:29 +00:00 committed by Gerrit Code Review
commit 075c21a944
2 changed files with 50 additions and 24 deletions
swift/proxy/controllers
test/unit/proxy

@ -1886,28 +1886,37 @@ class Controller(object):
resp.status = HTTP_UNAUTHORIZED resp.status = HTTP_UNAUTHORIZED
return resp return resp
# Allow all headers requested in the request. The CORS
# specification does leave the door open for this, as mentioned in
# http://www.w3.org/TR/cors/#resource-preflight-requests
# Note: Since the list of headers can be unbounded
# simply returning headers can be enough.
allow_headers = set()
if req.headers.get('Access-Control-Request-Headers'):
allow_headers.update(
list_from_csv(req.headers['Access-Control-Request-Headers']))
# Populate the response with the CORS preflight headers # Populate the response with the CORS preflight headers
if cors.get('allow_origin') and \ if cors.get('allow_origin') and \
cors.get('allow_origin').strip() == '*': cors.get('allow_origin').strip() == '*':
headers['access-control-allow-origin'] = '*' headers['access-control-allow-origin'] = '*'
else: else:
headers['access-control-allow-origin'] = req_origin_value headers['access-control-allow-origin'] = req_origin_value
if 'vary' in headers:
headers['vary'] += ', Origin'
else:
headers['vary'] = 'Origin'
if cors.get('max_age') is not None: if cors.get('max_age') is not None:
headers['access-control-max-age'] = cors.get('max_age') headers['access-control-max-age'] = cors.get('max_age')
headers['access-control-allow-methods'] = \ headers['access-control-allow-methods'] = \
', '.join(self.allowed_methods) ', '.join(self.allowed_methods)
# Allow all headers requested in the request. The CORS
# specification does leave the door open for this, as mentioned in
# http://www.w3.org/TR/cors/#resource-preflight-requests
# Note: Since the list of headers can be unbounded
# simply returning headers can be enough.
allow_headers = set(
list_from_csv(req.headers.get('Access-Control-Request-Headers')))
if allow_headers: if allow_headers:
headers['access-control-allow-headers'] = ', '.join(allow_headers) headers['access-control-allow-headers'] = ', '.join(allow_headers)
if 'vary' in headers:
headers['vary'] += ', Access-Control-Request-Headers'
else:
headers['vary'] = 'Access-Control-Request-Headers'
resp.headers = headers resp.headers = headers
return resp return resp

@ -5081,6 +5081,8 @@ class TestObjectController(unittest.TestCase):
'Access-Control-Request-Method': 'GET'}) 'Access-Control-Request-Method': 'GET'})
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(401, resp.status_int) self.assertEqual(401, resp.status_int)
self.assertNotIn('Access-Control-Allow-Origin', resp.headers)
self.assertNotIn('Vary', resp.headers)
def my_empty_origin_container_info(*args): def my_empty_origin_container_info(*args):
return {'cors': {'allow_origin': None}} return {'cors': {'allow_origin': None}}
@ -5092,6 +5094,8 @@ class TestObjectController(unittest.TestCase):
'Access-Control-Request-Method': 'GET'}) 'Access-Control-Request-Method': 'GET'})
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(401, resp.status_int) self.assertEqual(401, resp.status_int)
self.assertNotIn('Access-Control-Allow-Origin', resp.headers)
self.assertNotIn('Vary', resp.headers)
def my_container_info(*args): def my_container_info(*args):
return { return {
@ -5112,13 +5116,13 @@ class TestObjectController(unittest.TestCase):
self.assertEqual( self.assertEqual(
'https://foo.bar', 'https://foo.bar',
resp.headers['access-control-allow-origin']) resp.headers['access-control-allow-origin'])
for verb in 'OPTIONS GET POST PUT DELETE HEAD'.split(): self.assertEqual('Origin', resp.headers.get('vary'))
self.assertIn(verb,
resp.headers['access-control-allow-methods'])
self.assertEqual( self.assertEqual(
len(resp.headers['access-control-allow-methods'].split(', ')), sorted(resp.headers['access-control-allow-methods']
6) .split(', ')),
sorted('OPTIONS GET POST PUT DELETE HEAD'.split()))
self.assertEqual('999', resp.headers['access-control-max-age']) self.assertEqual('999', resp.headers['access-control-max-age'])
req = Request.blank( req = Request.blank(
'/v1/a/c/o.jpg', '/v1/a/c/o.jpg',
{'REQUEST_METHOD': 'OPTIONS'}, {'REQUEST_METHOD': 'OPTIONS'},
@ -5126,19 +5130,28 @@ class TestObjectController(unittest.TestCase):
req.content_length = 0 req.content_length = 0
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(401, resp.status_int) self.assertEqual(401, resp.status_int)
self.assertNotIn('Access-Control-Allow-Origin', resp.headers)
self.assertNotIn('Vary', resp.headers)
req = Request.blank('/v1/a/c/o.jpg', {'REQUEST_METHOD': 'OPTIONS'}) req = Request.blank('/v1/a/c/o.jpg', {'REQUEST_METHOD': 'OPTIONS'})
req.content_length = 0 req.content_length = 0
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
for verb in 'OPTIONS GET POST PUT DELETE HEAD'.split(): self.assertEqual(
self.assertIn(verb, resp.headers['Allow']) sorted(resp.headers['Allow'].split(', ')),
self.assertEqual(len(resp.headers['Allow'].split(', ')), 6) sorted('OPTIONS GET POST PUT DELETE HEAD'.split()))
self.assertNotIn('Access-Control-Allow-Origin', resp.headers)
self.assertNotIn('Vary', resp.headers)
req = Request.blank( req = Request.blank(
'/v1/a/c/o.jpg', '/v1/a/c/o.jpg',
{'REQUEST_METHOD': 'OPTIONS'}, {'REQUEST_METHOD': 'OPTIONS'},
headers={'Origin': 'http://foo.com'}) headers={'Origin': 'http://foo.com'})
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(401, resp.status_int) self.assertEqual(401, resp.status_int)
self.assertNotIn('Access-Control-Allow-Origin', resp.headers)
self.assertNotIn('Vary', resp.headers)
req = Request.blank( req = Request.blank(
'/v1/a/c/o.jpg', '/v1/a/c/o.jpg',
{'REQUEST_METHOD': 'OPTIONS'}, {'REQUEST_METHOD': 'OPTIONS'},
@ -5147,6 +5160,7 @@ class TestObjectController(unittest.TestCase):
controller.app.cors_allow_origin = ['http://foo.bar', ] controller.app.cors_allow_origin = ['http://foo.bar', ]
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
self.assertEqual('Origin', resp.headers.get('vary'))
def my_container_info_wildcard(*args): def my_container_info_wildcard(*args):
return { return {
@ -5165,12 +5179,11 @@ class TestObjectController(unittest.TestCase):
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
self.assertEqual('*', resp.headers['access-control-allow-origin']) self.assertEqual('*', resp.headers['access-control-allow-origin'])
for verb in 'OPTIONS GET POST PUT DELETE HEAD'.split(): self.assertNotIn('Vary', resp.headers)
self.assertIn(verb,
resp.headers['access-control-allow-methods'])
self.assertEqual( self.assertEqual(
len(resp.headers['access-control-allow-methods'].split(', ')), sorted(resp.headers['access-control-allow-methods']
6) .split(', ')),
sorted('OPTIONS GET POST PUT DELETE HEAD'.split()))
self.assertEqual('999', resp.headers['access-control-max-age']) self.assertEqual('999', resp.headers['access-control-max-age'])
def _get_CORS_response(self, container_cors, strict_mode, object_get=None): def _get_CORS_response(self, container_cors, strict_mode, object_get=None):
@ -7485,11 +7498,13 @@ class TestContainerController(unittest.TestCase):
'/v1/a/c/o.jpg', '/v1/a/c/o.jpg',
{'REQUEST_METHOD': 'OPTIONS'}, {'REQUEST_METHOD': 'OPTIONS'},
headers={'Origin': 'https://bar.baz', headers={'Origin': 'https://bar.baz',
'Access-Control-Request-Headers': ' , ,,',
'Access-Control-Request-Method': 'GET'}) 'Access-Control-Request-Method': 'GET'})
req.content_length = 0 req.content_length = 0
resp = controller.OPTIONS(req) resp = controller.OPTIONS(req)
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
self.assertEqual('*', resp.headers['access-control-allow-origin']) self.assertEqual('*', resp.headers['access-control-allow-origin'])
self.assertNotIn('access-control-allow-headers', resp.headers)
for verb in 'OPTIONS GET POST PUT DELETE HEAD'.split(): for verb in 'OPTIONS GET POST PUT DELETE HEAD'.split():
self.assertIn(verb, self.assertIn(verb,
resp.headers['access-control-allow-methods']) resp.headers['access-control-allow-methods'])
@ -7503,7 +7518,7 @@ class TestContainerController(unittest.TestCase):
{'REQUEST_METHOD': 'OPTIONS'}, {'REQUEST_METHOD': 'OPTIONS'},
headers={'Origin': 'https://bar.baz', headers={'Origin': 'https://bar.baz',
'Access-Control-Request-Headers': 'Access-Control-Request-Headers':
'x-foo, x-bar, x-auth-token', 'x-foo, x-bar, , x-auth-token',
'Access-Control-Request-Method': 'GET'} 'Access-Control-Request-Method': 'GET'}
) )
req.content_length = 0 req.content_length = 0
@ -7512,6 +7527,8 @@ class TestContainerController(unittest.TestCase):
self.assertEqual( self.assertEqual(
sortHeaderNames('x-foo, x-bar, x-auth-token'), sortHeaderNames('x-foo, x-bar, x-auth-token'),
sortHeaderNames(resp.headers['access-control-allow-headers'])) sortHeaderNames(resp.headers['access-control-allow-headers']))
self.assertEqual('Access-Control-Request-Headers',
resp.headers.get('vary'))
def test_CORS_valid(self): def test_CORS_valid(self):
with save_globals(): with save_globals():