Merge pull request #110 from channelcat/cookies-lazy-creation
Lazy cookie creation
This commit is contained in:
commit
977081f4af
129
sanic/cookies.py
Normal file
129
sanic/cookies.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# SimpleCookie
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
# Straight up copied this section of dark magic from SimpleCookie
|
||||||
|
|
||||||
|
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||||
|
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
|
||||||
|
|
||||||
|
_Translator = {n: '\\%03o' % n
|
||||||
|
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
|
||||||
|
_Translator.update({
|
||||||
|
ord('"'): '\\"',
|
||||||
|
ord('\\'): '\\\\',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _quote(str):
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
return '"' + str.translate(_Translator) + '"'
|
||||||
|
|
||||||
|
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# Custom SimpleCookie
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
class CookieJar(dict):
|
||||||
|
"""
|
||||||
|
CookieJar dynamically writes headers as cookies are added and removed
|
||||||
|
It gets around the limitation of one header per name by using the
|
||||||
|
MultiHeader class to provide a unique key that encodes to Set-Cookie
|
||||||
|
"""
|
||||||
|
def __init__(self, headers):
|
||||||
|
super().__init__()
|
||||||
|
self.headers = headers
|
||||||
|
self.cookie_headers = {}
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# If this cookie doesn't exist, add it to the header keys
|
||||||
|
cookie_header = self.cookie_headers.get(key)
|
||||||
|
if not cookie_header:
|
||||||
|
cookie = Cookie(key, value)
|
||||||
|
cookie_header = MultiHeader("Set-Cookie")
|
||||||
|
self.cookie_headers[key] = cookie_header
|
||||||
|
self.headers[cookie_header] = cookie
|
||||||
|
return super().__setitem__(key, cookie)
|
||||||
|
else:
|
||||||
|
self[key].value = value
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.cookie_headers[key]
|
||||||
|
return super().__delitem__(key)
|
||||||
|
|
||||||
|
|
||||||
|
class Cookie(dict):
|
||||||
|
"""
|
||||||
|
This is a stripped down version of Morsel from SimpleCookie #gottagofast
|
||||||
|
"""
|
||||||
|
_keys = {
|
||||||
|
"expires": "expires",
|
||||||
|
"path": "Path",
|
||||||
|
"comment": "Comment",
|
||||||
|
"domain": "Domain",
|
||||||
|
"max-age": "Max-Age",
|
||||||
|
"secure": "Secure",
|
||||||
|
"httponly": "HttpOnly",
|
||||||
|
"version": "Version",
|
||||||
|
}
|
||||||
|
_flags = {'secure', 'httponly'}
|
||||||
|
|
||||||
|
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):
|
||||||
|
if key not in self._keys:
|
||||||
|
raise KeyError("Unknown cookie property")
|
||||||
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
|
def encode(self, encoding):
|
||||||
|
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||||
|
for key, value in self.items():
|
||||||
|
if key == 'max-age' and isinstance(value, int):
|
||||||
|
output.append('%s=%d' % (self._keys[key], value))
|
||||||
|
elif key == 'expires' and isinstance(value, datetime):
|
||||||
|
output.append('%s=%s' % (
|
||||||
|
self._keys[key],
|
||||||
|
value.strftime("%a, %d-%b-%Y %T GMT")
|
||||||
|
))
|
||||||
|
elif key in self._flags:
|
||||||
|
output.append(self._keys[key])
|
||||||
|
else:
|
||||||
|
output.append('%s=%s' % (self._keys[key], value))
|
||||||
|
|
||||||
|
return "; ".join(output).encode(encoding)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# Header Trickery
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
class MultiHeader:
|
||||||
|
"""
|
||||||
|
Allows us to set a header within response that has a unique key,
|
||||||
|
but may contain duplicate header names
|
||||||
|
"""
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def encode(self):
|
||||||
|
return self.name.encode()
|
|
@ -1,6 +1,5 @@
|
||||||
from aiofiles import open as open_async
|
from aiofiles import open as open_async
|
||||||
from datetime import datetime
|
from .cookies import CookieJar
|
||||||
from http.cookies import SimpleCookie
|
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
|
@ -101,12 +100,6 @@ class HTTPResponse:
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
||||||
for name, value in self.headers.items()
|
for name, value in self.headers.items()
|
||||||
)
|
)
|
||||||
if self._cookies:
|
|
||||||
for cookie in self._cookies.values():
|
|
||||||
if type(cookie['expires']) is datetime:
|
|
||||||
cookie['expires'] = \
|
|
||||||
cookie['expires'].strftime("%a, %d-%b-%Y %T GMT")
|
|
||||||
headers += (str(self._cookies) + "\r\n").encode('utf-8')
|
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
|
@ -134,7 +127,7 @@ class HTTPResponse:
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
self._cookies = SimpleCookie()
|
self._cookies = CookieJar(self.headers)
|
||||||
return self._cookies
|
return self._cookies
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user