From a1a9cad038292e5ba981f5f827b22a9e9a049c6f Mon Sep 17 00:00:00 2001 From: Jeremy Freudberg Date: Fri, 6 Oct 2017 21:49:39 +0000 Subject: [PATCH] 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 --- mixmatch/proxy.py | 27 +++++++----- mixmatch/tests/unit/test_request_details.py | 31 ++++++++++++++ mixmatch/tests/unit/test_request_handler.py | 47 ++++++++++++--------- 3 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 mixmatch/tests/unit/test_request_details.py 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): def is_token_header_key(string): - return string.lower() in ['x-auth-token', 'x-service-token'] + return string in ['X-AUTH-TOKEN', 'X-SERVICE-TOKEN'] def strip_tokens_from_headers(headers): @@ -92,7 +92,7 @@ class RequestDetails(object): self.resource_type = utils.safe_pop(local_path) # this self.resource_id = utils.pop_if_uuid(local_path) # and this self.token = headers.get('X-AUTH-TOKEN', None) - self.headers = dict(headers) + self.headers = {k.upper(): v for k, v in dict(headers).items()} self.path = orig_path self.args = dict(request.args) # NOTE(jfreud): if chunked transfer, body must be accessed through @@ -219,7 +219,7 @@ class RequestHandler(object): final_response = flask.Response( text, response.status_code, - headers=self._prepare_headers(response.headers) + headers=self._prepare_headers(response.headers, fix_case=True) ) LOG.info(format_for_log(title='Response from proxy', status_code=final_response.status_code, @@ -295,15 +295,20 @@ class RequestHandler(object): return self._forward() @staticmethod - def _prepare_headers(user_headers): + def _prepare_headers(user_headers, fix_case=False): + # NOTE(jfreud): because this function may be called with either request + # headers or response headers, sometimes the header keys may not be + # already capitalized + if fix_case: + user_headers = {k.upper(): v for k, v in + dict(user_headers).items()} headers = dict() - headers['Accept'] = user_headers.get('Accept', '') - headers['Content-Type'] = user_headers.get('Content-Type', '') - accepted_headers = ['openstack-api-version'] + headers['ACCEPT'] = user_headers.get('ACCEPT', '') + headers['CONTENT-TYPE'] = user_headers.get('CONTENT-TYPE', '') + accepted_headers = ['OPENSTACK-API-VERSION'] for key, value in user_headers.items(): - k = key.lower() - if ((k.startswith('x-') and not is_token_header_key(key)) or - k in accepted_headers): + if ((key.startswith('X-') and not is_token_header_key(key)) or + key in accepted_headers): headers[key] = value return headers @@ -321,7 +326,7 @@ class RequestHandler(object): @utils.CachedProperty def chunked(self): - encoding = self.details.headers.get('Transfer-Encoding', '') + encoding = self.details.headers.get('TRANSFER-ENCODING', '') return encoding.lower() == 'chunked' @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 @@ +# Copyright 2017 Massachusetts Open Cloud +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from testtools import testcase + +from mixmatch import proxy + + +class TestRequestDetails(testcase.TestCase): + + def test_capitalized_headers(self): + normal_headers = {"Mm-Service-Provider": "default", + "X-Auth-Token": "tok", + "Transfer-Encoding": "chunked"} + with proxy.app.test_request_context(): + rd = proxy.RequestDetails("GET", "image/v2/images", normal_headers) + expected = {"MM-SERVICE-PROVIDER": "default", + "X-AUTH-TOKEN": "tok", + "TRANSFER-ENCODING": "chunked"} + 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): def test_prepare_headers(self): user_headers = { - 'x-auth-token': 'auth token', - 'x-service-token': 'service token', 'X-AUTH-TOKEN': 'AUTH TOKEN', 'X-SERVICE-TOKEN': 'SERVICE TOKEN', - 'x-tra cheese': 'extra cheese', - 'x-goth-token': 'x-auth-token', + 'X-TRA CHEESE': 'extra cheese', + 'X-GOTH-TOKEN': 'x-auth-token', 'X-MEN': 'X MEN', - 'y-men': 'y men', - 'extra cheese': 'x-tra cheese', - 'y-auth-token': 'x-auth-token', - 'xauth-token': 'x-auth-token', - 'start-x': 'startx', + 'Y-MEN': 'y men', + 'EXTRA CHEESE': 'x-tra cheese', + 'Y-AUTH-TOKEN': 'x-auth-token', + 'XAUTH-TOKEN': 'x-auth-token', + 'START-X': 'startx', - 'OpenStack-API-Version': 'volume 3.0' + 'OPENSTACK-API-VERSION': 'volume 3.0' } expected_headers = { - 'x-tra cheese': 'extra cheese', - 'x-goth-token': 'x-auth-token', + 'X-TRA CHEESE': 'extra cheese', + 'X-GOTH-TOKEN': 'x-auth-token', 'X-MEN': 'X MEN', - 'Accept': '', - 'Content-Type': '', - 'OpenStack-API-Version': 'volume 3.0' + 'ACCEPT': '', + 'CONTENT-TYPE': '', + 'OPENSTACK-API-VERSION': 'volume 3.0' } headers = proxy.RequestHandler._prepare_headers(user_headers) self.assertEqual(expected_headers, headers) + def test_prepare_headers_fix_case(self): + user_headers = { + 'X-Auth-Token': 'AUTH TOKEN', + 'X-Service-Token': 'SERVICE TOKEN', + 'Openstack-Api-Version': 'volume 3.0' + } + headers = proxy.RequestHandler._prepare_headers(user_headers) + self.assertTrue('OPENSTACK-API-VERSION' not in headers.keys() and + 'Openstack-Api-Version' not in headers.keys()) + headers = proxy.RequestHandler._prepare_headers(user_headers, True) + self.assertTrue('OPENSTACK-API-VERSION' in headers.keys() and + 'Openstack-Api-Version' not in headers.keys()) + def test_strip_tokens_from_logs(self): token = uuid.uuid4() headers = { - 'x-auth-token': token, 'X-AUTH-TOKEN': token, - 'not a token': 'not a token', - 'X-Service-Token': token, - 'x-service-token': token + 'NOT A TOKEN': 'not a token', + 'X-SERVICE-TOKEN': token, } stripped_headers = proxy.strip_tokens_from_headers(headers) self.assertFalse(token in stripped_headers.values())