Spaces:
Running
Running
from __future__ import annotations | |
import collections.abc as c | |
import hashlib | |
import typing as t | |
from collections.abc import MutableMapping | |
from datetime import datetime | |
from datetime import timezone | |
from itsdangerous import BadSignature | |
from itsdangerous import URLSafeTimedSerializer | |
from werkzeug.datastructures import CallbackDict | |
from .json.tag import TaggedJSONSerializer | |
if t.TYPE_CHECKING: # pragma: no cover | |
import typing_extensions as te | |
from .app import Flask | |
from .wrappers import Request | |
from .wrappers import Response | |
class SessionMixin(MutableMapping[str, t.Any]): | |
"""Expands a basic dictionary with session attributes.""" | |
def permanent(self) -> bool: | |
"""This reflects the ``'_permanent'`` key in the dict.""" | |
return self.get("_permanent", False) | |
def permanent(self, value: bool) -> None: | |
self["_permanent"] = bool(value) | |
#: Some implementations can detect whether a session is newly | |
#: created, but that is not guaranteed. Use with caution. The mixin | |
# default is hard-coded ``False``. | |
new = False | |
#: Some implementations can detect changes to the session and set | |
#: this when that happens. The mixin default is hard coded to | |
#: ``True``. | |
modified = True | |
#: Some implementations can detect when session data is read or | |
#: written and set this when that happens. The mixin default is hard | |
#: coded to ``True``. | |
accessed = True | |
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): | |
"""Base class for sessions based on signed cookies. | |
This session backend will set the :attr:`modified` and | |
:attr:`accessed` attributes. It cannot reliably track whether a | |
session is new (vs. empty), so :attr:`new` remains hard coded to | |
``False``. | |
""" | |
#: When data is changed, this is set to ``True``. Only the session | |
#: dictionary itself is tracked; if the session contains mutable | |
#: data (for example a nested dict) then this must be set to | |
#: ``True`` manually when modifying that data. The session cookie | |
#: will only be written to the response if this is ``True``. | |
modified = False | |
#: When data is read or written, this is set to ``True``. Used by | |
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` | |
#: header, which allows caching proxies to cache different pages for | |
#: different users. | |
accessed = False | |
def __init__( | |
self, | |
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, | |
) -> None: | |
def on_update(self: te.Self) -> None: | |
self.modified = True | |
self.accessed = True | |
super().__init__(initial, on_update) | |
def __getitem__(self, key: str) -> t.Any: | |
self.accessed = True | |
return super().__getitem__(key) | |
def get(self, key: str, default: t.Any = None) -> t.Any: | |
self.accessed = True | |
return super().get(key, default) | |
def setdefault(self, key: str, default: t.Any = None) -> t.Any: | |
self.accessed = True | |
return super().setdefault(key, default) | |
class NullSession(SecureCookieSession): | |
"""Class used to generate nicer error messages if sessions are not | |
available. Will still allow read-only access to the empty session | |
but fail on setting. | |
""" | |
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: | |
raise RuntimeError( | |
"The session is unavailable because no secret " | |
"key was set. Set the secret_key on the " | |
"application to something unique and secret." | |
) | |
__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 | |
del _fail | |
class SessionInterface: | |
"""The basic interface you have to implement in order to replace the | |
default session interface which uses werkzeug's securecookie | |
implementation. The only methods you have to implement are | |
:meth:`open_session` and :meth:`save_session`, the others have | |
useful defaults which you don't need to change. | |
The session object returned by the :meth:`open_session` method has to | |
provide a dictionary like interface plus the properties and methods | |
from the :class:`SessionMixin`. We recommend just subclassing a dict | |
and adding that mixin:: | |
class Session(dict, SessionMixin): | |
pass | |
If :meth:`open_session` returns ``None`` Flask will call into | |
:meth:`make_null_session` to create a session that acts as replacement | |
if the session support cannot work because some requirement is not | |
fulfilled. The default :class:`NullSession` class that is created | |
will complain that the secret key was not set. | |
To replace the session interface on an application all you have to do | |
is to assign :attr:`flask.Flask.session_interface`:: | |
app = Flask(__name__) | |
app.session_interface = MySessionInterface() | |
Multiple requests with the same session may be sent and handled | |
concurrently. When implementing a new session interface, consider | |
whether reads or writes to the backing store must be synchronized. | |
There is no guarantee on the order in which the session for each | |
request is opened or saved, it will occur in the order that requests | |
begin and end processing. | |
.. versionadded:: 0.8 | |
""" | |
#: :meth:`make_null_session` will look here for the class that should | |
#: be created when a null session is requested. Likewise the | |
#: :meth:`is_null_session` method will perform a typecheck against | |
#: this type. | |
null_session_class = NullSession | |
#: A flag that indicates if the session interface is pickle based. | |
#: This can be used by Flask extensions to make a decision in regards | |
#: to how to deal with the session object. | |
#: | |
#: .. versionadded:: 0.10 | |
pickle_based = False | |
def make_null_session(self, app: Flask) -> NullSession: | |
"""Creates a null session which acts as a replacement object if the | |
real session support could not be loaded due to a configuration | |
error. This mainly aids the user experience because the job of the | |
null session is to still support lookup without complaining but | |
modifications are answered with a helpful error message of what | |
failed. | |
This creates an instance of :attr:`null_session_class` by default. | |
""" | |
return self.null_session_class() | |
def is_null_session(self, obj: object) -> bool: | |
"""Checks if a given object is a null session. Null sessions are | |
not asked to be saved. | |
This checks if the object is an instance of :attr:`null_session_class` | |
by default. | |
""" | |
return isinstance(obj, self.null_session_class) | |
def get_cookie_name(self, app: Flask) -> str: | |
"""The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" | |
return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] | |
def get_cookie_domain(self, app: Flask) -> str | None: | |
"""The value of the ``Domain`` parameter on the session cookie. If not set, | |
browsers will only send the cookie to the exact domain it was set from. | |
Otherwise, they will send it to any subdomain of the given value as well. | |
Uses the :data:`SESSION_COOKIE_DOMAIN` config. | |
.. versionchanged:: 2.3 | |
Not set by default, does not fall back to ``SERVER_NAME``. | |
""" | |
return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] | |
def get_cookie_path(self, app: Flask) -> str: | |
"""Returns the path for which the cookie should be valid. The | |
default implementation uses the value from the ``SESSION_COOKIE_PATH`` | |
config var if it's set, and falls back to ``APPLICATION_ROOT`` or | |
uses ``/`` if it's ``None``. | |
""" | |
return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] | |
def get_cookie_httponly(self, app: Flask) -> bool: | |
"""Returns True if the session cookie should be httponly. This | |
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` | |
config var. | |
""" | |
return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] | |
def get_cookie_secure(self, app: Flask) -> bool: | |
"""Returns True if the cookie should be secure. This currently | |
just returns the value of the ``SESSION_COOKIE_SECURE`` setting. | |
""" | |
return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] | |
def get_cookie_samesite(self, app: Flask) -> str | None: | |
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the | |
``SameSite`` attribute. This currently just returns the value of | |
the :data:`SESSION_COOKIE_SAMESITE` setting. | |
""" | |
return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] | |
def get_cookie_partitioned(self, app: Flask) -> bool: | |
"""Returns True if the cookie should be partitioned. By default, uses | |
the value of :data:`SESSION_COOKIE_PARTITIONED`. | |
.. versionadded:: 3.1 | |
""" | |
return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] | |
def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: | |
"""A helper method that returns an expiration date for the session | |
or ``None`` if the session is linked to the browser session. The | |
default implementation returns now + the permanent session | |
lifetime configured on the application. | |
""" | |
if session.permanent: | |
return datetime.now(timezone.utc) + app.permanent_session_lifetime | |
return None | |
def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: | |
"""Used by session backends to determine if a ``Set-Cookie`` header | |
should be set for this session cookie for this response. If the session | |
has been modified, the cookie is set. If the session is permanent and | |
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is | |
always set. | |
This check is usually skipped if the session was deleted. | |
.. versionadded:: 0.11 | |
""" | |
return session.modified or ( | |
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] | |
) | |
def open_session(self, app: Flask, request: Request) -> SessionMixin | None: | |
"""This is called at the beginning of each request, after | |
pushing the request context, before matching the URL. | |
This must return an object which implements a dictionary-like | |
interface as well as the :class:`SessionMixin` interface. | |
This will return ``None`` to indicate that loading failed in | |
some way that is not immediately an error. The request | |
context will fall back to using :meth:`make_null_session` | |
in this case. | |
""" | |
raise NotImplementedError() | |
def save_session( | |
self, app: Flask, session: SessionMixin, response: Response | |
) -> None: | |
"""This is called at the end of each request, after generating | |
a response, before removing the request context. It is skipped | |
if :meth:`is_null_session` returns ``True``. | |
""" | |
raise NotImplementedError() | |
session_json_serializer = TaggedJSONSerializer() | |
def _lazy_sha1(string: bytes = b"") -> t.Any: | |
"""Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include | |
SHA-1, in which case the import and use as a default would fail before the | |
developer can configure something else. | |
""" | |
return hashlib.sha1(string) | |
class SecureCookieSessionInterface(SessionInterface): | |
"""The default session interface that stores sessions in signed cookies | |
through the :mod:`itsdangerous` module. | |
""" | |
#: the salt that should be applied on top of the secret key for the | |
#: signing of cookie based sessions. | |
salt = "cookie-session" | |
#: the hash function to use for the signature. The default is sha1 | |
digest_method = staticmethod(_lazy_sha1) | |
#: the name of the itsdangerous supported key derivation. The default | |
#: is hmac. | |
key_derivation = "hmac" | |
#: A python serializer for the payload. The default is a compact | |
#: JSON derived serializer with support for some extra Python types | |
#: such as datetime objects or tuples. | |
serializer = session_json_serializer | |
session_class = SecureCookieSession | |
def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: | |
if not app.secret_key: | |
return None | |
keys: list[str | bytes] = [app.secret_key] | |
if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: | |
keys.extend(fallbacks) | |
return URLSafeTimedSerializer( | |
keys, # type: ignore[arg-type] | |
salt=self.salt, | |
serializer=self.serializer, | |
signer_kwargs={ | |
"key_derivation": self.key_derivation, | |
"digest_method": self.digest_method, | |
}, | |
) | |
def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: | |
s = self.get_signing_serializer(app) | |
if s is None: | |
return None | |
val = request.cookies.get(self.get_cookie_name(app)) | |
if not val: | |
return self.session_class() | |
max_age = int(app.permanent_session_lifetime.total_seconds()) | |
try: | |
data = s.loads(val, max_age=max_age) | |
return self.session_class(data) | |
except BadSignature: | |
return self.session_class() | |
def save_session( | |
self, app: Flask, session: SessionMixin, response: Response | |
) -> None: | |
name = self.get_cookie_name(app) | |
domain = self.get_cookie_domain(app) | |
path = self.get_cookie_path(app) | |
secure = self.get_cookie_secure(app) | |
partitioned = self.get_cookie_partitioned(app) | |
samesite = self.get_cookie_samesite(app) | |
httponly = self.get_cookie_httponly(app) | |
# Add a "Vary: Cookie" header if the session was accessed at all. | |
if session.accessed: | |
response.vary.add("Cookie") | |
# If the session is modified to be empty, remove the cookie. | |
# If the session is empty, return without setting the cookie. | |
if not session: | |
if session.modified: | |
response.delete_cookie( | |
name, | |
domain=domain, | |
path=path, | |
secure=secure, | |
partitioned=partitioned, | |
samesite=samesite, | |
httponly=httponly, | |
) | |
response.vary.add("Cookie") | |
return | |
if not self.should_set_cookie(app, session): | |
return | |
expires = self.get_expiration_time(app, session) | |
val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] | |
response.set_cookie( | |
name, | |
val, | |
expires=expires, | |
httponly=httponly, | |
domain=domain, | |
path=path, | |
secure=secure, | |
partitioned=partitioned, | |
samesite=samesite, | |
) | |
response.vary.add("Cookie") | |