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
This commit is contained in:
Jeremy Freudberg 2017-10-06 21:49:39 +00:00
parent 270f46e66b
commit a1a9cad038
3 changed files with 75 additions and 30 deletions

View File

@ -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

View File

@ -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)

View File

@ -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())