aboutsummaryrefslogtreecommitdiffstats
path: root/python/flask/sessions.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/flask/sessions.py')
-rw-r--r--python/flask/sessions.py385
1 files changed, 385 insertions, 0 deletions
diff --git a/python/flask/sessions.py b/python/flask/sessions.py
new file mode 100644
index 0000000..c8b7d4e
--- /dev/null
+++ b/python/flask/sessions.py
@@ -0,0 +1,385 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.sessions
+ ~~~~~~~~~~~~~~
+
+ Implements cookie based sessions based on itsdangerous.
+
+ :copyright: © 2010 by the Pallets team.
+ :license: BSD, see LICENSE for more details.
+"""
+
+import hashlib
+import warnings
+from datetime import datetime
+
+from itsdangerous import BadSignature, URLSafeTimedSerializer
+from werkzeug.datastructures import CallbackDict
+
+from flask._compat import collections_abc
+from flask.helpers import is_ip, total_seconds
+from flask.json.tag import TaggedJSONSerializer
+
+
+class SessionMixin(collections_abc.MutableMapping):
+ """Expands a basic dictionary with session attributes."""
+
+ @property
+ def permanent(self):
+ """This reflects the ``'_permanent'`` key in the dict."""
+ return self.get('_permanent', False)
+
+ @permanent.setter
+ def permanent(self, value):
+ 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=None):
+ def on_update(self):
+ self.modified = True
+ self.accessed = True
+
+ super(SecureCookieSession, self).__init__(initial, on_update)
+
+ def __getitem__(self, key):
+ self.accessed = True
+ return super(SecureCookieSession, self).__getitem__(key)
+
+ def get(self, key, default=None):
+ self.accessed = True
+ return super(SecureCookieSession, self).get(key, default)
+
+ def setdefault(self, key, default=None):
+ self.accessed = True
+ return super(SecureCookieSession, self).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, **kwargs):
+ 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
+ del _fail
+
+
+class SessionInterface(object):
+ """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()
+
+ .. 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):
+ """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):
+ """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_domain(self, app):
+ """Returns the domain that should be set for the session cookie.
+
+ Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
+ falls back to detecting the domain based on ``SERVER_NAME``.
+
+ Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
+ updated to avoid re-running the logic.
+ """
+
+ rv = app.config['SESSION_COOKIE_DOMAIN']
+
+ # set explicitly, or cached from SERVER_NAME detection
+ # if False, return None
+ if rv is not None:
+ return rv if rv else None
+
+ rv = app.config['SERVER_NAME']
+
+ # server name not set, cache False to return none next time
+ if not rv:
+ app.config['SESSION_COOKIE_DOMAIN'] = False
+ return None
+
+ # chop off the port which is usually not supported by browsers
+ # remove any leading '.' since we'll add that later
+ rv = rv.rsplit(':', 1)[0].lstrip('.')
+
+ if '.' not in rv:
+ # Chrome doesn't allow names without a '.'
+ # this should only come up with localhost
+ # hack around this by not setting the name, and show a warning
+ warnings.warn(
+ '"{rv}" is not a valid cookie domain, it must contain a ".".'
+ ' Add an entry to your hosts file, for example'
+ ' "{rv}.localdomain", and use that instead.'.format(rv=rv)
+ )
+ app.config['SESSION_COOKIE_DOMAIN'] = False
+ return None
+
+ ip = is_ip(rv)
+
+ if ip:
+ warnings.warn(
+ 'The session cookie domain is an IP address. This may not work'
+ ' as intended in some browsers. Add an entry to your hosts'
+ ' file, for example "localhost.localdomain", and use that'
+ ' instead.'
+ )
+
+ # if this is not an ip and app is mounted at the root, allow subdomain
+ # matching by adding a '.' prefix
+ if self.get_cookie_path(app) == '/' and not ip:
+ rv = '.' + rv
+
+ app.config['SESSION_COOKIE_DOMAIN'] = rv
+ return rv
+
+ def get_cookie_path(self, app):
+ """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):
+ """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):
+ """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):
+ """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, session):
+ """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.utcnow() + app.permanent_session_lifetime
+
+ def should_set_cookie(self, app, session):
+ """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, request):
+ """This method has to be implemented and must either return ``None``
+ in case the loading failed because of a configuration error or an
+ instance of a session object which implements a dictionary like
+ interface + the methods and attributes on :class:`SessionMixin`.
+ """
+ raise NotImplementedError()
+
+ def save_session(self, app, session, response):
+ """This is called for actual sessions returned by :meth:`open_session`
+ at the end of the request. This is still called during a request
+ context so if you absolutely need access to the request you can do
+ that.
+ """
+ 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):
+ 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, request):
+ s = self.get_signing_serializer(app)
+ if s is None:
+ return None
+ val = request.cookies.get(app.session_cookie_name)
+ if not val:
+ return self.session_class()
+ max_age = total_seconds(app.permanent_session_lifetime)
+ 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, session, response):
+ domain = self.get_cookie_domain(app)
+ path = self.get_cookie_path(app)
+
+ # 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(
+ app.session_cookie_name,
+ domain=domain,
+ path=path
+ )
+
+ return
+
+ # Add a "Vary: Cookie" header if the session was accessed at all.
+ if session.accessed:
+ response.vary.add('Cookie')
+
+ if not self.should_set_cookie(app, session):
+ return
+
+ httponly = self.get_cookie_httponly(app)
+ secure = self.get_cookie_secure(app)
+ samesite = self.get_cookie_samesite(app)
+ expires = self.get_expiration_time(app, session)
+ val = self.get_signing_serializer(app).dumps(dict(session))
+ response.set_cookie(
+ app.session_cookie_name,
+ val,
+ expires=expires,
+ httponly=httponly,
+ domain=domain,
+ path=path,
+ secure=secure,
+ samesite=samesite
+ )