sanic/sanic/cookies.py

157 lines
5.1 KiB
Python
Raw Normal View History

2016-10-25 09:27:54 +01:00
import re
import string
from datetime import datetime
2021-01-29 14:19:10 +00:00
from typing import Dict
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) + '"'
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
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"] = "/"
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]
# 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):
"""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():
raise ValueError("Cookie max-age must be an integer")
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):
"""
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
"""
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":
output.append(
"%s=%s"
% (self._keys[key], value.strftime("%a, %d-%b-%Y %T GMT"))
)
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
return "; ".join(output)