Merge "py3: Fix s3api header casing"

This commit is contained in:
Zuul 2019-11-21 21:56:25 +00:00 committed by Gerrit Code Review
commit d215fef8fa
3 changed files with 49 additions and 50 deletions

View File

@ -16,13 +16,6 @@
import six import six
def _title(s):
if six.PY2:
return s.title()
else:
return s.encode('latin1').title().decode('latin1')
class HeaderKeyDict(dict): class HeaderKeyDict(dict):
""" """
A dict that title-cases all keys on the way in, so as to be A dict that title-cases all keys on the way in, so as to be
@ -36,35 +29,43 @@ class HeaderKeyDict(dict):
self.update(base_headers) self.update(base_headers)
self.update(kwargs) self.update(kwargs)
@staticmethod
def _title(s):
if six.PY2:
return s.title()
else:
return s.encode('latin1').title().decode('latin1')
def update(self, other): def update(self, other):
if hasattr(other, 'keys'): if hasattr(other, 'keys'):
for key in other.keys(): for key in other.keys():
self[_title(key)] = other[key] self[self._title(key)] = other[key]
else: else:
for key, value in other: for key, value in other:
self[_title(key)] = value self[self._title(key)] = value
def __getitem__(self, key): def __getitem__(self, key):
return dict.get(self, _title(key)) return dict.get(self, self._title(key))
def __setitem__(self, key, value): def __setitem__(self, key, value):
key = self._title(key)
if value is None: if value is None:
self.pop(_title(key), None) self.pop(key, None)
elif six.PY2 and isinstance(value, six.text_type): elif six.PY2 and isinstance(value, six.text_type):
return dict.__setitem__(self, _title(key), value.encode('utf-8')) return dict.__setitem__(self, key, value.encode('utf-8'))
elif six.PY3 and isinstance(value, six.binary_type): elif six.PY3 and isinstance(value, six.binary_type):
return dict.__setitem__(self, _title(key), value.decode('latin-1')) return dict.__setitem__(self, key, value.decode('latin-1'))
else: else:
return dict.__setitem__(self, _title(key), str(value)) return dict.__setitem__(self, key, str(value))
def __contains__(self, key): def __contains__(self, key):
return dict.__contains__(self, _title(key)) return dict.__contains__(self, self._title(key))
def __delitem__(self, key): def __delitem__(self, key):
return dict.__delitem__(self, _title(key)) return dict.__delitem__(self, self._title(key))
def get(self, key, default=None): def get(self, key, default=None):
return dict.get(self, _title(key), default) return dict.get(self, self._title(key), default)
def setdefault(self, key, value=None): def setdefault(self, key, value=None):
if key not in self: if key not in self:
@ -72,4 +73,4 @@ class HeaderKeyDict(dict):
return self[key] return self[key]
def pop(self, key, default=None): def pop(self, key, default=None):
return dict.pop(self, _title(key), default) return dict.pop(self, self._title(key), default)

View File

@ -17,6 +17,7 @@ import re
from collections import MutableMapping from collections import MutableMapping
from functools import partial from functools import partial
from swift.common import header_key_dict
from swift.common import swob from swift.common import swob
from swift.common.utils import config_true_value from swift.common.utils import config_true_value
from swift.common.request_helpers import is_sys_meta from swift.common.request_helpers import is_sys_meta
@ -26,42 +27,21 @@ from swift.common.middleware.s3api.utils import snake_to_camel, \
from swift.common.middleware.s3api.etree import Element, SubElement, tostring from swift.common.middleware.s3api.etree import Element, SubElement, tostring
class HeaderKey(str): class HeaderKeyDict(header_key_dict.HeaderKeyDict):
""" """
A string object that normalizes string as S3 clients expect with title(). Similar to the Swift's normal HeaderKeyDict class, but its key name is
normalized as S3 clients expect.
""" """
def title(self): @staticmethod
if self.lower() == 'etag': def _title(s):
s = header_key_dict.HeaderKeyDict._title(s)
if s.lower() == 'etag':
# AWS Java SDK expects only 'ETag'. # AWS Java SDK expects only 'ETag'.
return 'ETag' return 'ETag'
if self.lower().startswith('x-amz-'): if s.lower().startswith('x-amz-'):
# AWS headers returned by S3 are lowercase. # AWS headers returned by S3 are lowercase.
return self.lower() return swob.bytes_to_wsgi(swob.wsgi_to_bytes(s).lower())
return str.title(self) return s
class HeaderKeyDict(swob.HeaderKeyDict):
"""
Similar to the HeaderKeyDict class in Swift, but its key name is normalized
as S3 clients expect.
"""
def __getitem__(self, key):
return swob.HeaderKeyDict.__getitem__(self, HeaderKey(key))
def __setitem__(self, key, value):
return swob.HeaderKeyDict.__setitem__(self, HeaderKey(key), value)
def __contains__(self, key):
return swob.HeaderKeyDict.__contains__(self, HeaderKey(key))
def __delitem__(self, key):
return swob.HeaderKeyDict.__delitem__(self, HeaderKey(key))
def get(self, key, default=None):
return swob.HeaderKeyDict.get(self, HeaderKey(key), default)
def pop(self, key, default=None):
return swob.HeaderKeyDict.pop(self, HeaderKey(key), default)
class S3ResponseBase(object): class S3ResponseBase(object):
@ -116,7 +96,7 @@ class S3Response(S3ResponseBase, swob.Response):
# Handle swift headers # Handle swift headers
for key, val in sw_headers.items(): for key, val in sw_headers.items():
_key = key.lower() _key = swob.bytes_to_wsgi(swob.wsgi_to_bytes(key).lower())
if _key.startswith('x-object-meta-'): if _key.startswith('x-object-meta-'):
# Note that AWS ignores user-defined headers with '=' in the # Note that AWS ignores user-defined headers with '=' in the

View File

@ -35,6 +35,24 @@ class TestResponse(unittest.TestCase):
else: else:
self.assertEqual('"theetag"', s3resp.headers['ETag']) self.assertEqual('"theetag"', s3resp.headers['ETag'])
def test_response_s3api_user_meta_headers(self):
resp = Response(headers={
'X-Object-Meta-Foo': 'Bar',
'X-Object-Meta-Non-\xdcnicode-Value': '\xff',
'X-Object-Sysmeta-Baz': 'quux',
'Etag': 'unquoted',
'Content-type': 'text/plain',
'content-length': '0',
})
s3resp = S3Response.from_swift_resp(resp)
self.assertEqual(dict(s3resp.headers), {
'x-amz-meta-foo': 'Bar',
'x-amz-meta-non-\xdcnicode-value': '\xff',
'ETag': '"unquoted"',
'Content-Type': 'text/plain',
'Content-Length': '0',
})
def test_response_s3api_sysmeta_headers(self): def test_response_s3api_sysmeta_headers(self):
for _server_type in ('object', 'container'): for _server_type in ('object', 'container'):
swift_headers = HeaderKeyDict( swift_headers = HeaderKeyDict(