Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| 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 | |
| from .app import Flask | |
| from .wrappers import Request, Response | |
| class SessionMixin(MutableMapping): | |
| """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, 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: t.Any = None) -> None: | |
| def on_update(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"] | |
| 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``. | |
| """ | |
| rv = app.config["SESSION_COOKIE_DOMAIN"] | |
| return rv if rv else None | |
| 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"] | |
| 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"] | |
| 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"] | |
| def get_cookie_samesite(self, app: Flask) -> str: | |
| """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"] | |
| 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() | |
| 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(hashlib.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 | |
| signer_kwargs = dict( | |
| key_derivation=self.key_derivation, digest_method=self.digest_method | |
| ) | |
| return URLSafeTimedSerializer( | |
| app.secret_key, | |
| salt=self.salt, | |
| serializer=self.serializer, | |
| signer_kwargs=signer_kwargs, | |
| ) | |
| 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) | |
| 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, | |
| 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 | |
| response.set_cookie( | |
| name, | |
| val, # type: ignore | |
| expires=expires, | |
| httponly=httponly, | |
| domain=domain, | |
| path=path, | |
| secure=secure, | |
| samesite=samesite, | |
| ) | |
| response.vary.add("Cookie") | |