Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
sessions.py380 linesDownload Raw Back to flask
1from __future__ import annotations2 3import hashlib4import typing as t5from collections.abc import MutableMapping6from datetime import datetime7from datetime import timezone8 9from itsdangerous import BadSignature10from itsdangerous import URLSafeTimedSerializer11from werkzeug.datastructures import CallbackDict12 13from .json.tag import TaggedJSONSerializer14 15if t.TYPE_CHECKING:  # pragma: no cover16    import typing_extensions as te17 18    from .app import Flask19    from .wrappers import Request20    from .wrappers import Response21 22 23# TODO generic when Python > 3.824class SessionMixin(MutableMapping):  # type: ignore[type-arg]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)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    #: Some implementations can detect when session data is read or47    #: written and set this when that happens. The mixin default is hard48    #: coded to ``True``.49    accessed = True50 51 52# TODO generic when Python > 3.853class SecureCookieSession(CallbackDict, SessionMixin):  # type: ignore[type-arg]54    """Base class for sessions based on signed cookies.55 56    This session backend will set the :attr:`modified` and57    :attr:`accessed` attributes. It cannot reliably track whether a58    session is new (vs. empty), so :attr:`new` remains hard coded to59    ``False``.60    """61 62    #: When data is changed, this is set to ``True``. Only the session63    #: dictionary itself is tracked; if the session contains mutable64    #: data (for example a nested dict) then this must be set to65    #: ``True`` manually when modifying that data. The session cookie66    #: will only be written to the response if this is ``True``.67    modified = False68 69    #: When data is read or written, this is set to ``True``. Used by70    # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``71    #: header, which allows caching proxies to cache different pages for72    #: different users.73    accessed = False74 75    def __init__(self, initial: t.Any = None) -> None:76        def on_update(self: te.Self) -> None:77            self.modified = True78            self.accessed = True79 80        super().__init__(initial, on_update)81 82    def __getitem__(self, key: str) -> t.Any:83        self.accessed = True84        return super().__getitem__(key)85 86    def get(self, key: str, default: t.Any = None) -> t.Any:87        self.accessed = True88        return super().get(key, default)89 90    def setdefault(self, key: str, default: t.Any = None) -> t.Any:91        self.accessed = True92        return super().setdefault(key, default)93 94 95class NullSession(SecureCookieSession):96    """Class used to generate nicer error messages if sessions are not97    available.  Will still allow read-only access to the empty session98    but fail on setting.99    """100 101    def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:102        raise RuntimeError(103            "The session is unavailable because no secret "104            "key was set.  Set the secret_key on the "105            "application to something unique and secret."106        )107 108    __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail  # type: ignore # noqa: B950109    del _fail110 111 112class SessionInterface:113    """The basic interface you have to implement in order to replace the114    default session interface which uses werkzeug's securecookie115    implementation.  The only methods you have to implement are116    :meth:`open_session` and :meth:`save_session`, the others have117    useful defaults which you don't need to change.118 119    The session object returned by the :meth:`open_session` method has to120    provide a dictionary like interface plus the properties and methods121    from the :class:`SessionMixin`.  We recommend just subclassing a dict122    and adding that mixin::123 124        class Session(dict, SessionMixin):125            pass126 127    If :meth:`open_session` returns ``None`` Flask will call into128    :meth:`make_null_session` to create a session that acts as replacement129    if the session support cannot work because some requirement is not130    fulfilled.  The default :class:`NullSession` class that is created131    will complain that the secret key was not set.132 133    To replace the session interface on an application all you have to do134    is to assign :attr:`flask.Flask.session_interface`::135 136        app = Flask(__name__)137        app.session_interface = MySessionInterface()138 139    Multiple requests with the same session may be sent and handled140    concurrently. When implementing a new session interface, consider141    whether reads or writes to the backing store must be synchronized.142    There is no guarantee on the order in which the session for each143    request is opened or saved, it will occur in the order that requests144    begin and end processing.145 146    .. versionadded:: 0.8147    """148 149    #: :meth:`make_null_session` will look here for the class that should150    #: be created when a null session is requested.  Likewise the151    #: :meth:`is_null_session` method will perform a typecheck against152    #: this type.153    null_session_class = NullSession154 155    #: A flag that indicates if the session interface is pickle based.156    #: This can be used by Flask extensions to make a decision in regards157    #: to how to deal with the session object.158    #:159    #: .. versionadded:: 0.10160    pickle_based = False161 162    def make_null_session(self, app: Flask) -> NullSession:163        """Creates a null session which acts as a replacement object if the164        real session support could not be loaded due to a configuration165        error.  This mainly aids the user experience because the job of the166        null session is to still support lookup without complaining but167        modifications are answered with a helpful error message of what168        failed.169 170        This creates an instance of :attr:`null_session_class` by default.171        """172        return self.null_session_class()173 174    def is_null_session(self, obj: object) -> bool:175        """Checks if a given object is a null session.  Null sessions are176        not asked to be saved.177 178        This checks if the object is an instance of :attr:`null_session_class`179        by default.180        """181        return isinstance(obj, self.null_session_class)182 183    def get_cookie_name(self, app: Flask) -> str:184        """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""185        return app.config["SESSION_COOKIE_NAME"]  # type: ignore[no-any-return]186 187    def get_cookie_domain(self, app: Flask) -> str | None:188        """The value of the ``Domain`` parameter on the session cookie. If not set,189        browsers will only send the cookie to the exact domain it was set from.190        Otherwise, they will send it to any subdomain of the given value as well.191 192        Uses the :data:`SESSION_COOKIE_DOMAIN` config.193 194        .. versionchanged:: 2.3195            Not set by default, does not fall back to ``SERVER_NAME``.196        """197        return app.config["SESSION_COOKIE_DOMAIN"]  # type: ignore[no-any-return]198 199    def get_cookie_path(self, app: Flask) -> str:200        """Returns the path for which the cookie should be valid.  The201        default implementation uses the value from the ``SESSION_COOKIE_PATH``202        config var if it's set, and falls back to ``APPLICATION_ROOT`` or203        uses ``/`` if it's ``None``.204        """205        return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"]  # type: ignore[no-any-return]206 207    def get_cookie_httponly(self, app: Flask) -> bool:208        """Returns True if the session cookie should be httponly.  This209        currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``210        config var.211        """212        return app.config["SESSION_COOKIE_HTTPONLY"]  # type: ignore[no-any-return]213 214    def get_cookie_secure(self, app: Flask) -> bool:215        """Returns True if the cookie should be secure.  This currently216        just returns the value of the ``SESSION_COOKIE_SECURE`` setting.217        """218        return app.config["SESSION_COOKIE_SECURE"]  # type: ignore[no-any-return]219 220    def get_cookie_samesite(self, app: Flask) -> str | None:221        """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the222        ``SameSite`` attribute. This currently just returns the value of223        the :data:`SESSION_COOKIE_SAMESITE` setting.224        """225        return app.config["SESSION_COOKIE_SAMESITE"]  # type: ignore[no-any-return]226 227    def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:228        """A helper method that returns an expiration date for the session229        or ``None`` if the session is linked to the browser session.  The230        default implementation returns now + the permanent session231        lifetime configured on the application.232        """233        if session.permanent:234            return datetime.now(timezone.utc) + app.permanent_session_lifetime235        return None236 237    def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:238        """Used by session backends to determine if a ``Set-Cookie`` header239        should be set for this session cookie for this response. If the session240        has been modified, the cookie is set. If the session is permanent and241        the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is242        always set.243 244        This check is usually skipped if the session was deleted.245 246        .. versionadded:: 0.11247        """248 249        return session.modified or (250            session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]251        )252 253    def open_session(self, app: Flask, request: Request) -> SessionMixin | None:254        """This is called at the beginning of each request, after255        pushing the request context, before matching the URL.256 257        This must return an object which implements a dictionary-like258        interface as well as the :class:`SessionMixin` interface.259 260        This will return ``None`` to indicate that loading failed in261        some way that is not immediately an error. The request262        context will fall back to using :meth:`make_null_session`263        in this case.264        """265        raise NotImplementedError()266 267    def save_session(268        self, app: Flask, session: SessionMixin, response: Response269    ) -> None:270        """This is called at the end of each request, after generating271        a response, before removing the request context. It is skipped272        if :meth:`is_null_session` returns ``True``.273        """274        raise NotImplementedError()275 276 277session_json_serializer = TaggedJSONSerializer()278 279 280def _lazy_sha1(string: bytes = b"") -> t.Any:281    """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include282    SHA-1, in which case the import and use as a default would fail before the283    developer can configure something else.284    """285    return hashlib.sha1(string)286 287 288class SecureCookieSessionInterface(SessionInterface):289    """The default session interface that stores sessions in signed cookies290    through the :mod:`itsdangerous` module.291    """292 293    #: the salt that should be applied on top of the secret key for the294    #: signing of cookie based sessions.295    salt = "cookie-session"296    #: the hash function to use for the signature.  The default is sha1297    digest_method = staticmethod(_lazy_sha1)298    #: the name of the itsdangerous supported key derivation.  The default299    #: is hmac.300    key_derivation = "hmac"301    #: A python serializer for the payload.  The default is a compact302    #: JSON derived serializer with support for some extra Python types303    #: such as datetime objects or tuples.304    serializer = session_json_serializer305    session_class = SecureCookieSession306 307    def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:308        if not app.secret_key:309            return None310        signer_kwargs = dict(311            key_derivation=self.key_derivation, digest_method=self.digest_method312        )313        return URLSafeTimedSerializer(314            app.secret_key,315            salt=self.salt,316            serializer=self.serializer,317            signer_kwargs=signer_kwargs,318        )319 320    def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None:321        s = self.get_signing_serializer(app)322        if s is None:323            return None324        val = request.cookies.get(self.get_cookie_name(app))325        if not val:326            return self.session_class()327        max_age = int(app.permanent_session_lifetime.total_seconds())328        try:329            data = s.loads(val, max_age=max_age)330            return self.session_class(data)331        except BadSignature:332            return self.session_class()333 334    def save_session(335        self, app: Flask, session: SessionMixin, response: Response336    ) -> None:337        name = self.get_cookie_name(app)338        domain = self.get_cookie_domain(app)339        path = self.get_cookie_path(app)340        secure = self.get_cookie_secure(app)341        samesite = self.get_cookie_samesite(app)342        httponly = self.get_cookie_httponly(app)343 344        # Add a "Vary: Cookie" header if the session was accessed at all.345        if session.accessed:346            response.vary.add("Cookie")347 348        # If the session is modified to be empty, remove the cookie.349        # If the session is empty, return without setting the cookie.350        if not session:351            if session.modified:352                response.delete_cookie(353                    name,354                    domain=domain,355                    path=path,356                    secure=secure,357                    samesite=samesite,358                    httponly=httponly,359                )360                response.vary.add("Cookie")361 362            return363 364        if not self.should_set_cookie(app, session):365            return366 367        expires = self.get_expiration_time(app, session)368        val = self.get_signing_serializer(app).dumps(dict(session))  # type: ignore369        response.set_cookie(370            name,371            val,  # type: ignore372            expires=expires,373            httponly=httponly,374            domain=domain,375            path=path,376            secure=secure,377            samesite=samesite,378        )379        response.vary.add("Cookie")380 
codekingpro/portable-devtools · Team Ai