Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/sessions.py: 47%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import hashlib
4import typing as t
5from collections.abc import MutableMapping
6from datetime import datetime
7from datetime import timezone
9from itsdangerous import BadSignature
10from itsdangerous import URLSafeTimedSerializer
11from werkzeug.datastructures import CallbackDict
13from .json.tag import TaggedJSONSerializer
15if t.TYPE_CHECKING: # pragma: no cover
16 import typing_extensions as te
18 from .app import Flask
19 from .wrappers import Request
20 from .wrappers import Response
23# TODO generic when Python > 3.8
24class SessionMixin(MutableMapping): # type: ignore[type-arg]
25 """Expands a basic dictionary with session attributes."""
27 @property
28 def permanent(self) -> bool:
29 """This reflects the ``'_permanent'`` key in the dict."""
30 return self.get("_permanent", False)
32 @permanent.setter
33 def permanent(self, value: bool) -> None:
34 self["_permanent"] = bool(value)
36 #: Some implementations can detect whether a session is newly
37 #: created, but that is not guaranteed. Use with caution. The mixin
38 # default is hard-coded ``False``.
39 new = False
41 #: Some implementations can detect changes to the session and set
42 #: this when that happens. The mixin default is hard coded to
43 #: ``True``.
44 modified = True
46 #: Some implementations can detect when session data is read or
47 #: written and set this when that happens. The mixin default is hard
48 #: coded to ``True``.
49 accessed = True
52# TODO generic when Python > 3.8
53class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg]
54 """Base class for sessions based on signed cookies.
56 This session backend will set the :attr:`modified` and
57 :attr:`accessed` attributes. It cannot reliably track whether a
58 session is new (vs. empty), so :attr:`new` remains hard coded to
59 ``False``.
60 """
62 #: When data is changed, this is set to ``True``. Only the session
63 #: dictionary itself is tracked; if the session contains mutable
64 #: data (for example a nested dict) then this must be set to
65 #: ``True`` manually when modifying that data. The session cookie
66 #: will only be written to the response if this is ``True``.
67 modified = False
69 #: When data is read or written, this is set to ``True``. Used by
70 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
71 #: header, which allows caching proxies to cache different pages for
72 #: different users.
73 accessed = False
75 def __init__(self, initial: t.Any = None) -> None:
76 def on_update(self: te.Self) -> None:
77 self.modified = True
78 self.accessed = True
80 super().__init__(initial, on_update)
82 def __getitem__(self, key: str) -> t.Any:
83 self.accessed = True
84 return super().__getitem__(key)
86 def get(self, key: str, default: t.Any = None) -> t.Any:
87 self.accessed = True
88 return super().get(key, default)
90 def setdefault(self, key: str, default: t.Any = None) -> t.Any:
91 self.accessed = True
92 return super().setdefault(key, default)
95class NullSession(SecureCookieSession):
96 """Class used to generate nicer error messages if sessions are not
97 available. Will still allow read-only access to the empty session
98 but fail on setting.
99 """
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 )
108 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
109 del _fail
112class SessionInterface:
113 """The basic interface you have to implement in order to replace the
114 default session interface which uses werkzeug's securecookie
115 implementation. The only methods you have to implement are
116 :meth:`open_session` and :meth:`save_session`, the others have
117 useful defaults which you don't need to change.
119 The session object returned by the :meth:`open_session` method has to
120 provide a dictionary like interface plus the properties and methods
121 from the :class:`SessionMixin`. We recommend just subclassing a dict
122 and adding that mixin::
124 class Session(dict, SessionMixin):
125 pass
127 If :meth:`open_session` returns ``None`` Flask will call into
128 :meth:`make_null_session` to create a session that acts as replacement
129 if the session support cannot work because some requirement is not
130 fulfilled. The default :class:`NullSession` class that is created
131 will complain that the secret key was not set.
133 To replace the session interface on an application all you have to do
134 is to assign :attr:`flask.Flask.session_interface`::
136 app = Flask(__name__)
137 app.session_interface = MySessionInterface()
139 Multiple requests with the same session may be sent and handled
140 concurrently. When implementing a new session interface, consider
141 whether reads or writes to the backing store must be synchronized.
142 There is no guarantee on the order in which the session for each
143 request is opened or saved, it will occur in the order that requests
144 begin and end processing.
146 .. versionadded:: 0.8
147 """
149 #: :meth:`make_null_session` will look here for the class that should
150 #: be created when a null session is requested. Likewise the
151 #: :meth:`is_null_session` method will perform a typecheck against
152 #: this type.
153 null_session_class = NullSession
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 regards
157 #: to how to deal with the session object.
158 #:
159 #: .. versionadded:: 0.10
160 pickle_based = False
162 def make_null_session(self, app: Flask) -> NullSession:
163 """Creates a null session which acts as a replacement object if the
164 real session support could not be loaded due to a configuration
165 error. This mainly aids the user experience because the job of the
166 null session is to still support lookup without complaining but
167 modifications are answered with a helpful error message of what
168 failed.
170 This creates an instance of :attr:`null_session_class` by default.
171 """
172 return self.null_session_class()
174 def is_null_session(self, obj: object) -> bool:
175 """Checks if a given object is a null session. Null sessions are
176 not asked to be saved.
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)
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]
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.
192 Uses the :data:`SESSION_COOKIE_DOMAIN` config.
194 .. versionchanged:: 2.3
195 Not set by default, does not fall back to ``SERVER_NAME``.
196 """
197 return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]
199 def get_cookie_path(self, app: Flask) -> str:
200 """Returns the path for which the cookie should be valid. The
201 default implementation uses the value from the ``SESSION_COOKIE_PATH``
202 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
203 uses ``/`` if it's ``None``.
204 """
205 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]
207 def get_cookie_httponly(self, app: Flask) -> bool:
208 """Returns True if the session cookie should be httponly. This
209 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]
214 def get_cookie_secure(self, app: Flask) -> bool:
215 """Returns True if the cookie should be secure. This currently
216 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
217 """
218 return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]
220 def get_cookie_samesite(self, app: Flask) -> str | None:
221 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
222 ``SameSite`` attribute. This currently just returns the value of
223 the :data:`SESSION_COOKIE_SAMESITE` setting.
224 """
225 return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]
227 def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None:
228 """A helper method that returns an expiration date for the session
229 or ``None`` if the session is linked to the browser session. The
230 default implementation returns now + the permanent session
231 lifetime configured on the application.
232 """
233 if session.permanent:
234 return datetime.now(timezone.utc) + app.permanent_session_lifetime
235 return None
237 def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool:
238 """Used by session backends to determine if a ``Set-Cookie`` header
239 should be set for this session cookie for this response. If the session
240 has been modified, the cookie is set. If the session is permanent and
241 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
242 always set.
244 This check is usually skipped if the session was deleted.
246 .. versionadded:: 0.11
247 """
249 return session.modified or (
250 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
251 )
253 def open_session(self, app: Flask, request: Request) -> SessionMixin | None:
254 """This is called at the beginning of each request, after
255 pushing the request context, before matching the URL.
257 This must return an object which implements a dictionary-like
258 interface as well as the :class:`SessionMixin` interface.
260 This will return ``None`` to indicate that loading failed in
261 some way that is not immediately an error. The request
262 context will fall back to using :meth:`make_null_session`
263 in this case.
264 """
265 raise NotImplementedError()
267 def save_session(
268 self, app: Flask, session: SessionMixin, response: Response
269 ) -> None:
270 """This is called at the end of each request, after generating
271 a response, before removing the request context. It is skipped
272 if :meth:`is_null_session` returns ``True``.
273 """
274 raise NotImplementedError()
277session_json_serializer = TaggedJSONSerializer()
280def _lazy_sha1(string: bytes = b"") -> t.Any:
281 """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include
282 SHA-1, in which case the import and use as a default would fail before the
283 developer can configure something else.
284 """
285 return hashlib.sha1(string)
288class SecureCookieSessionInterface(SessionInterface):
289 """The default session interface that stores sessions in signed cookies
290 through the :mod:`itsdangerous` module.
291 """
293 #: the salt that should be applied on top of the secret key for the
294 #: signing of cookie based sessions.
295 salt = "cookie-session"
296 #: the hash function to use for the signature. The default is sha1
297 digest_method = staticmethod(_lazy_sha1)
298 #: the name of the itsdangerous supported key derivation. The default
299 #: is hmac.
300 key_derivation = "hmac"
301 #: A python serializer for the payload. The default is a compact
302 #: JSON derived serializer with support for some extra Python types
303 #: such as datetime objects or tuples.
304 serializer = session_json_serializer
305 session_class = SecureCookieSession
307 def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None:
308 if not app.secret_key:
309 return None
310 signer_kwargs = dict(
311 key_derivation=self.key_derivation, digest_method=self.digest_method
312 )
313 return URLSafeTimedSerializer(
314 app.secret_key,
315 salt=self.salt,
316 serializer=self.serializer,
317 signer_kwargs=signer_kwargs,
318 )
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 None
324 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()
334 def save_session(
335 self, app: Flask, session: SessionMixin, response: Response
336 ) -> 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)
344 # Add a "Vary: Cookie" header if the session was accessed at all.
345 if session.accessed:
346 response.vary.add("Cookie")
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")
362 return
364 if not self.should_set_cookie(app, session):
365 return
367 expires = self.get_expiration_time(app, session)
368 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore
369 response.set_cookie(
370 name,
371 val, # type: ignore
372 expires=expires,
373 httponly=httponly,
374 domain=domain,
375 path=path,
376 secure=secure,
377 samesite=samesite,
378 )
379 response.vary.add("Cookie")