Browse Source

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
Jeremy Freudberg 1 year ago
parent
commit
a1a9cad038

+ 16
- 11
mixmatch/proxy.py View File

@@ -57,7 +57,7 @@ def is_json_response(response):
57 57
 
58 58
 
59 59
 def 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
 
63 63
 def strip_tokens_from_headers(headers):
@@ -92,7 +92,7 @@ class RequestDetails(object):
92 92
         self.resource_type = utils.safe_pop(local_path)  # this
93 93
         self.resource_id = utils.pop_if_uuid(local_path)  # and this
94 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 96
         self.path = orig_path
97 97
         self.args = dict(request.args)
98 98
         # NOTE(jfreud): if chunked transfer, body must be accessed through
@@ -219,7 +219,7 @@ class RequestHandler(object):
219 219
         final_response = flask.Response(
220 220
             text,
221 221
             response.status_code,
222
-            headers=self._prepare_headers(response.headers)
222
+            headers=self._prepare_headers(response.headers, fix_case=True)
223 223
         )
224 224
         LOG.info(format_for_log(title='Response from proxy',
225 225
                                 status_code=final_response.status_code,
@@ -295,15 +295,20 @@ class RequestHandler(object):
295 295
         return self._forward()
296 296
 
297 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 305
         headers = dict()
300
-        headers['Accept'] = user_headers.get('Accept', '')
301
-        headers['Content-Type'] = user_headers.get('Content-Type', '')
302
-        accepted_headers = ['openstack-api-version']
306
+        headers['ACCEPT'] = user_headers.get('ACCEPT', '')
307
+        headers['CONTENT-TYPE'] = user_headers.get('CONTENT-TYPE', '')
308
+        accepted_headers = ['OPENSTACK-API-VERSION']
303 309
         for key, value in user_headers.items():
304
-            k = key.lower()
305
-            if ((k.startswith('x-') and not is_token_header_key(key)) or
306
-                    k in accepted_headers):
310
+            if ((key.startswith('X-') and not is_token_header_key(key)) or
311
+                    key in accepted_headers):
307 312
                 headers[key] = value
308 313
         return headers
309 314
 
@@ -321,7 +326,7 @@ class RequestHandler(object):
321 326
 
322 327
     @utils.CachedProperty
323 328
     def chunked(self):
324
-        encoding = self.details.headers.get('Transfer-Encoding', '')
329
+        encoding = self.details.headers.get('TRANSFER-ENCODING', '')
325 330
         return encoding.lower() == 'chunked'
326 331
 
327 332
     @utils.CachedProperty

+ 31
- 0
mixmatch/tests/unit/test_request_details.py View File

@@ -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
+
15
+from testtools import testcase
16
+
17
+from mixmatch import proxy
18
+
19
+
20
+class 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)

+ 28
- 19
mixmatch/tests/unit/test_request_handler.py View File

@@ -31,42 +31,51 @@ class TestRequestHandler(BaseTest):
31 31
 
32 32
     def test_prepare_headers(self):
33 33
         user_headers = {
34
-            'x-auth-token': 'auth token',
35
-            'x-service-token': 'service token',
36 34
             'X-AUTH-TOKEN': 'AUTH TOKEN',
37 35
             'X-SERVICE-TOKEN': 'SERVICE TOKEN',
38 36
 
39
-            'x-tra cheese': 'extra cheese',
40
-            'x-goth-token': 'x-auth-token',
37
+            'X-TRA CHEESE': 'extra cheese',
38
+            'X-GOTH-TOKEN': 'x-auth-token',
41 39
             'X-MEN': 'X MEN',
42 40
 
43
-            'y-men': 'y men',
44
-            'extra cheese': 'x-tra cheese',
45
-            'y-auth-token': 'x-auth-token',
46
-            'xauth-token': 'x-auth-token',
47
-            'start-x': 'startx',
41
+            'Y-MEN': 'y men',
42
+            'EXTRA CHEESE': 'x-tra cheese',
43
+            'Y-AUTH-TOKEN': 'x-auth-token',
44
+            'XAUTH-TOKEN': 'x-auth-token',
45
+            'START-X': 'startx',
48 46
 
49
-            'OpenStack-API-Version': 'volume 3.0'
47
+            'OPENSTACK-API-VERSION': 'volume 3.0'
50 48
         }
51 49
         expected_headers = {
52
-            'x-tra cheese': 'extra cheese',
53
-            'x-goth-token': 'x-auth-token',
50
+            'X-TRA CHEESE': 'extra cheese',
51
+            'X-GOTH-TOKEN': 'x-auth-token',
54 52
             'X-MEN': 'X MEN',
55
-            'Accept': '',
56
-            'Content-Type': '',
57
-            'OpenStack-API-Version': 'volume 3.0'
53
+            'ACCEPT': '',
54
+            'CONTENT-TYPE': '',
55
+            'OPENSTACK-API-VERSION': 'volume 3.0'
58 56
         }
59 57
         headers = proxy.RequestHandler._prepare_headers(user_headers)
60 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 73
     def test_strip_tokens_from_logs(self):
63 74
         token = uuid.uuid4()
64 75
         headers = {
65
-            'x-auth-token': token,
66 76
             'X-AUTH-TOKEN': token,
67
-            'not a token': 'not a token',
68
-            'X-Service-Token': token,
69
-            'x-service-token': token
77
+            'NOT A TOKEN': 'not a token',
78
+            'X-SERVICE-TOKEN': token,
70 79
         }
71 80
         stripped_headers = proxy.strip_tokens_from_headers(headers)
72 81
         self.assertFalse(token in stripped_headers.values())

Loading…
Cancel
Save