2016-10-25 09:27:54 +01:00
|
|
|
import re
|
|
|
|
import string
|
|
|
|
|
2019-02-06 18:29:33 +00:00
|
|
|
from datetime import datetime
|
2021-01-29 14:19:10 +00:00
|
|
|
from typing import Dict
|
2019-02-06 18:29:33 +00:00
|
|
|
|
2019-01-09 21:47:26 +00:00
|
|
|
|
2019-01-03 23:01:54 +00:00
|
|
|
DEFAULT_MAX_AGE = 0
|
2018-10-18 05:20:16 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
# SimpleCookie
|
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
|
|
|
|
# Straight up copied this section of dark magic from SimpleCookie
|
|
|
|
|
|
|
|
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
2018-10-14 01:55:33 +01:00
|
|
|
_UnescapedChars = _LegalChars + " ()/<=>?@[]{}"
|
2016-10-25 09:27:54 +01:00
|
|
|
|
2018-10-14 01:55:33 +01:00
|
|
|
_Translator = {
|
|
|
|
n: "\\%03o" % n for n in set(range(256)) - set(map(ord, _UnescapedChars))
|
|
|
|
}
|
|
|
|
_Translator.update({ord('"'): '\\"', ord("\\"): "\\\\"})
|
2016-10-25 09:27:54 +01:00
|
|
|
|
2016-10-25 09:36:12 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
def _quote(str):
|
2018-10-26 09:29:53 +01:00
|
|
|
r"""Quote a string for use in a cookie header.
|
2016-10-25 09:27:54 +01:00
|
|
|
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
|
|
|
|
else:
|
|
|
|
return '"' + str.translate(_Translator) + '"'
|
|
|
|
|
2016-11-16 18:55:13 +00:00
|
|
|
|
2018-10-14 01:55:33 +01:00
|
|
|
_is_legal_key = re.compile("[%s]+" % re.escape(_LegalChars)).fullmatch
|
2016-10-25 09:27:54 +01:00
|
|
|
|
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
# Custom SimpleCookie
|
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
|
2016-10-25 09:36:12 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
class CookieJar(dict):
|
2021-01-29 14:19:10 +00:00
|
|
|
"""
|
|
|
|
CookieJar dynamically writes headers as cookies are added and removed
|
2016-10-25 09:27:54 +01:00
|
|
|
It gets around the limitation of one header per name by using the
|
2017-01-25 09:53:39 +00:00
|
|
|
MultiHeader class to provide a unique key that encodes to Set-Cookie.
|
2016-10-25 09:27:54 +01:00
|
|
|
"""
|
2017-01-25 09:53:39 +00:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
def __init__(self, headers):
|
|
|
|
super().__init__()
|
2021-01-29 14:19:10 +00:00
|
|
|
self.headers: Dict[str, str] = headers
|
|
|
|
self.cookie_headers: Dict[str, str] = {}
|
|
|
|
self.header_key: str = "Set-Cookie"
|
2016-10-25 09:36:12 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
def __setitem__(self, key, value):
|
|
|
|
# If this cookie doesn't exist, add it to the header keys
|
2018-07-11 09:44:21 +01:00
|
|
|
if not self.cookie_headers.get(key):
|
2016-10-25 09:27:54 +01:00
|
|
|
cookie = Cookie(key, value)
|
2018-10-14 01:55:33 +01:00
|
|
|
cookie["path"] = "/"
|
2018-07-11 09:44:21 +01:00
|
|
|
self.cookie_headers[key] = self.header_key
|
|
|
|
self.headers.add(self.header_key, cookie)
|
2016-10-25 09:27:54 +01:00
|
|
|
return super().__setitem__(key, cookie)
|
|
|
|
else:
|
|
|
|
self[key].value = value
|
|
|
|
|
|
|
|
def __delitem__(self, key):
|
2017-01-25 09:53:39 +00:00
|
|
|
if key not in self.cookie_headers:
|
2018-10-14 01:55:33 +01:00
|
|
|
self[key] = ""
|
|
|
|
self[key]["max-age"] = 0
|
2017-01-25 09:53:39 +00:00
|
|
|
else:
|
|
|
|
cookie_header = self.cookie_headers[key]
|
2018-07-11 09:44:21 +01:00
|
|
|
# remove it from header
|
|
|
|
cookies = self.headers.popall(cookie_header)
|
|
|
|
for cookie in cookies:
|
|
|
|
if cookie.key != key:
|
|
|
|
self.headers.add(cookie_header, cookie)
|
2017-01-25 09:53:39 +00:00
|
|
|
del self.cookie_headers[key]
|
|
|
|
return super().__delitem__(key)
|
2016-10-25 09:27:54 +01:00
|
|
|
|
2016-10-25 09:36:12 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
class Cookie(dict):
|
2017-02-14 19:10:19 +00:00
|
|
|
"""A stripped down version of Morsel from SimpleCookie #gottagofast"""
|
2018-10-14 01:55:33 +01:00
|
|
|
|
2016-10-25 09:27:54 +01:00
|
|
|
_keys = {
|
2016-10-25 09:36:12 +01:00
|
|
|
"expires": "expires",
|
|
|
|
"path": "Path",
|
|
|
|
"comment": "Comment",
|
|
|
|
"domain": "Domain",
|
|
|
|
"max-age": "Max-Age",
|
|
|
|
"secure": "Secure",
|
|
|
|
"httponly": "HttpOnly",
|
|
|
|
"version": "Version",
|
2017-12-24 10:33:52 +00:00
|
|
|
"samesite": "SameSite",
|
2016-10-25 09:27:54 +01:00
|
|
|
}
|
2018-10-14 01:55:33 +01:00
|
|
|
_flags = {"secure", "httponly"}
|
2016-10-25 09:27:54 +01:00
|
|
|
|
|
|
|
def __init__(self, key, value):
|
|
|
|
if key in self._keys:
|
|
|
|
raise KeyError("Cookie name is a reserved word")
|
|
|
|
if not _is_legal_key(key):
|
|
|
|
raise KeyError("Cookie key contains illegal characters")
|
|
|
|
self.key = key
|
|
|
|
self.value = value
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
def __setitem__(self, key, value):
|
2016-10-25 09:36:12 +01:00
|
|
|
if key not in self._keys:
|
2016-10-25 09:27:54 +01:00
|
|
|
raise KeyError("Unknown cookie property")
|
2017-10-06 06:20:36 +01:00
|
|
|
if value is not False:
|
2019-01-03 23:01:54 +00:00
|
|
|
if key.lower() == "max-age":
|
|
|
|
if not str(value).isdigit():
|
2021-01-18 23:11:39 +00:00
|
|
|
raise ValueError("Cookie max-age must be an integer")
|
2019-02-06 18:29:33 +00:00
|
|
|
elif key.lower() == "expires":
|
|
|
|
if not isinstance(value, datetime):
|
|
|
|
raise TypeError(
|
|
|
|
"Cookie 'expires' property must be a datetime"
|
|
|
|
)
|
2017-10-06 06:20:36 +01:00
|
|
|
return super().__setitem__(key, value)
|
2016-10-25 09:27:54 +01:00
|
|
|
|
|
|
|
def encode(self, encoding):
|
2018-12-08 06:27:10 +00:00
|
|
|
"""
|
|
|
|
Encode the cookie content in a specific type of encoding instructed
|
|
|
|
by the developer. Leverages the :func:`str.encode` method provided
|
|
|
|
by python.
|
|
|
|
|
|
|
|
This method can be used to encode and embed ``utf-8`` content into
|
|
|
|
the cookies.
|
|
|
|
|
|
|
|
:param encoding: Encoding to be used with the cookie
|
|
|
|
:return: Cookie encoded in a codec of choosing.
|
|
|
|
:except: UnicodeEncodeError
|
|
|
|
"""
|
2019-12-23 23:30:45 +00:00
|
|
|
return str(self).encode(encoding)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""Format as a Set-Cookie header value."""
|
2018-10-14 01:55:33 +01:00
|
|
|
output = ["%s=%s" % (self.key, _quote(self.value))]
|
2016-10-25 09:27:54 +01:00
|
|
|
for key, value in self.items():
|
2018-10-14 01:55:33 +01:00
|
|
|
if key == "max-age":
|
2017-02-16 03:21:23 +00:00
|
|
|
try:
|
2018-10-14 01:55:33 +01:00
|
|
|
output.append("%s=%d" % (self._keys[key], value))
|
2017-02-16 03:21:23 +00:00
|
|
|
except TypeError:
|
2018-10-14 01:55:33 +01:00
|
|
|
output.append("%s=%s" % (self._keys[key], value))
|
|
|
|
elif key == "expires":
|
2019-02-06 18:29:33 +00:00
|
|
|
output.append(
|
|
|
|
"%s=%s"
|
|
|
|
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
|
|
|
|
)
|
2017-05-11 19:49:32 +01:00
|
|
|
elif key in self._flags and self[key]:
|
|
|
|
output.append(self._keys[key])
|
2016-10-25 09:27:54 +01:00
|
|
|
else:
|
2018-10-14 01:55:33 +01:00
|
|
|
output.append("%s=%s" % (self._keys[key], value))
|
2016-10-25 09:27:54 +01:00
|
|
|
|
2019-12-23 23:30:45 +00:00
|
|
|
return "; ".join(output)
|