summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeremy Freudberg <jeremyfreudberg@gmail.com>2017-10-06 21:49:39 +0000
committerJeremy Freudberg <jeremyfreudberg@gmail.com>2017-10-10 17:45:25 +0000
commita1a9cad038292e5ba981f5f827b22a9e9a049c6f (patch)
treede6ee0a44f547d7f2cff5baab3ca1b9c53184781
parent270f46e66bc0a720b6c27abc689008d160fa574e (diff)
Request headers are case insensitive
Per RFC2616. Within the codebase itself we represent headers as uppercase strings, but now they can be passed with any capitalization style. (Including whatever keystoneauth or requests chooses to send.) Change-Id: Ia4e932a91dec030b9efeb947759ceebdb7a426fc Closes-Bug: #1720433
Notes
Notes (review): Code-Review+2: Kristi Nikolla <knikolla@bu.edu> Workflow+1: Kristi Nikolla <knikolla@bu.edu> Verified+2: Jenkins Submitted-by: Jenkins Submitted-at: Fri, 13 Oct 2017 16:43:55 +0000 Reviewed-on: https://review.openstack.org/510238 Project: openstack/mixmatch Branch: refs/heads/master
-rw-r--r--mixmatch/proxy.py27
-rw-r--r--mixmatch/tests/unit/test_request_details.py31
-rw-r--r--mixmatch/tests/unit/test_request_handler.py47
3 files changed, 75 insertions, 30 deletions
diff --git a/mixmatch/proxy.py b/mixmatch/proxy.py
index 0cec502..73cee77 100644
--- a/mixmatch/proxy.py
+++ b/mixmatch/proxy.py
@@ -57,7 +57,7 @@ def is_json_response(response):
57 57
58 58
59def is_token_header_key(string): 59def is_token_header_key(string):
60 return string.lower() in ['x-auth-token', 'x-service-token'] 60 return string in ['X-AUTH-TOKEN', 'X-SERVICE-TOKEN']
61 61
62 62
63def strip_tokens_from_headers(headers): 63def strip_tokens_from_headers(headers):
@@ -92,7 +92,7 @@ class RequestDetails(object):
92 self.resource_type = utils.safe_pop(local_path) # this 92 self.resource_type = utils.safe_pop(local_path) # this
93 self.resource_id = utils.pop_if_uuid(local_path) # and this 93 self.resource_id = utils.pop_if_uuid(local_path) # and this
94 self.token = headers.get('X-AUTH-TOKEN', None) 94 self.token = headers.get('X-AUTH-TOKEN', None)
95 self.headers = dict(headers) 95 self.headers = {k.upper(): v for k, v in dict(headers).items()}
96 self.path = orig_path 96 self.path = orig_path
97 self.args = dict(request.args) 97 self.args = dict(request.args)
98 # NOTE(jfreud): if chunked transfer, body must be accessed through 98 # NOTE(jfreud): if chunked transfer, body must be accessed through
@@ -219,7 +219,7 @@ class RequestHandler(object):
219 final_response = flask.Response( 219 final_response = flask.Response(
220 text, 220 text,
221 response.status_code, 221 response.status_code,
222 headers=self._prepare_headers(response.headers) 222 headers=self._prepare_headers(response.headers, fix_case=True)
223 ) 223 )
224 LOG.info(format_for_log(title='Response from proxy', 224 LOG.info(format_for_log(title='Response from proxy',
225 status_code=final_response.status_code, 225 status_code=final_response.status_code,
@@ -295,15 +295,20 @@ class RequestHandler(object):
295 return self._forward() 295 return self._forward()
296 296
297 @staticmethod 297 @staticmethod
298 def _prepare_headers(user_headers): 298 def _prepare_headers(user_headers, fix_case=False):
299 # NOTE(jfreud): because this function may be called with either request
300 # headers or response headers, sometimes the header keys may not be
301 # already capitalized
302 if fix_case:
303 user_headers = {k.upper(): v for k, v in
304 dict(user_headers).items()}
299 headers = dict() 305 headers = dict()
300 headers['Accept'] = user_headers.get('Accept', '') 306 headers['ACCEPT'] = user_headers.get('ACCEPT', '')
301 headers['Content-Type'] = user_headers.get('Content-Type', '') 307 headers['CONTENT-TYPE'] = user_headers.get('CONTENT-TYPE', '')
302 accepted_headers = ['openstack-api-version'] 308 accepted_headers = ['OPENSTACK-API-VERSION']
303 for key, value in user_headers.items(): 309 for key, value in user_headers.items():
304 k = key.lower() 310 if ((key.startswith('X-') and not is_token_header_key(key)) or
305 if ((k.startswith('x-') and not is_token_header_key(key)) or 311 key in accepted_headers):
306 k in accepted_headers):
307 headers[key] = value 312 headers[key] = value
308 return headers 313 return headers
309 314
@@ -321,7 +326,7 @@ class RequestHandler(object):
321 326
322 @utils.CachedProperty 327 @utils.CachedProperty
323 def chunked(self): 328 def chunked(self):
324 encoding = self.details.headers.get('Transfer-Encoding', '') 329 encoding = self.details.headers.get('TRANSFER-ENCODING', '')
325 return encoding.lower() == 'chunked' 330 return encoding.lower() == 'chunked'
326 331
327 @utils.CachedProperty 332 @utils.CachedProperty
diff --git a/mixmatch/tests/unit/test_request_details.py b/mixmatch/tests/unit/test_request_details.py
new file mode 100644
index 0000000..30c1341
--- /dev/null
+++ b/mixmatch/tests/unit/test_request_details.py
@@ -0,0 +1,31 @@
1# Copyright 2017 Massachusetts Open Cloud
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from testtools import testcase
16
17from mixmatch import proxy
18
19
20class TestRequestDetails(testcase.TestCase):
21
22 def test_capitalized_headers(self):
23 normal_headers = {"Mm-Service-Provider": "default",
24 "X-Auth-Token": "tok",
25 "Transfer-Encoding": "chunked"}
26 with proxy.app.test_request_context():
27 rd = proxy.RequestDetails("GET", "image/v2/images", normal_headers)
28 expected = {"MM-SERVICE-PROVIDER": "default",
29 "X-AUTH-TOKEN": "tok",
30 "TRANSFER-ENCODING": "chunked"}
31 self.assertEqual(expected, rd.headers)
diff --git a/mixmatch/tests/unit/test_request_handler.py b/mixmatch/tests/unit/test_request_handler.py
index ecbb491..48ce23d 100644
--- a/mixmatch/tests/unit/test_request_handler.py
+++ b/mixmatch/tests/unit/test_request_handler.py
@@ -31,42 +31,51 @@ class TestRequestHandler(BaseTest):
31 31
32 def test_prepare_headers(self): 32 def test_prepare_headers(self):
33 user_headers = { 33 user_headers = {
34 'x-auth-token': 'auth token',
35 'x-service-token': 'service token',
36 'X-AUTH-TOKEN': 'AUTH TOKEN', 34 'X-AUTH-TOKEN': 'AUTH TOKEN',
37 'X-SERVICE-TOKEN': 'SERVICE TOKEN', 35 'X-SERVICE-TOKEN': 'SERVICE TOKEN',
38 36
39 'x-tra cheese': 'extra cheese', 37 'X-TRA CHEESE': 'extra cheese',
40 'x-goth-token': 'x-auth-token', 38 'X-GOTH-TOKEN': 'x-auth-token',
41 'X-MEN': 'X MEN', 39 'X-MEN': 'X MEN',
42 40
43 'y-men': 'y men', 41 'Y-MEN': 'y men',
44 'extra cheese': 'x-tra cheese', 42 'EXTRA CHEESE': 'x-tra cheese',
45 'y-auth-token': 'x-auth-token', 43 'Y-AUTH-TOKEN': 'x-auth-token',
46 'xauth-token': 'x-auth-token', 44 'XAUTH-TOKEN': 'x-auth-token',
47 'start-x': 'startx', 45 'START-X': 'startx',
48 46
49 'OpenStack-API-Version': 'volume 3.0' 47 'OPENSTACK-API-VERSION': 'volume 3.0'
50 } 48 }
51 expected_headers = { 49 expected_headers = {
52 'x-tra cheese': 'extra cheese', 50 'X-TRA CHEESE': 'extra cheese',
53 'x-goth-token': 'x-auth-token', 51 'X-GOTH-TOKEN': 'x-auth-token',
54 'X-MEN': 'X MEN', 52 'X-MEN': 'X MEN',
55 'Accept': '', 53 'ACCEPT': '',
56 'Content-Type': '', 54 'CONTENT-TYPE': '',
57 'OpenStack-API-Version': 'volume 3.0' 55 'OPENSTACK-API-VERSION': 'volume 3.0'
58 } 56 }
59 headers = proxy.RequestHandler._prepare_headers(user_headers) 57 headers = proxy.RequestHandler._prepare_headers(user_headers)
60 self.assertEqual(expected_headers, headers) 58 self.assertEqual(expected_headers, headers)
61 59
60 def test_prepare_headers_fix_case(self):
61 user_headers = {
62 'X-Auth-Token': 'AUTH TOKEN',
63 'X-Service-Token': 'SERVICE TOKEN',
64 'Openstack-Api-Version': 'volume 3.0'
65 }
66 headers = proxy.RequestHandler._prepare_headers(user_headers)
67 self.assertTrue('OPENSTACK-API-VERSION' not in headers.keys() and
68 'Openstack-Api-Version' not in headers.keys())
69 headers = proxy.RequestHandler._prepare_headers(user_headers, True)
70 self.assertTrue('OPENSTACK-API-VERSION' in headers.keys() and
71 'Openstack-Api-Version' not in headers.keys())
72
62 def test_strip_tokens_from_logs(self): 73 def test_strip_tokens_from_logs(self):
63 token = uuid.uuid4() 74 token = uuid.uuid4()
64 headers = { 75 headers = {
65 'x-auth-token': token,
66 'X-AUTH-TOKEN': token, 76 'X-AUTH-TOKEN': token,
67 'not a token': 'not a token', 77 'NOT A TOKEN': 'not a token',
68 'X-Service-Token': token, 78 'X-SERVICE-TOKEN': token,
69 'x-service-token': token
70 } 79 }
71 stripped_headers = proxy.strip_tokens_from_headers(headers) 80 stripped_headers = proxy.strip_tokens_from_headers(headers)
72 self.assertFalse(token in stripped_headers.values()) 81 self.assertFalse(token in stripped_headers.values())