chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .response import Cookie, CookieJar
|
||||
|
||||
|
||||
__all__ = ("Cookie", "CookieJar")
|
||||
@@ -0,0 +1,159 @@
|
||||
import re
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sanic.cookies.response import Cookie
|
||||
from sanic.request.parameters import RequestParameters
|
||||
|
||||
|
||||
COOKIE_NAME_RESERVED_CHARS = re.compile(
|
||||
'[\x00-\x1f\x7f-\xff()<>@,;:\\\\"/[\\]?={} \x09]'
|
||||
)
|
||||
OCTAL_PATTERN = re.compile(r"\\[0-3][0-7][0-7]")
|
||||
QUOTE_PATTERN = re.compile(r"[\\].")
|
||||
|
||||
|
||||
def _unquote(str): # no cov
|
||||
if str is None or len(str) < 2:
|
||||
return str
|
||||
if str[0] != '"' or str[-1] != '"':
|
||||
return str
|
||||
|
||||
str = str[1:-1]
|
||||
|
||||
i = 0
|
||||
n = len(str)
|
||||
res = []
|
||||
while 0 <= i < n:
|
||||
o_match = OCTAL_PATTERN.search(str, i)
|
||||
q_match = QUOTE_PATTERN.search(str, i)
|
||||
if not o_match and not q_match:
|
||||
res.append(str[i:])
|
||||
break
|
||||
# else:
|
||||
j = k = -1
|
||||
if o_match:
|
||||
j = o_match.start(0)
|
||||
if q_match:
|
||||
k = q_match.start(0)
|
||||
if q_match and (not o_match or k < j):
|
||||
res.append(str[i:k])
|
||||
res.append(str[k + 1])
|
||||
i = k + 2
|
||||
else:
|
||||
res.append(str[i:j])
|
||||
res.append(chr(int(str[j + 1 : j + 4], 8))) # noqa: E203
|
||||
i = j + 4
|
||||
return "".join(res)
|
||||
|
||||
|
||||
def parse_cookie(raw: str) -> dict[str, list[str]]:
|
||||
"""Parses a raw cookie string into a dictionary.
|
||||
|
||||
The function takes a raw cookie string (usually from HTTP headers) and
|
||||
returns a dictionary where each key is a cookie name and the value is a
|
||||
list of values for that cookie. The function handles quoted values and
|
||||
skips invalid cookie names.
|
||||
|
||||
Args:
|
||||
raw (str): The raw cookie string to be parsed.
|
||||
|
||||
Returns:
|
||||
Dict[str, List[str]]: A dictionary containing the cookie names as keys
|
||||
and a list of values for each cookie.
|
||||
|
||||
Example:
|
||||
```python
|
||||
raw = 'name1=value1; name2="value2"; name3=value3'
|
||||
cookies = parse_cookie(raw)
|
||||
# cookies will be {'name1': ['value1'], 'name2': ['value2'], 'name3': ['value3']}
|
||||
```
|
||||
""" # noqa: E501
|
||||
cookies: dict[str, list[str]] = {}
|
||||
|
||||
for token in raw.split(";"):
|
||||
name, sep, value = token.partition("=")
|
||||
name = name.strip()
|
||||
value = value.strip()
|
||||
|
||||
# Support cookies =value or plain value with no name
|
||||
# https://github.com/httpwg/http-extensions/issues/159
|
||||
if not sep:
|
||||
if not name:
|
||||
# Empty value like ;; or a cookie header with no value
|
||||
continue
|
||||
name, value = "", name
|
||||
|
||||
if COOKIE_NAME_RESERVED_CHARS.search(name): # no cov
|
||||
continue
|
||||
|
||||
if len(value) > 2 and value[0] == '"' and value[-1] == '"': # no cov
|
||||
value = _unquote(value)
|
||||
|
||||
if name in cookies:
|
||||
cookies[name].append(value)
|
||||
else:
|
||||
cookies[name] = [value]
|
||||
|
||||
return cookies
|
||||
|
||||
|
||||
class CookieRequestParameters(RequestParameters):
|
||||
"""A container for accessing single and multiple cookie values.
|
||||
|
||||
Because the HTTP standard allows for multiple cookies with the same name,
|
||||
a standard dictionary cannot be used to access cookie values. This class
|
||||
provides a way to access cookie values in a way that is similar to a
|
||||
dictionary, but also allows for accessing multiple values for a single
|
||||
cookie name when necessary.
|
||||
|
||||
Args:
|
||||
cookies (Dict[str, List[str]]): A dictionary containing the cookie
|
||||
names as keys and a list of values for each cookie.
|
||||
|
||||
Example:
|
||||
```python
|
||||
raw = 'name1=value1; name2="value2"; name3=value3'
|
||||
cookies = parse_cookie(raw)
|
||||
# cookies will be {'name1': ['value1'], 'name2': ['value2'], 'name3': ['value3']}
|
||||
|
||||
request_cookies = CookieRequestParameters(cookies)
|
||||
request_cookies['name1'] # 'value1'
|
||||
request_cookies.get('name1') # 'value1'
|
||||
request_cookies.getlist('name1') # ['value1']
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
def __getitem__(self, key: str) -> str | None:
|
||||
try:
|
||||
value = self._get_prefixed_cookie(key)
|
||||
except KeyError:
|
||||
value = super().__getitem__(key)
|
||||
return value
|
||||
|
||||
def __getattr__(self, key: str) -> str:
|
||||
if key.startswith("_"):
|
||||
return self.__getattribute__(key)
|
||||
key = key.rstrip("_").replace("_", "-")
|
||||
return str(self.get(key, ""))
|
||||
|
||||
def get(self, name: str, default: Any | None = None) -> Any | None:
|
||||
try:
|
||||
return self._get_prefixed_cookie(name)[0]
|
||||
except KeyError:
|
||||
return super().get(name, default)
|
||||
|
||||
def getlist(
|
||||
self, name: str, default: list[Any] | None = None
|
||||
) -> list[Any]:
|
||||
try:
|
||||
return self._get_prefixed_cookie(name)
|
||||
except KeyError:
|
||||
return super().getlist(name, default)
|
||||
|
||||
def _get_prefixed_cookie(self, name: str) -> Any:
|
||||
getitem = super().__getitem__
|
||||
try:
|
||||
return getitem(f"{Cookie.HOST_PREFIX}{name}")
|
||||
except KeyError:
|
||||
return getitem(f"{Cookie.SECURE_PREFIX}{name}")
|
||||
@@ -0,0 +1,607 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import string
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from sanic.exceptions import ServerError
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sanic.compat import Header
|
||||
|
||||
|
||||
SameSite = (
|
||||
Literal["Strict"]
|
||||
| Literal["Lax"]
|
||||
| Literal["None"]
|
||||
| Literal["strict"]
|
||||
| Literal["lax"]
|
||||
| Literal["none"]
|
||||
)
|
||||
|
||||
DEFAULT_MAX_AGE = 0
|
||||
SAMESITE_VALUES = ("strict", "lax", "none")
|
||||
|
||||
LEGAL_CHARS = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
|
||||
UNESCAPED_CHARS = LEGAL_CHARS + " ()/<=>?@[]{}"
|
||||
TRANSLATOR = {ch: f"\\{ch:03o}" for ch in bytes(range(32)) + b'";\\\x7f'}
|
||||
|
||||
|
||||
def _quote(str): # no cov
|
||||
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 f'"{str.translate(TRANSLATOR)}"'
|
||||
|
||||
|
||||
_is_legal_key = re.compile("[%s]+" % re.escape(LEGAL_CHARS)).fullmatch
|
||||
|
||||
|
||||
class CookieJar:
|
||||
"""A container to manipulate cookies.
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
headers (Header): The headers object to write cookies to.
|
||||
"""
|
||||
|
||||
HEADER_KEY = "Set-Cookie"
|
||||
|
||||
def __init__(self, headers: Header):
|
||||
self.headers = headers
|
||||
|
||||
def __len__(self): # no cov
|
||||
return len(self.cookies)
|
||||
|
||||
@property
|
||||
def cookies(self) -> list[Cookie]:
|
||||
"""A list of cookies in the CookieJar.
|
||||
|
||||
Returns:
|
||||
List[Cookie]: A list of cookies in the CookieJar.
|
||||
"""
|
||||
return self.headers.getall(self.HEADER_KEY, [])
|
||||
|
||||
def get_cookie(
|
||||
self,
|
||||
key: str,
|
||||
path: str = "/",
|
||||
domain: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Cookie | None:
|
||||
"""Fetch a cookie from the CookieJar.
|
||||
|
||||
Args:
|
||||
key (str): The key of the cookie to fetch.
|
||||
path (str, optional): The path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): The domain of the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
Optional[Cookie]: The cookie if it exists, otherwise `None`.
|
||||
""" # noqa: E501
|
||||
for cookie in self.cookies:
|
||||
if (
|
||||
cookie.key == Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
and cookie.path == path
|
||||
and cookie.domain == domain
|
||||
):
|
||||
return cookie
|
||||
return None
|
||||
|
||||
def has_cookie(
|
||||
self,
|
||||
key: str,
|
||||
path: str = "/",
|
||||
domain: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> bool:
|
||||
"""Check if a cookie exists in the CookieJar.
|
||||
|
||||
Args:
|
||||
key (str): The key of the cookie to check.
|
||||
path (str, optional): The path of the cookie. Defaults to `"/"`.
|
||||
domain (Optional[str], optional): The domain of the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
bool: Whether the cookie exists.
|
||||
""" # noqa: E501
|
||||
for cookie in self.cookies:
|
||||
if (
|
||||
cookie.key == Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
and cookie.path == path
|
||||
and cookie.domain == domain
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_cookie(
|
||||
self,
|
||||
key: str,
|
||||
value: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: str | None = None,
|
||||
secure: bool = True,
|
||||
max_age: int | None = None,
|
||||
expires: datetime | None = None,
|
||||
httponly: bool = False,
|
||||
samesite: SameSite | None = "Lax",
|
||||
partitioned: bool = False,
|
||||
comment: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> Cookie:
|
||||
"""Add a cookie to the CookieJar.
|
||||
|
||||
Args:
|
||||
key (str): Key of the cookie.
|
||||
value (str): Value of the cookie.
|
||||
path (str, optional): Path of the cookie. Defaults to "/".
|
||||
domain (Optional[str], optional): Domain of the cookie. Defaults to None.
|
||||
secure (bool, optional): Whether to set it as a secure cookie. Defaults to True.
|
||||
max_age (Optional[int], optional): Max age of the cookie in seconds; if set to 0 a
|
||||
browser should delete it. Defaults to None.
|
||||
expires (Optional[datetime], optional): When the cookie expires; if set to None browsers
|
||||
should set it as a session cookie. Defaults to None.
|
||||
httponly (bool, optional): Whether to set it as HTTP only. Defaults to False.
|
||||
samesite (Optional[SameSite], optional): How to set the samesite property, should be
|
||||
strict, lax, or none (case insensitive). Defaults to "Lax".
|
||||
partitioned (bool, optional): Whether to set it as partitioned. Defaults to False.
|
||||
comment (Optional[str], optional): A cookie comment. Defaults to None.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True. Defaults to False.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Cookie: The instance of the created cookie.
|
||||
|
||||
Raises:
|
||||
ServerError: If host_prefix is set without secure=True.
|
||||
ServerError: If host_prefix is set without path="/" and domain=None.
|
||||
ServerError: If host_prefix is set with domain.
|
||||
ServerError: If secure_prefix is set without secure=True.
|
||||
ServerError: If partitioned is set without host_prefix=True.
|
||||
|
||||
Examples:
|
||||
Basic usage
|
||||
```python
|
||||
cookie = add_cookie('name', 'value')
|
||||
```
|
||||
|
||||
Adding a cookie with a custom path and domain
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', path='/custom', domain='example.com')
|
||||
```
|
||||
|
||||
Adding a secure, HTTP-only cookie with a comment
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', secure=True, httponly=True, comment='My Cookie')
|
||||
```
|
||||
|
||||
Adding a cookie with a max age of 60 seconds
|
||||
```python
|
||||
cookie = add_cookie('name', 'value', max_age=60)
|
||||
```
|
||||
""" # noqa: E501
|
||||
cookie = Cookie(
|
||||
key,
|
||||
value,
|
||||
path=path,
|
||||
expires=expires,
|
||||
comment=comment,
|
||||
domain=domain,
|
||||
max_age=max_age,
|
||||
secure=secure,
|
||||
httponly=httponly,
|
||||
samesite=samesite,
|
||||
partitioned=partitioned,
|
||||
host_prefix=host_prefix,
|
||||
secure_prefix=secure_prefix,
|
||||
)
|
||||
self.headers.add(self.HEADER_KEY, cookie)
|
||||
|
||||
return cookie
|
||||
|
||||
def delete_cookie(
|
||||
self,
|
||||
key: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: str | None = None,
|
||||
secure: bool = True,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a cookie
|
||||
|
||||
This will effectively set it as Max-Age: 0, which a browser should
|
||||
interpret it to mean: "delete the cookie".
|
||||
|
||||
Since it is a browser/client implementation, your results may vary
|
||||
depending upon which client is being used.
|
||||
|
||||
:param key: The key to be deleted
|
||||
:type key: str
|
||||
:param path: Path of the cookie, defaults to None
|
||||
:type path: Optional[str], optional
|
||||
:param domain: Domain of the cookie, defaults to None
|
||||
:type domain: Optional[str], optional
|
||||
:param secure: Whether to delete a secure cookie. Defaults to True.
|
||||
:param secure: bool
|
||||
:param host_prefix: Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True,
|
||||
defaults to False
|
||||
:type host_prefix: bool
|
||||
:param secure_prefix: Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True, defaults to False
|
||||
:type secure_prefix: bool
|
||||
"""
|
||||
if host_prefix and not (secure and path == "/" and domain is None):
|
||||
raise ServerError(
|
||||
"Cannot set host_prefix on a cookie without "
|
||||
"path='/', domain=None, and secure=True"
|
||||
)
|
||||
if secure_prefix and not secure:
|
||||
raise ServerError(
|
||||
"Cannot set secure_prefix on a cookie without secure=True"
|
||||
)
|
||||
|
||||
cookies: list[Cookie] = self.headers.popall(self.HEADER_KEY, [])
|
||||
existing_cookie = None
|
||||
for cookie in cookies:
|
||||
if (
|
||||
cookie.key != Cookie.make_key(key, host_prefix, secure_prefix)
|
||||
or cookie.path != path
|
||||
or cookie.domain != domain
|
||||
):
|
||||
self.headers.add(self.HEADER_KEY, cookie)
|
||||
elif existing_cookie is None:
|
||||
existing_cookie = cookie
|
||||
|
||||
if existing_cookie is not None:
|
||||
# Use all the same values as the cookie to be deleted
|
||||
# except value="" and max_age=0
|
||||
self.add_cookie(
|
||||
key=key,
|
||||
value="",
|
||||
path=existing_cookie.path,
|
||||
domain=existing_cookie.domain,
|
||||
secure=existing_cookie.secure,
|
||||
max_age=0,
|
||||
httponly=existing_cookie.httponly,
|
||||
partitioned=existing_cookie.partitioned,
|
||||
samesite=existing_cookie.samesite,
|
||||
host_prefix=host_prefix,
|
||||
secure_prefix=secure_prefix,
|
||||
)
|
||||
else:
|
||||
self.add_cookie(
|
||||
key=key,
|
||||
value="",
|
||||
path=path,
|
||||
domain=domain,
|
||||
secure=secure,
|
||||
max_age=0,
|
||||
samesite=None,
|
||||
host_prefix=host_prefix,
|
||||
secure_prefix=secure_prefix,
|
||||
)
|
||||
|
||||
|
||||
class Cookie:
|
||||
"""A representation of a HTTP cookie, providing an interface to manipulate cookie attributes intended for a response.
|
||||
|
||||
This class is a simplified representation of a cookie, similar to the Morsel SimpleCookie in Python's standard library.
|
||||
It allows the manipulation of various cookie attributes including path, domain, security settings, and others.
|
||||
|
||||
Several "smart defaults" are provided to make it easier to create cookies that are secure by default. These include:
|
||||
|
||||
- Setting the `secure` flag to `True` by default
|
||||
- Setting the `samesite` flag to `Lax` by default
|
||||
|
||||
Args:
|
||||
key (str): The key (name) of the cookie.
|
||||
value (str): The value of the cookie.
|
||||
path (str, optional): The path for the cookie. Defaults to "/".
|
||||
domain (Optional[str], optional): The domain for the cookie.
|
||||
Defaults to `None`.
|
||||
secure (bool, optional): Whether the cookie is secure.
|
||||
Defaults to `True`.
|
||||
max_age (Optional[int], optional): The maximum age of the cookie
|
||||
in seconds. Defaults to `None`.
|
||||
expires (Optional[datetime], optional): The expiration date of the
|
||||
cookie. Defaults to `None`.
|
||||
httponly (bool, optional): HttpOnly flag for the cookie.
|
||||
Defaults to `False`.
|
||||
samesite (Optional[SameSite], optional): The SameSite attribute for
|
||||
the cookie. Defaults to `"Lax"`.
|
||||
partitioned (bool, optional): Whether the cookie is partitioned.
|
||||
Defaults to `False`.
|
||||
comment (Optional[str], optional): A comment for the cookie.
|
||||
Defaults to `None`.
|
||||
host_prefix (bool, optional): Whether to use the host prefix.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to use the secure prefix.
|
||||
Defaults to `False`.
|
||||
""" # noqa: E501
|
||||
|
||||
HOST_PREFIX = "__Host-"
|
||||
SECURE_PREFIX = "__Secure-"
|
||||
|
||||
__slots__ = (
|
||||
"key",
|
||||
"value",
|
||||
"_path",
|
||||
"_comment",
|
||||
"_domain",
|
||||
"_secure",
|
||||
"_httponly",
|
||||
"_partitioned",
|
||||
"_expires",
|
||||
"_max_age",
|
||||
"_samesite",
|
||||
)
|
||||
|
||||
_keys = {
|
||||
"path": "Path",
|
||||
"comment": "Comment",
|
||||
"domain": "Domain",
|
||||
"max-age": "Max-Age",
|
||||
"expires": "expires",
|
||||
"samesite": "SameSite",
|
||||
# "version": "Version",
|
||||
"secure": "Secure",
|
||||
"httponly": "HttpOnly",
|
||||
"partitioned": "Partitioned",
|
||||
}
|
||||
_flags = {"secure", "httponly", "partitioned"}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: str,
|
||||
value: str,
|
||||
*,
|
||||
path: str = "/",
|
||||
domain: str | None = None,
|
||||
secure: bool = True,
|
||||
max_age: int | None = None,
|
||||
expires: datetime | None = None,
|
||||
httponly: bool = False,
|
||||
samesite: SameSite | None = "Lax",
|
||||
partitioned: bool = False,
|
||||
comment: str | None = None,
|
||||
host_prefix: bool = False,
|
||||
secure_prefix: bool = False,
|
||||
):
|
||||
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")
|
||||
if host_prefix:
|
||||
if not secure:
|
||||
raise ServerError(
|
||||
"Cannot set host_prefix on a cookie without secure=True"
|
||||
)
|
||||
if path != "/":
|
||||
raise ServerError(
|
||||
"Cannot set host_prefix on a cookie unless path='/'"
|
||||
)
|
||||
if domain:
|
||||
raise ServerError(
|
||||
"Cannot set host_prefix on a cookie with a defined domain"
|
||||
)
|
||||
elif secure_prefix and not secure:
|
||||
raise ServerError(
|
||||
"Cannot set secure_prefix on a cookie without secure=True"
|
||||
)
|
||||
if partitioned and not host_prefix:
|
||||
# This is technically possible, but it is not advisable so we will
|
||||
# take a stand and say "don't shoot yourself in the foot"
|
||||
raise ServerError(
|
||||
"Cannot create a partitioned cookie without "
|
||||
"also setting host_prefix=True"
|
||||
)
|
||||
|
||||
self.key = self.make_key(key, host_prefix, secure_prefix)
|
||||
self.value = value
|
||||
|
||||
self._path = path
|
||||
self._comment = comment
|
||||
self._domain = domain
|
||||
self._secure = secure
|
||||
self._httponly = httponly
|
||||
self._partitioned = partitioned
|
||||
self._expires: datetime | None = None
|
||||
self._max_age: int | None = None
|
||||
self._samesite: SameSite | None = None
|
||||
|
||||
if expires is not None:
|
||||
self.expires = expires
|
||||
if max_age is not None:
|
||||
self.max_age = max_age
|
||||
if samesite is not None:
|
||||
self.samesite = samesite
|
||||
|
||||
def __str__(self):
|
||||
"""Format as a Set-Cookie header value."""
|
||||
output = ["{}={}".format(self.key, _quote(self.value))]
|
||||
ordered_keys = list(self._keys.keys())
|
||||
for key in sorted(
|
||||
self._keys.keys(), key=lambda k: ordered_keys.index(k)
|
||||
):
|
||||
value = getattr(self, key.replace("-", "_"))
|
||||
if value is not None and value is not False:
|
||||
if key == "max-age":
|
||||
try:
|
||||
output.append("%s=%d" % (self._keys[key], value))
|
||||
except TypeError:
|
||||
output.append("{}={}".format(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:
|
||||
output.append(self._keys[key])
|
||||
else:
|
||||
output.append("{}={}".format(self._keys[key], value))
|
||||
|
||||
return "; ".join(output)
|
||||
|
||||
@property
|
||||
def path(self) -> str: # no cov
|
||||
"""The path of the cookie. Defaults to `"/"`."""
|
||||
return self._path
|
||||
|
||||
@path.setter
|
||||
def path(self, value: str) -> None: # no cov
|
||||
self._path = value
|
||||
|
||||
@property
|
||||
def expires(self) -> datetime | None: # no cov
|
||||
"""The expiration date of the cookie. Defaults to `None`."""
|
||||
return self._expires
|
||||
|
||||
@expires.setter
|
||||
def expires(self, value: datetime) -> None: # no cov
|
||||
if not isinstance(value, datetime):
|
||||
raise TypeError("Cookie 'expires' property must be a datetime")
|
||||
self._expires = value
|
||||
|
||||
@property
|
||||
def comment(self) -> str | None: # no cov
|
||||
"""A comment for the cookie. Defaults to `None`."""
|
||||
return self._comment
|
||||
|
||||
@comment.setter
|
||||
def comment(self, value: str) -> None: # no cov
|
||||
self._comment = value
|
||||
|
||||
@property
|
||||
def domain(self) -> str | None: # no cov
|
||||
"""The domain of the cookie. Defaults to `None`."""
|
||||
return self._domain
|
||||
|
||||
@domain.setter
|
||||
def domain(self, value: str) -> None: # no cov
|
||||
self._domain = value
|
||||
|
||||
@property
|
||||
def max_age(self) -> int | None: # no cov
|
||||
"""The maximum age of the cookie in seconds. Defaults to `None`."""
|
||||
return self._max_age
|
||||
|
||||
@max_age.setter
|
||||
def max_age(self, value: int) -> None: # no cov
|
||||
if not str(value).isdigit():
|
||||
raise ValueError("Cookie max-age must be an integer")
|
||||
self._max_age = value
|
||||
|
||||
@property
|
||||
def secure(self) -> bool: # no cov
|
||||
"""Whether the cookie is secure. Defaults to `True`."""
|
||||
return self._secure
|
||||
|
||||
@secure.setter
|
||||
def secure(self, value: bool) -> None: # no cov
|
||||
self._secure = value
|
||||
|
||||
@property
|
||||
def httponly(self) -> bool: # no cov
|
||||
"""Whether the cookie is HTTP only. Defaults to `False`."""
|
||||
return self._httponly
|
||||
|
||||
@httponly.setter
|
||||
def httponly(self, value: bool) -> None: # no cov
|
||||
self._httponly = value
|
||||
|
||||
@property
|
||||
def samesite(self) -> SameSite | None: # no cov
|
||||
"""The SameSite attribute for the cookie. Defaults to `"Lax"`."""
|
||||
return self._samesite
|
||||
|
||||
@samesite.setter
|
||||
def samesite(self, value: SameSite) -> None: # no cov
|
||||
if value.lower() not in SAMESITE_VALUES:
|
||||
raise TypeError(
|
||||
"Cookie 'samesite' property must "
|
||||
f"be one of: {','.join(SAMESITE_VALUES)}"
|
||||
)
|
||||
self._samesite = cast(SameSite, value.title())
|
||||
|
||||
@property
|
||||
def partitioned(self) -> bool: # no cov
|
||||
"""Whether the cookie is partitioned. Defaults to `False`."""
|
||||
return self._partitioned
|
||||
|
||||
@partitioned.setter
|
||||
def partitioned(self, value: bool) -> None: # no cov
|
||||
self._partitioned = value
|
||||
|
||||
@classmethod
|
||||
def make_key(
|
||||
cls, key: str, host_prefix: bool = False, secure_prefix: bool = False
|
||||
) -> str:
|
||||
"""Create a cookie key with the appropriate prefix.
|
||||
|
||||
Cookies can have one ow two prefixes. The first is `__Host-` which
|
||||
requires that the cookie be set with `path="/", domain=None, and
|
||||
secure=True`. The second is `__Secure-` which requires that
|
||||
`secure=True`.
|
||||
|
||||
They cannot be combined.
|
||||
|
||||
Args:
|
||||
key (str): The key (name) of the cookie.
|
||||
host_prefix (bool, optional): Whether to add __Host- as a prefix to the key.
|
||||
This requires that path="/", domain=None, and secure=True.
|
||||
Defaults to `False`.
|
||||
secure_prefix (bool, optional): Whether to add __Secure- as a prefix to the key.
|
||||
This requires that secure=True. Defaults to `False`.
|
||||
|
||||
Raises:
|
||||
ServerError: If both host_prefix and secure_prefix are set.
|
||||
|
||||
Returns:
|
||||
str: The key with the appropriate prefix.
|
||||
""" # noqa: E501
|
||||
if host_prefix and secure_prefix:
|
||||
raise ServerError(
|
||||
"Both host_prefix and secure_prefix were requested. "
|
||||
"A cookie should have only one prefix."
|
||||
)
|
||||
elif host_prefix:
|
||||
key = cls.HOST_PREFIX + key
|
||||
elif secure_prefix:
|
||||
key = cls.SECURE_PREFIX + key
|
||||
return key
|
||||
Reference in New Issue
Block a user