hack-house/.venv/lib/python3.12/site-packages/sanic/cookies/response.py
leetcrypt bb1d662ee1 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>
2026-05-30 13:29:14 -07:00

608 lines
21 KiB
Python

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