diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py index fda02b7..8dab74f 100644 --- a/Lib/http/cookies.py +++ b/Lib/http/cookies.py @@ -134,10 +134,6 @@ import string __all__ = ["CookieError", "BaseCookie", "SimpleCookie"] -_nulljoin = ''.join -_semispacejoin = '; '.join -_spacejoin = ' '.join - def _warn_deprecated_setter(setter): import warnings msg = ('The .%s setter is deprecated. The attribute will be read-only in ' @@ -164,60 +160,62 @@ class CookieError(Exception): # _LegalChars is the list of chars which don't require "'s # _Translator hash-table for fast quoting # -_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" -_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}' +_LegalChars = string.ascii_letters.encode('ascii') \ + + string.digits.encode('ascii') \ + + b"!#$%&'*+-.^_`|~:" +_UnescapedChars = _LegalChars + b' ()/<=>?@[]{}' -_Translator = {n: '\\%03o' % n - for n in set(range(256)) - set(map(ord, _UnescapedChars))} +_Translator = {n: b'\\%03o' % n + for n in set(range(256)) - set(_UnescapedChars)} _Translator.update({ - ord('"'): '\\"', - ord('\\'): '\\\\', + ord(b'"'): b'\\"', + ord(b'\\'): b'\\\\', }) -_is_legal_key = re.compile('[%s]+' % _LegalChars).fullmatch +_is_legal_key = re.compile(br'[%s]+' % _LegalChars).fullmatch -def _quote(str): +def _quote(s): r"""Quote a string for use in a cookie header. If the string does not need to be double-quoted, then just return the string. Otherwise, surround the string in doublequotes and quote (with a \) special characters. """ - if str is None or _is_legal_key(str): - return str + if s is None or _is_legal_key(s): + return s else: - return '"' + str.translate(_Translator) + '"' + return b'"' + s.translate(_Translator) + b'"' -_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") -_QuotePatt = re.compile(r"[\\].") +_OctalPatt = re.compile(br"\\[0-3][0-7][0-7]") +_QuotePatt = re.compile(br"[\\].") -def _unquote(str): +def _unquote(s): # If there aren't any doublequotes, # then there can't be any special characters. See RFC 2109. - if str is None or len(str) < 2: - return str - if str[0] != '"' or str[-1] != '"': - return str + if s is None or len(s) < 2: + return s + if s[0] != ord('"') or s[-1] != ord('"'): + return s - # We have to assume that we must decode this string. + # We have to assume that we must decode this s. # Down to work. # Remove the "s - str = str[1:-1] + s = s[1:-1] # Check for special sequences. Examples: # \012 --> \n # \" --> " # i = 0 - n = len(str) + n = len(s) res = [] while 0 <= i < n: - o_match = _OctalPatt.search(str, i) - q_match = _QuotePatt.search(str, i) + o_match = _OctalPatt.search(s, i) + q_match = _QuotePatt.search(s, i) if not o_match and not q_match: # Neither matched - res.append(str[i:]) + res.append(s[i:]) break # else: j = k = -1 @@ -226,14 +224,14 @@ def _unquote(str): if q_match: k = q_match.start(0) if q_match and (not o_match or k < j): # QuotePatt matched - res.append(str[i:k]) - res.append(str[k+1]) + res.append(s[i:k]) + res.append(s[k+1]) i = k + 2 else: # OctalPatt matched - res.append(str[i:j]) - res.append(chr(int(str[j+1:j+4], 8))) + res.append(s[i:j]) + res.append(chr(int(s[j+1:j+4], 8))) i = j + 4 - return _nulljoin(res) + return b''.join(res) # The _getdate() routine is used to set the expiration time in the cookie's HTTP # header. By default, _getdate() returns the current time in the appropriate @@ -242,17 +240,17 @@ def _unquote(str): # ago". The offset may be a floating point number. # -_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] +_weekdayname = [b'Mon', b'Tue', b'Wed', b'Thu', b'Fri', b'Sat', b'Sun'] _monthname = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + b'Jan', b'Feb', b'Mar', b'Apr', b'May', b'Jun', + b'Jul', b'Aug', b'Sep', b'Oct', b'Nov', b'Dec'] def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): from time import gmtime, time now = time() year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future) - return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \ + return b"%s, %02d %3s %4d %02d:%02d:%02d GMT" % \ (weekdayname[wd], day, monthname[month], year, hh, mm, ss) @@ -279,25 +277,25 @@ class Morsel(dict): # variant on the left to the appropriate traditional # formatting on the right. _reserved = { - "expires" : "expires", - "path" : "Path", - "comment" : "Comment", - "domain" : "Domain", - "max-age" : "Max-Age", - "secure" : "Secure", - "httponly" : "HttpOnly", - "version" : "Version", + b"expires" : b"expires", + b"path" : b"Path", + b"comment" : b"Comment", + b"domain" : b"Domain", + b"max-age" : b"Max-Age", + b"secure" : b"Secure", + b"httponly" : b"HttpOnly", + b"version" : b"Version", } - _flags = {'secure', 'httponly'} + _flags = {b'secure', b'httponly'} def __init__(self): # Set defaults - self._key = self._value = self._coded_value = None + self._key = self._value = self._coded_value = b'' # Set default attributes for key in self._reserved: - dict.__setitem__(self, key, "") + dict.__setitem__(self, key, b"") @property def key(self): @@ -396,13 +394,16 @@ class Morsel(dict): self._value = state['value'] self._coded_value = state['coded_value'] - def output(self, attrs=None, header="Set-Cookie:"): - return "%s %s" % (header, self.OutputString(attrs)) + def output(self, attrs=None, header=b"Set-Cookie:"): + return b"%s %s" % (header, self.OutputString(attrs)) + + __bytes__ = output - __str__ = output + def __str__(self): + return self.output().decode('ascii') def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, self.OutputString()) + return '<%s: %s>' % (self.__class__.__name__, self.OutputString().decode('ascii')) def js_output(self, attrs=None): # Print javascript @@ -412,7 +413,7 @@ class Morsel(dict): document.cookie = \"%s\"; // end hiding --> - """ % (self.OutputString(attrs).replace('"', r'\"')) + """ % (self.OutputString(attrs).replace(b'"', br'\"').decode('ascii')) def OutputString(self, attrs=None): # Build up our result @@ -421,29 +422,29 @@ class Morsel(dict): append = result.append # First, the key=value pair - append("%s=%s" % (self.key, self.coded_value)) + append(b"%s=%s" % (self.key, self.coded_value)) # Now add any defined attributes if attrs is None: attrs = self._reserved items = sorted(self.items()) for key, value in items: - if value == "": + if value == b"": continue if key not in attrs: continue - if key == "expires" and isinstance(value, int): - append("%s=%s" % (self._reserved[key], _getdate(value))) - elif key == "max-age" and isinstance(value, int): - append("%s=%d" % (self._reserved[key], value)) + if key == b"expires" and isinstance(value, int): + append(b"%s=%s" % (self._reserved[key], _getdate(value))) + elif key == b"max-age" and isinstance(value, int): + append(b"%s=%d" % (self._reserved[key], value)) elif key in self._flags: if value: - append(str(self._reserved[key])) + append(self._reserved[key]) else: - append("%s=%s" % (self._reserved[key], value)) + append(b"%s=%s" % (self._reserved[key], value)) # Return the result - return _semispacejoin(result) + return b'; '.join(result) # @@ -455,13 +456,13 @@ class Morsel(dict): # result, the parsing rules here are less strict. # -_LegalKeyChars = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=" -_LegalValueChars = _LegalKeyChars + '\[\]' -_CookiePattern = re.compile(r""" +_LegalKeyChars = br"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=" +_LegalValueChars = _LegalKeyChars + br'\[\]' +_CookiePattern = re.compile(br""" (?x) # This is a verbose pattern \s* # Optional whitespace at start of cookie (?P # Start of group 'key' - [""" + _LegalKeyChars + r"""]+? # Any word of at least one letter + [""" + _LegalKeyChars + br"""]+? # Any word of at least one letter ) # End of group 'key' ( # Optional group: there may not be a value. \s*=\s* # Equal Sign @@ -470,7 +471,7 @@ _CookiePattern = re.compile(r""" | # or \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr | # or - [""" + _LegalValueChars + r"""]* # Any word or empty string + [""" + _LegalValueChars + br"""]* # Any word or empty string ) # End of group 'val' )? # End of optional value group \s* # Any number of spaces. @@ -499,7 +500,7 @@ class BaseCookie(dict): representation. The VALUE is the value being assigned. Override this function to modify the behavior of cookies. """ - strval = str(val) + strval = bytes(val) return strval, strval def __init__(self, input=None): @@ -521,7 +522,7 @@ class BaseCookie(dict): rval, cval = self.value_encode(value) self.__set(key, rval, cval) - def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): + def output(self, attrs=None, header=b"Set-Cookie:", sep=b"\015\012"): """Return a string suitable for HTTP.""" result = [] items = sorted(self.items()) @@ -529,14 +530,17 @@ class BaseCookie(dict): result.append(value.output(attrs, header)) return sep.join(result) - __str__ = output + __bytes__ = output + + def __str__(self): + return self.output().decode('ascii') def __repr__(self): l = [] items = sorted(self.items()) for key, value in items: l.append('%s=%s' % (key, repr(value.value))) - return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l)) + return '<%s: %s>' % (self.__class__.__name__, ' '.join(l)) def js_output(self, attrs=None): """Return a string suitable for JavaScript.""" @@ -544,7 +548,7 @@ class BaseCookie(dict): items = sorted(self.items()) for key, value in items: result.append(value.js_output(attrs)) - return _nulljoin(result) + return ''.join(result) def load(self, rawdata): """Load cookies from a string (presumably HTTP_COOKIE) or @@ -552,19 +556,21 @@ class BaseCookie(dict): is equivalent to calling: map(Cookie.__setitem__, d.keys(), d.values()) """ - if isinstance(rawdata, str): + if isinstance(rawdata, bytes): self.__parse_string(rawdata) - else: + elif hasattr(rawdata, 'items'): # self.update() wouldn't call our custom __setitem__ for key, value in rawdata.items(): self[key] = value + else: + raise TypeError("Please provide a byte string or a dict of byte strings.") return - def __parse_string(self, str, patt=_CookiePattern): - i = 0 # Our starting point - n = len(str) # Length of string - parsed_items = [] # Parsed (type, key, value) triples - morsel_seen = False # A key=value pair was previously encountered + def __parse_string(self, s, patt=_CookiePattern): + i = 0 # Our starting point + n = len(s) # Length of the bytestring + parsed_items = [] # Parsed (type, key, value) triples + morsel_seen = False # A key=value pair was previously encountered TYPE_ATTRIBUTE = 1 TYPE_KEYVALUE = 2 @@ -574,7 +580,7 @@ class BaseCookie(dict): # attacks). while 0 <= i < n: # Start looking for a cookie - match = patt.match(str, i) + match = patt.match(s, i) if not match: # No more cookies break @@ -582,7 +588,7 @@ class BaseCookie(dict): key, value = match.group("key"), match.group("val") i = match.end(0) - if key[0] == "$": + if key[0] == b"$": if not morsel_seen: # We ignore attributes which pertain to the cookie # mechanism as a whole, such as "$Version". @@ -632,5 +638,5 @@ class SimpleCookie(BaseCookie): return _unquote(val), val def value_encode(self, val): - strval = str(val) + strval = bytes(val) return strval, _quote(strval) diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py index f74ab98..1f5858c 100644 --- a/Lib/test/test_http_cookies.py +++ b/Lib/test/test_http_cookies.py @@ -69,14 +69,14 @@ class CookieTests(unittest.TestCase): def test_load(self): C = cookies.SimpleCookie() - C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme') + C.load(b'Customer="WILE_E_COYOTE"; Version=1; Path=/acme') - self.assertEqual(C['Customer'].value, 'WILE_E_COYOTE') - self.assertEqual(C['Customer']['version'], '1') - self.assertEqual(C['Customer']['path'], '/acme') + self.assertEqual(C[b'Customer'].value, b'WILE_E_COYOTE') + self.assertEqual(C[b'Customer'][b'version'], b'1') + self.assertEqual(C[b'Customer'][b'path'], b'/acme') - self.assertEqual(C.output(['path']), - 'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme') + self.assertEqual(C.output([b'path']), + b'Set-Cookie: Customer="WILE_E_COYOTE"; Path=/acme') self.assertEqual(C.js_output(), r""" """) - self.assertEqual(C.js_output(['path']), r""" + self.assertEqual(C.js_output([b'path']), r""" """) - self.assertEqual(C.js_output(['path']), r""" + self.assertEqual(C.js_output([b'path']), r""" - """ % (i, "%s_coded_val" % i) + """ % (i.decode('ascii'), "%s_coded_val" % i.decode('ascii')) self.assertEqual(M.js_output(), expected_js_output) - for i in ["foo bar", "foo@bar"]: + for i in [b"foo bar", b"foo@bar"]: # Try some illegal characters self.assertRaises(cookies.CookieError, - M.set, i, '%s_value' % i, '%s_value' % i) + M.set, i, b'%s_value' % i, b'%s_value' % i) def test_deprecation(self): morsel = cookies.Morsel() @@ -357,68 +357,68 @@ class MorselTests(unittest.TestCase): def test_setitem(self): morsel = cookies.Morsel() - morsel['expires'] = 0 - self.assertEqual(morsel['expires'], 0) - morsel['Version'] = 2 - self.assertEqual(morsel['version'], 2) - morsel['DOMAIN'] = 'example.com' - self.assertEqual(morsel['domain'], 'example.com') + morsel[b'expires'] = 0 + self.assertEqual(morsel[b'expires'], 0) + morsel[b'Version'] = 2 + self.assertEqual(morsel[b'version'], 2) + morsel[b'DOMAIN'] = b'example.com' + self.assertEqual(morsel[b'domain'], b'example.com') with self.assertRaises(cookies.CookieError): - morsel['invalid'] = 'value' - self.assertNotIn('invalid', morsel) + morsel[b'invalid'] = b'value' + self.assertNotIn(b'invalid', morsel) def test_setdefault(self): morsel = cookies.Morsel() morsel.update({ - 'domain': 'example.com', - 'version': 2, + b'domain': b'example.com', + b'version': 2, }) # this shouldn't override the default value - self.assertEqual(morsel.setdefault('expires', 'value'), '') - self.assertEqual(morsel['expires'], '') - self.assertEqual(morsel.setdefault('Version', 1), 2) - self.assertEqual(morsel['version'], 2) - self.assertEqual(morsel.setdefault('DOMAIN', 'value'), 'example.com') - self.assertEqual(morsel['domain'], 'example.com') + self.assertEqual(morsel.setdefault(b'expires', b'value'), b'') + self.assertEqual(morsel[b'expires'], b'') + self.assertEqual(morsel.setdefault(b'Version', 1), 2) + self.assertEqual(morsel[b'version'], 2) + self.assertEqual(morsel.setdefault(b'DOMAIN', b'value'), b'example.com') + self.assertEqual(morsel[b'domain'], b'example.com') with self.assertRaises(cookies.CookieError): - morsel.setdefault('invalid', 'value') - self.assertNotIn('invalid', morsel) + morsel.setdefault(b'invalid', b'value') + self.assertNotIn(b'invalid', morsel) def test_update(self): - attribs = {'expires': 1, 'Version': 2, 'DOMAIN': 'example.com'} + attribs = {b'expires': 1, b'Version': 2, b'DOMAIN': b'example.com'} # test dict update morsel = cookies.Morsel() morsel.update(attribs) - self.assertEqual(morsel['expires'], 1) - self.assertEqual(morsel['version'], 2) - self.assertEqual(morsel['domain'], 'example.com') + self.assertEqual(morsel[b'expires'], 1) + self.assertEqual(morsel[b'version'], 2) + self.assertEqual(morsel[b'domain'], b'example.com') # test iterable update morsel = cookies.Morsel() morsel.update(list(attribs.items())) - self.assertEqual(morsel['expires'], 1) - self.assertEqual(morsel['version'], 2) - self.assertEqual(morsel['domain'], 'example.com') + self.assertEqual(morsel[b'expires'], 1) + self.assertEqual(morsel[b'version'], 2) + self.assertEqual(morsel[b'domain'], b'example.com') # test iterator update morsel = cookies.Morsel() morsel.update((k, v) for k, v in attribs.items()) - self.assertEqual(morsel['expires'], 1) - self.assertEqual(morsel['version'], 2) - self.assertEqual(morsel['domain'], 'example.com') + self.assertEqual(morsel[b'expires'], 1) + self.assertEqual(morsel[b'version'], 2) + self.assertEqual(morsel[b'domain'], b'example.com') with self.assertRaises(cookies.CookieError): - morsel.update({'invalid': 'value'}) - self.assertNotIn('invalid', morsel) + morsel.update({b'invalid': b'value'}) + self.assertNotIn(b'invalid', morsel) self.assertRaises(TypeError, morsel.update) self.assertRaises(TypeError, morsel.update, 0) def test_pickle(self): morsel_a = cookies.Morsel() - morsel_a.set('foo', 'bar', 'baz') + morsel_a.set(b'foo', b'bar', b'baz') morsel_a.update({ - 'version': 2, - 'comment': 'foo', + b'version': 2, + b'comment': b'foo', }) for proto in range(pickle.HIGHEST_PROTOCOL + 1): with self.subTest(proto=proto):