Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sessions.py386 linesDownload Raw Back to flask
1from __future__ import annotations2 3import collections.abc as c4import hashlib5import typing as t6from collections.abc import MutableMapping7from datetime import datetime8from datetime import timezone9 10from itsdangerous import BadSignature11from itsdangerous import URLSafeTimedSerializer12from werkzeug.datastructures import CallbackDict13 14from .json.tag import TaggedJSONSerializer15 16if t.TYPE_CHECKING:  # pragma: no cover17    import typing_extensions as te18 19    from .app import Flask20    from .wrappers import Request21    from .wrappers import Response22 23 24class SessionMixin(MutableMapping[str, t.Any]):25    """Expands a basic dictionary with session attributes."""26 27    @property28    def permanent(self) -> bool:29        """This reflects the ``'_permanent'`` key in the dict."""30        return self.get("_permanent", False)  # type: ignore[no-any-return]31 32    @permanent.setter33    def permanent(self, value: bool) -> None:34        self["_permanent"] = bool(value)35 36    #: Some implementations can detect whether a session is newly37    #: created, but that is not guaranteed. Use with caution. The mixin38    # default is hard-coded ``False``.39    new = False40 41    #: Some implementations can detect changes to the session and set42    #: this when that happens. The mixin default is hard coded to43    #: ``True``.44    modified = True45 46    accessed = False47    """Indicates if the session was accessed, even if it was not modified. This48    is set when the session object is accessed through the request context,49    including the global :data:`.session` proxy. A ``Vary: cookie`` header will50    be added if this is ``True``.51 52    .. versionchanged:: 3.1.353        This is tracked by the request context.54    """55 56 57class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):58    """Base class for sessions based on signed cookies.59 60    This session backend will set the :attr:`modified` and61    :attr:`accessed` attributes. It cannot reliably track whether a62    session is new (vs. empty), so :attr:`new` remains hard coded to63    ``False``.64    """65 66    #: When data is changed, this is set to ``True``. Only the session67    #: dictionary itself is tracked; if the session contains mutable68    #: data (for example a nested dict) then this must be set to69    #: ``True`` manually when modifying that data. The session cookie70    #: will only be written to the response if this is ``True``.71    modified = False72 73    def __init__(74        self,75        initial: c.Mapping[str, t.Any] | None = None,76    ) -> None:77        def on_update(self: te.Self) -> None:78            self.modified = True79 80        super().__init__(initial, on_update)81 82 83class NullSession(SecureCookieSession):84    """Class used to generate nicer error messages if sessions are not85    available.  Will still allow read-only access to the empty session86    but fail on setting.87    """88 89    def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:90        raise RuntimeError(91            "The session is unavailable because no secret "92            "key was set.  Set the secret_key on the "93            "application to something unique and secret."94        )95 96    __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail97    del _fail98 99 100class SessionInterface:101    """The basic interface you have to implement in order to replace the102    default session interface which uses werkzeug's securecookie103    implementation.  The only methods you have to implement are104    :meth:`open_session` and :meth:`save_session`, the others have105    useful defaults which you don't need to change.106 107    The session object returned by the :meth:`open_session` method has to108    provide a dictionary like interface plus the properties and methods109    from the :class:`SessionMixin`.  We recommend just subclassing a dict110    and adding that mixin::111 112        class Session(dict, SessionMixin):113            pass114 115    If :meth:`open_session` returns ``None`` Flask will call into116    :meth:`make_null_session` to create a session that acts as replacement117    if the session support cannot work because some requirement is not118    fulfilled.  The default :class:`NullSession` class that is created119    will complain that the secret key was not set.120 121    To replace the session interface on an application all you have to do122    is to assign :attr:`flask.Flask.session_interface`::123 124        app = Flask(__name__)125        app.session_interface = MySessionInterface()126 127    Multiple requests with the same session may be sent and handled128    concurrently. When implementing a new session interface, consider129    whether reads or writes to the backing store must be synchronized.130    There is no guarantee on the order in which the session for each131    request is opened or saved, it will occur in the order that requests132    begin and end processing.133 134    .. versionadded:: 0.8135    """136 137    #: :meth:`make_null_session` will look here for the class that should138    #: be created when a null session is requested.  Likewise the139    #: :meth:`is_null_session` method will perform a typecheck against140    #: this type.141    null_session_class = NullSession142 143    #: A flag that indicates if the session interface is pickle based.144    #: This can be used by Flask extensions to make a decision in regards145    #: to how to deal with the session object.146    #:147    #: .. versionadded:: 0.10148    pickle_based = False149 150    def make_null_session(self, app: Flask) -> NullSession:151        """Creates a null session which acts as a replacement object if the152        real session support could not be loaded due to a configuration153        error.  This mainly aids the user experience because the job of the154        null session is to still support lookup without complaining but155        modifications are answered with a helpful error message of what156        failed.157 158        This creates an instance of :attr:`null_session_class` by default.159        """160        return self.null_session_class()161 162    def is_null_session(self, obj: object) -> bool:163        """Checks if a given object is a null session.  Null sessions are164        not asked to be saved.165 166        This checks if the object is an instance of :attr:`null_session_class`167        by default.168        """169        return isinstance(obj, self.null_session_class)170 171    def get_cookie_name(self, app: Flask) -> str:172        """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""173        return app.config["SESSION_COOKIE_NAME"]  # type: ignore[no-any-return]174 175    def get_cookie_domain(self, app: Flask) -> str | None:176        """The value of the ``Domain`` parameter on the session cookie. If not set,177        browsers will only send the cookie to the exact domain it was set from.178        Otherwise, they will send it to any subdomain of the given value as well.179 180        Uses the :data:`SESSION_COOKIE_DOMAIN` config.181 182        .. versionchanged:: 2.3183            Not set by default, does not fall back to ``SERVER_NAME``.184        """185        return app.config["SESSION_COOKIE_DOMAIN"]  # type: ignore[no-any-return]186 187    def get_cookie_path(self, app: Flask) -> str:188        """Returns the path for which the cookie should be valid.  The189        default implementation uses the value from the ``SESSION_COOKIE_PATH``190        config var if it's set, and falls back to ``APPLICATION_ROOT`` or191        uses ``/`` if it's ``None``.192        """193        return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"]  # type: ignore[no-any-return]194 195    def get_cookie_httponly(self, app: Flask) -> bool:196        """Returns True if the session cookie should be httponly.  This197        currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``198        config var.199        """200        return app.config["SESSION_COOKIE_HTTPONLY"]  # type: ignore[no-any-return]201 202    def get_cookie_secure(self, app: Flask) -> bool:203        """Returns True if the cookie should be secure.  This currently204        just returns the value of the ``SESSION_COOKIE_SECURE`` setting.205        """206        return app.config["SESSION_COOKIE_SECURE"]  # type: ignore[no-any-return]207 208    def get_cookie_samesite(self, app: Flask) -> str | None:209        """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the210        ``SameSite`` attribute. This currently just returns the value of211        the :data:`SESSION_COOKIE_SAMESITE` setting.212        """213        return app.config["SESSION_COOKIE_SAMESITE"]  # type: ignore[no-any-return]214 215    def get_cookie_partitioned(self, app: Flask) -> bool:216        """Returns True if the cookie should be partitioned. By default, uses217        the value of :data:`SESSION_COOKIE_PARTITIONED`.218 219        .. versionadded:: 3.1220        """221        return app.config["SESSION_COOKIE_PARTITIONED"]  # type: ignore[no-any-return]222 223    def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:224        """A helper method that returns an expiration date for the session225        or ``None`` if the session is linked to the browser session.  The226        default implementation returns now + the permanent session227        lifetime configured on the application.228        """229        if session.permanent:230            return datetime.now(timezone.utc) + app.permanent_session_lifetime231        return None232 233    def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:234        """Used by session backends to determine if a ``Set-Cookie`` header235        should be set for this session cookie for this response. If the session236        has been modified, the cookie is set. If the session is permanent and237        the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is238        always set.239 240        This check is usually skipped if the session was deleted.241 242        .. versionadded:: 0.11243        """244 245        return session.modified or (246            session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]247        )248 249    def open_session(self, app: Flask, request: Request) -> SessionMixin | None:250        """This is called at the beginning of each request, after251        pushing the request context, before matching the URL.252 253        This must return an object which implements a dictionary-like254        interface as well as the :class:`SessionMixin` interface.255 256        This will return ``None`` to indicate that loading failed in257        some way that is not immediately an error. The request258        context will fall back to using :meth:`make_null_session`259        in this case.260        """261        raise NotImplementedError()262 263    def save_session(264        self, app: Flask, session: SessionMixin, response: Response265    ) -> None:266        """This is called at the end of each request, after generating267        a response, before removing the request context. It is skipped268        if :meth:`is_null_session` returns ``True``.269        """270        raise NotImplementedError()271 272 273session_json_serializer = TaggedJSONSerializer()274 275 276def _lazy_sha1(string: bytes = b"") -> t.Any:277    """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include278    SHA-1, in which case the import and use as a default would fail before the279    developer can configure something else.280    """281    return hashlib.sha1(string)282 283 284class SecureCookieSessionInterface(SessionInterface):285    """The default session interface that stores sessions in signed cookies286    through the :mod:`itsdangerous` module.287    """288 289    #: the salt that should be applied on top of the secret key for the290    #: signing of cookie based sessions.291    salt = "cookie-session"292    #: the hash function to use for the signature.  The default is sha1293    digest_method = staticmethod(_lazy_sha1)294    #: the name of the itsdangerous supported key derivation.  The default295    #: is hmac.296    key_derivation = "hmac"297    #: A python serializer for the payload.  The default is a compact298    #: JSON derived serializer with support for some extra Python types299    #: such as datetime objects or tuples.300    serializer = session_json_serializer301    session_class = SecureCookieSession302 303    def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:304        if not app.secret_key:305            return None306 307        keys: list[str | bytes] = []308 309        if fallbacks := app.config["SECRET_KEY_FALLBACKS"]:310            keys.extend(fallbacks)311 312        keys.append(app.secret_key)  # itsdangerous expects current key at top313        return URLSafeTimedSerializer(314            keys,  # type: ignore[arg-type]315            salt=self.salt,316            serializer=self.serializer,317            signer_kwargs={318                "key_derivation": self.key_derivation,319                "digest_method": self.digest_method,320            },321        )322 323    def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:324        s = self.get_signing_serializer(app)325        if s is None:326            return None327        val = request.cookies.get(self.get_cookie_name(app))328        if not val:329            return self.session_class()330        max_age = int(app.permanent_session_lifetime.total_seconds())331        try:332            data = s.loads(val, max_age=max_age)333            return self.session_class(data)334        except BadSignature:335            return self.session_class()336 337    def save_session(338        self, app: Flask, session: SessionMixin, response: Response339    ) -> None:340        name = self.get_cookie_name(app)341        domain = self.get_cookie_domain(app)342        path = self.get_cookie_path(app)343        secure = self.get_cookie_secure(app)344        partitioned = self.get_cookie_partitioned(app)345        samesite = self.get_cookie_samesite(app)346        httponly = self.get_cookie_httponly(app)347 348        # Add a "Vary: Cookie" header if the session was accessed at all.349        if session.accessed:350            response.vary.add("Cookie")351 352        # If the session is modified to be empty, remove the cookie.353        # If the session is empty, return without setting the cookie.354        if not session:355            if session.modified:356                response.delete_cookie(357                    name,358                    domain=domain,359                    path=path,360                    secure=secure,361                    partitioned=partitioned,362                    samesite=samesite,363                    httponly=httponly,364                )365                response.vary.add("Cookie")366 367            return368 369        if not self.should_set_cookie(app, session):370            return371 372        expires = self.get_expiration_time(app, session)373        val = self.get_signing_serializer(app).dumps(dict(session))  # type: ignore[union-attr]374        response.set_cookie(375            name,376            val,377            expires=expires,378            httponly=httponly,379            domain=domain,380            path=path,381            secure=secure,382            partitioned=partitioned,383            samesite=samesite,384        )385        response.vary.add("Cookie")386 
codekingpro/portable-devtools · Team Ai