v0.152.0
 1from __future__ import annotations
 2
 3from http import cookies
 4
 5from plain.runtime import settings
 6from plain.signing import BadSignature, TimestampSigner
 7from plain.utils.encoding import force_bytes
 8
 9
10def parse_cookie(cookie: str) -> dict[str, str]:
11    """
12    Return a dictionary parsed from a `Cookie:` header string.
13    """
14    cookiedict = {}
15    for chunk in cookie.split(";"):
16        if "=" in chunk:
17            key, val = chunk.split("=", 1)
18        else:
19            # Assume an empty name per
20            # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
21            key, val = "", chunk
22        key, val = key.strip(), val.strip()
23        if key or val:
24            # unquote using Python's algorithm.
25            cookiedict[key] = cookies._unquote(val)
26    return cookiedict
27
28
29def _cookie_key(key: str) -> bytes:
30    """
31    Generate a key for cookie signing that matches the pattern used by
32    set_signed_cookie and get_signed_cookie.
33    """
34    return b"plain.http.cookies" + force_bytes(key)
35
36
37def get_signed_cookie_signer(key: str, salt: str = "") -> TimestampSigner:
38    """
39    Create a TimestampSigner for signed cookies with the same configuration
40    used by both set_signed_cookie and get_signed_cookie.
41    """
42    return TimestampSigner(
43        key=_cookie_key(settings.SECRET_KEY).decode(),
44        fallback_keys=[_cookie_key(k).decode() for k in settings.SECRET_KEY_FALLBACKS],
45        salt=key + salt,
46    )
47
48
49def sign_cookie_value(key: str, value: str, salt: str = "") -> str:
50    """
51    Sign a cookie value using the standard Plain cookie signing approach.
52    """
53    signer = get_signed_cookie_signer(key, salt)
54    return signer.sign(value)
55
56
57def unsign_cookie_value(
58    key: str,
59    signed_value: str,
60    salt: str = "",
61    max_age: int | None = None,
62    default: str | None = None,
63) -> str | None:
64    """
65    Unsign a cookie value using the standard Plain cookie signing approach.
66    Returns the default value if the signature is invalid or the cookie has expired.
67    """
68    signer = get_signed_cookie_signer(key, salt)
69    try:
70        return signer.unsign(signed_value, max_age=max_age)
71    except BadSignature:
72        return default