Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/flask/sessions.py: 41%
135 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
1import hashlib
2import typing as t
3import warnings
4from collections.abc import MutableMapping
5from datetime import datetime
6from datetime import timezone
8from itsdangerous import BadSignature
9from itsdangerous import URLSafeTimedSerializer
10from werkzeug.datastructures import CallbackDict
12from .helpers import is_ip
13from .json.tag import TaggedJSONSerializer
15if t.TYPE_CHECKING: # pragma: no cover
16 import typing_extensions as te
17 from .app import Flask
18 from .wrappers import Request, Response
21class SessionMixin(MutableMapping):
22 """Expands a basic dictionary with session attributes."""
24 @property
25 def permanent(self) -> bool:
26 """This reflects the ``'_permanent'`` key in the dict."""
27 return self.get("_permanent", False)
29 @permanent.setter
30 def permanent(self, value: bool) -> None:
31 self["_permanent"] = bool(value)
33 #: Some implementations can detect whether a session is newly
34 #: created, but that is not guaranteed. Use with caution. The mixin
35 # default is hard-coded ``False``.
36 new = False
38 #: Some implementations can detect changes to the session and set
39 #: this when that happens. The mixin default is hard coded to
40 #: ``True``.
41 modified = True
43 #: Some implementations can detect when session data is read or
44 #: written and set this when that happens. The mixin default is hard
45 #: coded to ``True``.
46 accessed = True
49class SecureCookieSession(CallbackDict, SessionMixin):
50 """Base class for sessions based on signed cookies.
52 This session backend will set the :attr:`modified` and
53 :attr:`accessed` attributes. It cannot reliably track whether a
54 session is new (vs. empty), so :attr:`new` remains hard coded to
55 ``False``.
56 """
58 #: When data is changed, this is set to ``True``. Only the session
59 #: dictionary itself is tracked; if the session contains mutable
60 #: data (for example a nested dict) then this must be set to
61 #: ``True`` manually when modifying that data. The session cookie
62 #: will only be written to the response if this is ``True``.
63 modified = False
65 #: When data is read or written, this is set to ``True``. Used by
66 # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
67 #: header, which allows caching proxies to cache different pages for
68 #: different users.
69 accessed = False
71 def __init__(self, initial: t.Any = None) -> None:
72 def on_update(self) -> None:
73 self.modified = True
74 self.accessed = True
76 super().__init__(initial, on_update)
78 def __getitem__(self, key: str) -> t.Any:
79 self.accessed = True
80 return super().__getitem__(key)
82 def get(self, key: str, default: t.Any = None) -> t.Any:
83 self.accessed = True
84 return super().get(key, default)
86 def setdefault(self, key: str, default: t.Any = None) -> t.Any:
87 self.accessed = True
88 return super().setdefault(key, default)
91class NullSession(SecureCookieSession):
92 """Class used to generate nicer error messages if sessions are not
93 available. Will still allow read-only access to the empty session
94 but fail on setting.
95 """
97 def _fail(self, *args: t.Any, **kwargs: t.Any) -> "te.NoReturn":
98 raise RuntimeError(
99 "The session is unavailable because no secret "
100 "key was set. Set the secret_key on the "
101 "application to something unique and secret."
102 )
104 __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
105 del _fail
108class SessionInterface:
109 """The basic interface you have to implement in order to replace the
110 default session interface which uses werkzeug's securecookie
111 implementation. The only methods you have to implement are
112 :meth:`open_session` and :meth:`save_session`, the others have
113 useful defaults which you don't need to change.
115 The session object returned by the :meth:`open_session` method has to
116 provide a dictionary like interface plus the properties and methods
117 from the :class:`SessionMixin`. We recommend just subclassing a dict
118 and adding that mixin::
120 class Session(dict, SessionMixin):
121 pass
123 If :meth:`open_session` returns ``None`` Flask will call into
124 :meth:`make_null_session` to create a session that acts as replacement
125 if the session support cannot work because some requirement is not
126 fulfilled. The default :class:`NullSession` class that is created
127 will complain that the secret key was not set.
129 To replace the session interface on an application all you have to do
130 is to assign :attr:`flask.Flask.session_interface`::
132 app = Flask(__name__)
133 app.session_interface = MySessionInterface()
135 Multiple requests with the same session may be sent and handled
136 concurrently. When implementing a new session interface, consider
137 whether reads or writes to the backing store must be synchronized.
138 There is no guarantee on the order in which the session for each
139 request is opened or saved, it will occur in the order that requests
140 begin and end processing.
142 .. versionadded:: 0.8
143 """
145 #: :meth:`make_null_session` will look here for the class that should
146 #: be created when a null session is requested. Likewise the
147 #: :meth:`is_null_session` method will perform a typecheck against
148 #: this type.
149 null_session_class = NullSession
151 #: A flag that indicates if the session interface is pickle based.
152 #: This can be used by Flask extensions to make a decision in regards
153 #: to how to deal with the session object.
154 #:
155 #: .. versionadded:: 0.10
156 pickle_based = False
158 def make_null_session(self, app: "Flask") -> NullSession:
159 """Creates a null session which acts as a replacement object if the
160 real session support could not be loaded due to a configuration
161 error. This mainly aids the user experience because the job of the
162 null session is to still support lookup without complaining but
163 modifications are answered with a helpful error message of what
164 failed.
166 This creates an instance of :attr:`null_session_class` by default.
167 """
168 return self.null_session_class()
170 def is_null_session(self, obj: object) -> bool:
171 """Checks if a given object is a null session. Null sessions are
172 not asked to be saved.
174 This checks if the object is an instance of :attr:`null_session_class`
175 by default.
176 """
177 return isinstance(obj, self.null_session_class)
179 def get_cookie_name(self, app: "Flask") -> str:
180 """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
181 return app.config["SESSION_COOKIE_NAME"]
183 def get_cookie_domain(self, app: "Flask") -> t.Optional[str]:
184 """Returns the domain that should be set for the session cookie.
186 Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
187 falls back to detecting the domain based on ``SERVER_NAME``.
189 Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
190 updated to avoid re-running the logic.
191 """
193 rv = app.config["SESSION_COOKIE_DOMAIN"]
195 # set explicitly, or cached from SERVER_NAME detection
196 # if False, return None
197 if rv is not None:
198 return rv if rv else None
200 rv = app.config["SERVER_NAME"]
202 # server name not set, cache False to return none next time
203 if not rv:
204 app.config["SESSION_COOKIE_DOMAIN"] = False
205 return None
207 # chop off the port which is usually not supported by browsers
208 # remove any leading '.' since we'll add that later
209 rv = rv.rsplit(":", 1)[0].lstrip(".")
211 if "." not in rv:
212 # Chrome doesn't allow names without a '.'. This should only
213 # come up with localhost. Hack around this by not setting
214 # the name, and show a warning.
215 warnings.warn(
216 f"{rv!r} is not a valid cookie domain, it must contain"
217 " a '.'. Add an entry to your hosts file, for example"
218 f" '{rv}.localdomain', and use that instead."
219 )
220 app.config["SESSION_COOKIE_DOMAIN"] = False
221 return None
223 ip = is_ip(rv)
225 if ip:
226 warnings.warn(
227 "The session cookie domain is an IP address. This may not work"
228 " as intended in some browsers. Add an entry to your hosts"
229 ' file, for example "localhost.localdomain", and use that'
230 " instead."
231 )
233 # if this is not an ip and app is mounted at the root, allow subdomain
234 # matching by adding a '.' prefix
235 if self.get_cookie_path(app) == "/" and not ip:
236 rv = f".{rv}"
238 app.config["SESSION_COOKIE_DOMAIN"] = rv
239 return rv
241 def get_cookie_path(self, app: "Flask") -> str:
242 """Returns the path for which the cookie should be valid. The
243 default implementation uses the value from the ``SESSION_COOKIE_PATH``
244 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
245 uses ``/`` if it's ``None``.
246 """
247 return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"]
249 def get_cookie_httponly(self, app: "Flask") -> bool:
250 """Returns True if the session cookie should be httponly. This
251 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
252 config var.
253 """
254 return app.config["SESSION_COOKIE_HTTPONLY"]
256 def get_cookie_secure(self, app: "Flask") -> bool:
257 """Returns True if the cookie should be secure. This currently
258 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
259 """
260 return app.config["SESSION_COOKIE_SECURE"]
262 def get_cookie_samesite(self, app: "Flask") -> str:
263 """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
264 ``SameSite`` attribute. This currently just returns the value of
265 the :data:`SESSION_COOKIE_SAMESITE` setting.
266 """
267 return app.config["SESSION_COOKIE_SAMESITE"]
269 def get_expiration_time(
270 self, app: "Flask", session: SessionMixin
271 ) -> t.Optional[datetime]:
272 """A helper method that returns an expiration date for the session
273 or ``None`` if the session is linked to the browser session. The
274 default implementation returns now + the permanent session
275 lifetime configured on the application.
276 """
277 if session.permanent:
278 return datetime.now(timezone.utc) + app.permanent_session_lifetime
279 return None
281 def should_set_cookie(self, app: "Flask", session: SessionMixin) -> bool:
282 """Used by session backends to determine if a ``Set-Cookie`` header
283 should be set for this session cookie for this response. If the session
284 has been modified, the cookie is set. If the session is permanent and
285 the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
286 always set.
288 This check is usually skipped if the session was deleted.
290 .. versionadded:: 0.11
291 """
293 return session.modified or (
294 session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
295 )
297 def open_session(
298 self, app: "Flask", request: "Request"
299 ) -> t.Optional[SessionMixin]:
300 """This is called at the beginning of each request, after
301 pushing the request context, before matching the URL.
303 This must return an object which implements a dictionary-like
304 interface as well as the :class:`SessionMixin` interface.
306 This will return ``None`` to indicate that loading failed in
307 some way that is not immediately an error. The request
308 context will fall back to using :meth:`make_null_session`
309 in this case.
310 """
311 raise NotImplementedError()
313 def save_session(
314 self, app: "Flask", session: SessionMixin, response: "Response"
315 ) -> None:
316 """This is called at the end of each request, after generating
317 a response, before removing the request context. It is skipped
318 if :meth:`is_null_session` returns ``True``.
319 """
320 raise NotImplementedError()
323session_json_serializer = TaggedJSONSerializer()
326class SecureCookieSessionInterface(SessionInterface):
327 """The default session interface that stores sessions in signed cookies
328 through the :mod:`itsdangerous` module.
329 """
331 #: the salt that should be applied on top of the secret key for the
332 #: signing of cookie based sessions.
333 salt = "cookie-session"
334 #: the hash function to use for the signature. The default is sha1
335 digest_method = staticmethod(hashlib.sha1)
336 #: the name of the itsdangerous supported key derivation. The default
337 #: is hmac.
338 key_derivation = "hmac"
339 #: A python serializer for the payload. The default is a compact
340 #: JSON derived serializer with support for some extra Python types
341 #: such as datetime objects or tuples.
342 serializer = session_json_serializer
343 session_class = SecureCookieSession
345 def get_signing_serializer(
346 self, app: "Flask"
347 ) -> t.Optional[URLSafeTimedSerializer]:
348 if not app.secret_key:
349 return None
350 signer_kwargs = dict(
351 key_derivation=self.key_derivation, digest_method=self.digest_method
352 )
353 return URLSafeTimedSerializer(
354 app.secret_key,
355 salt=self.salt,
356 serializer=self.serializer,
357 signer_kwargs=signer_kwargs,
358 )
360 def open_session(
361 self, app: "Flask", request: "Request"
362 ) -> t.Optional[SecureCookieSession]:
363 s = self.get_signing_serializer(app)
364 if s is None:
365 return None
366 val = request.cookies.get(self.get_cookie_name(app))
367 if not val:
368 return self.session_class()
369 max_age = int(app.permanent_session_lifetime.total_seconds())
370 try:
371 data = s.loads(val, max_age=max_age)
372 return self.session_class(data)
373 except BadSignature:
374 return self.session_class()
376 def save_session(
377 self, app: "Flask", session: SessionMixin, response: "Response"
378 ) -> None:
379 name = self.get_cookie_name(app)
380 domain = self.get_cookie_domain(app)
381 path = self.get_cookie_path(app)
382 secure = self.get_cookie_secure(app)
383 samesite = self.get_cookie_samesite(app)
384 httponly = self.get_cookie_httponly(app)
386 # Add a "Vary: Cookie" header if the session was accessed at all.
387 if session.accessed:
388 response.vary.add("Cookie")
390 # If the session is modified to be empty, remove the cookie.
391 # If the session is empty, return without setting the cookie.
392 if not session:
393 if session.modified:
394 response.delete_cookie(
395 name,
396 domain=domain,
397 path=path,
398 secure=secure,
399 samesite=samesite,
400 httponly=httponly,
401 )
402 response.vary.add("Cookie")
404 return
406 if not self.should_set_cookie(app, session):
407 return
409 expires = self.get_expiration_time(app, session)
410 val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore
411 response.set_cookie(
412 name,
413 val, # type: ignore
414 expires=expires,
415 httponly=httponly,
416 domain=domain,
417 path=path,
418 secure=secure,
419 samesite=samesite,
420 )
421 response.vary.add("Cookie")