sessions.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # -*- coding: utf-8 -*-
  2. """
  3. flask.sessions
  4. ~~~~~~~~~~~~~~
  5. Implements cookie based sessions based on itsdangerous.
  6. :copyright: © 2010 by the Pallets team.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import hashlib
  10. import warnings
  11. from collections import MutableMapping
  12. from datetime import datetime
  13. from itsdangerous import BadSignature, URLSafeTimedSerializer
  14. from werkzeug.datastructures import CallbackDict
  15. from flask.helpers import is_ip, total_seconds
  16. from flask.json.tag import TaggedJSONSerializer
  17. class SessionMixin(MutableMapping):
  18. """Expands a basic dictionary with session attributes."""
  19. @property
  20. def permanent(self):
  21. """This reflects the ``'_permanent'`` key in the dict."""
  22. return self.get('_permanent', False)
  23. @permanent.setter
  24. def permanent(self, value):
  25. self['_permanent'] = bool(value)
  26. #: Some implementations can detect whether a session is newly
  27. #: created, but that is not guaranteed. Use with caution. The mixin
  28. # default is hard-coded ``False``.
  29. new = False
  30. #: Some implementations can detect changes to the session and set
  31. #: this when that happens. The mixin default is hard coded to
  32. #: ``True``.
  33. modified = True
  34. #: Some implementations can detect when session data is read or
  35. #: written and set this when that happens. The mixin default is hard
  36. #: coded to ``True``.
  37. accessed = True
  38. class SecureCookieSession(CallbackDict, SessionMixin):
  39. """Base class for sessions based on signed cookies.
  40. This session backend will set the :attr:`modified` and
  41. :attr:`accessed` attributes. It cannot reliably track whether a
  42. session is new (vs. empty), so :attr:`new` remains hard coded to
  43. ``False``.
  44. """
  45. #: When data is changed, this is set to ``True``. Only the session
  46. #: dictionary itself is tracked; if the session contains mutable
  47. #: data (for example a nested dict) then this must be set to
  48. #: ``True`` manually when modifying that data. The session cookie
  49. #: will only be written to the response if this is ``True``.
  50. modified = False
  51. #: When data is read or written, this is set to ``True``. Used by
  52. # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
  53. #: header, which allows caching proxies to cache different pages for
  54. #: different users.
  55. accessed = False
  56. def __init__(self, initial=None):
  57. def on_update(self):
  58. self.modified = True
  59. self.accessed = True
  60. super(SecureCookieSession, self).__init__(initial, on_update)
  61. def __getitem__(self, key):
  62. self.accessed = True
  63. return super(SecureCookieSession, self).__getitem__(key)
  64. def get(self, key, default=None):
  65. self.accessed = True
  66. return super(SecureCookieSession, self).get(key, default)
  67. def setdefault(self, key, default=None):
  68. self.accessed = True
  69. return super(SecureCookieSession, self).setdefault(key, default)
  70. class NullSession(SecureCookieSession):
  71. """Class used to generate nicer error messages if sessions are not
  72. available. Will still allow read-only access to the empty session
  73. but fail on setting.
  74. """
  75. def _fail(self, *args, **kwargs):
  76. raise RuntimeError('The session is unavailable because no secret '
  77. 'key was set. Set the secret_key on the '
  78. 'application to something unique and secret.')
  79. __setitem__ = __delitem__ = clear = pop = popitem = \
  80. update = setdefault = _fail
  81. del _fail
  82. class SessionInterface(object):
  83. """The basic interface you have to implement in order to replace the
  84. default session interface which uses werkzeug's securecookie
  85. implementation. The only methods you have to implement are
  86. :meth:`open_session` and :meth:`save_session`, the others have
  87. useful defaults which you don't need to change.
  88. The session object returned by the :meth:`open_session` method has to
  89. provide a dictionary like interface plus the properties and methods
  90. from the :class:`SessionMixin`. We recommend just subclassing a dict
  91. and adding that mixin::
  92. class Session(dict, SessionMixin):
  93. pass
  94. If :meth:`open_session` returns ``None`` Flask will call into
  95. :meth:`make_null_session` to create a session that acts as replacement
  96. if the session support cannot work because some requirement is not
  97. fulfilled. The default :class:`NullSession` class that is created
  98. will complain that the secret key was not set.
  99. To replace the session interface on an application all you have to do
  100. is to assign :attr:`flask.Flask.session_interface`::
  101. app = Flask(__name__)
  102. app.session_interface = MySessionInterface()
  103. .. versionadded:: 0.8
  104. """
  105. #: :meth:`make_null_session` will look here for the class that should
  106. #: be created when a null session is requested. Likewise the
  107. #: :meth:`is_null_session` method will perform a typecheck against
  108. #: this type.
  109. null_session_class = NullSession
  110. #: A flag that indicates if the session interface is pickle based.
  111. #: This can be used by Flask extensions to make a decision in regards
  112. #: to how to deal with the session object.
  113. #:
  114. #: .. versionadded:: 0.10
  115. pickle_based = False
  116. def make_null_session(self, app):
  117. """Creates a null session which acts as a replacement object if the
  118. real session support could not be loaded due to a configuration
  119. error. This mainly aids the user experience because the job of the
  120. null session is to still support lookup without complaining but
  121. modifications are answered with a helpful error message of what
  122. failed.
  123. This creates an instance of :attr:`null_session_class` by default.
  124. """
  125. return self.null_session_class()
  126. def is_null_session(self, obj):
  127. """Checks if a given object is a null session. Null sessions are
  128. not asked to be saved.
  129. This checks if the object is an instance of :attr:`null_session_class`
  130. by default.
  131. """
  132. return isinstance(obj, self.null_session_class)
  133. def get_cookie_domain(self, app):
  134. """Returns the domain that should be set for the session cookie.
  135. Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
  136. falls back to detecting the domain based on ``SERVER_NAME``.
  137. Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
  138. updated to avoid re-running the logic.
  139. """
  140. rv = app.config['SESSION_COOKIE_DOMAIN']
  141. # set explicitly, or cached from SERVER_NAME detection
  142. # if False, return None
  143. if rv is not None:
  144. return rv if rv else None
  145. rv = app.config['SERVER_NAME']
  146. # server name not set, cache False to return none next time
  147. if not rv:
  148. app.config['SESSION_COOKIE_DOMAIN'] = False
  149. return None
  150. # chop off the port which is usually not supported by browsers
  151. # remove any leading '.' since we'll add that later
  152. rv = rv.rsplit(':', 1)[0].lstrip('.')
  153. if '.' not in rv:
  154. # Chrome doesn't allow names without a '.'
  155. # this should only come up with localhost
  156. # hack around this by not setting the name, and show a warning
  157. warnings.warn(
  158. '"{rv}" is not a valid cookie domain, it must contain a ".".'
  159. ' Add an entry to your hosts file, for example'
  160. ' "{rv}.localdomain", and use that instead.'.format(rv=rv)
  161. )
  162. app.config['SESSION_COOKIE_DOMAIN'] = False
  163. return None
  164. ip = is_ip(rv)
  165. if ip:
  166. warnings.warn(
  167. 'The session cookie domain is an IP address. This may not work'
  168. ' as intended in some browsers. Add an entry to your hosts'
  169. ' file, for example "localhost.localdomain", and use that'
  170. ' instead.'
  171. )
  172. # if this is not an ip and app is mounted at the root, allow subdomain
  173. # matching by adding a '.' prefix
  174. if self.get_cookie_path(app) == '/' and not ip:
  175. rv = '.' + rv
  176. app.config['SESSION_COOKIE_DOMAIN'] = rv
  177. return rv
  178. def get_cookie_path(self, app):
  179. """Returns the path for which the cookie should be valid. The
  180. default implementation uses the value from the ``SESSION_COOKIE_PATH``
  181. config var if it's set, and falls back to ``APPLICATION_ROOT`` or
  182. uses ``/`` if it's ``None``.
  183. """
  184. return app.config['SESSION_COOKIE_PATH'] \
  185. or app.config['APPLICATION_ROOT']
  186. def get_cookie_httponly(self, app):
  187. """Returns True if the session cookie should be httponly. This
  188. currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
  189. config var.
  190. """
  191. return app.config['SESSION_COOKIE_HTTPONLY']
  192. def get_cookie_secure(self, app):
  193. """Returns True if the cookie should be secure. This currently
  194. just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
  195. """
  196. return app.config['SESSION_COOKIE_SECURE']
  197. def get_cookie_samesite(self, app):
  198. """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
  199. ``SameSite`` attribute. This currently just returns the value of
  200. the :data:`SESSION_COOKIE_SAMESITE` setting.
  201. """
  202. return app.config['SESSION_COOKIE_SAMESITE']
  203. def get_expiration_time(self, app, session):
  204. """A helper method that returns an expiration date for the session
  205. or ``None`` if the session is linked to the browser session. The
  206. default implementation returns now + the permanent session
  207. lifetime configured on the application.
  208. """
  209. if session.permanent:
  210. return datetime.utcnow() + app.permanent_session_lifetime
  211. def should_set_cookie(self, app, session):
  212. """Used by session backends to determine if a ``Set-Cookie`` header
  213. should be set for this session cookie for this response. If the session
  214. has been modified, the cookie is set. If the session is permanent and
  215. the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
  216. always set.
  217. This check is usually skipped if the session was deleted.
  218. .. versionadded:: 0.11
  219. """
  220. return session.modified or (
  221. session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
  222. )
  223. def open_session(self, app, request):
  224. """This method has to be implemented and must either return ``None``
  225. in case the loading failed because of a configuration error or an
  226. instance of a session object which implements a dictionary like
  227. interface + the methods and attributes on :class:`SessionMixin`.
  228. """
  229. raise NotImplementedError()
  230. def save_session(self, app, session, response):
  231. """This is called for actual sessions returned by :meth:`open_session`
  232. at the end of the request. This is still called during a request
  233. context so if you absolutely need access to the request you can do
  234. that.
  235. """
  236. raise NotImplementedError()
  237. session_json_serializer = TaggedJSONSerializer()
  238. class SecureCookieSessionInterface(SessionInterface):
  239. """The default session interface that stores sessions in signed cookies
  240. through the :mod:`itsdangerous` module.
  241. """
  242. #: the salt that should be applied on top of the secret key for the
  243. #: signing of cookie based sessions.
  244. salt = 'cookie-session'
  245. #: the hash function to use for the signature. The default is sha1
  246. digest_method = staticmethod(hashlib.sha1)
  247. #: the name of the itsdangerous supported key derivation. The default
  248. #: is hmac.
  249. key_derivation = 'hmac'
  250. #: A python serializer for the payload. The default is a compact
  251. #: JSON derived serializer with support for some extra Python types
  252. #: such as datetime objects or tuples.
  253. serializer = session_json_serializer
  254. session_class = SecureCookieSession
  255. def get_signing_serializer(self, app):
  256. if not app.secret_key:
  257. return None
  258. signer_kwargs = dict(
  259. key_derivation=self.key_derivation,
  260. digest_method=self.digest_method
  261. )
  262. return URLSafeTimedSerializer(app.secret_key, salt=self.salt,
  263. serializer=self.serializer,
  264. signer_kwargs=signer_kwargs)
  265. def open_session(self, app, request):
  266. s = self.get_signing_serializer(app)
  267. if s is None:
  268. return None
  269. val = request.cookies.get(app.session_cookie_name)
  270. if not val:
  271. return self.session_class()
  272. max_age = total_seconds(app.permanent_session_lifetime)
  273. try:
  274. data = s.loads(val, max_age=max_age)
  275. return self.session_class(data)
  276. except BadSignature:
  277. return self.session_class()
  278. def save_session(self, app, session, response):
  279. domain = self.get_cookie_domain(app)
  280. path = self.get_cookie_path(app)
  281. # If the session is modified to be empty, remove the cookie.
  282. # If the session is empty, return without setting the cookie.
  283. if not session:
  284. if session.modified:
  285. response.delete_cookie(
  286. app.session_cookie_name,
  287. domain=domain,
  288. path=path
  289. )
  290. return
  291. # Add a "Vary: Cookie" header if the session was accessed at all.
  292. if session.accessed:
  293. response.vary.add('Cookie')
  294. if not self.should_set_cookie(app, session):
  295. return
  296. httponly = self.get_cookie_httponly(app)
  297. secure = self.get_cookie_secure(app)
  298. samesite = self.get_cookie_samesite(app)
  299. expires = self.get_expiration_time(app, session)
  300. val = self.get_signing_serializer(app).dumps(dict(session))
  301. response.set_cookie(
  302. app.session_cookie_name,
  303. val,
  304. expires=expires,
  305. httponly=httponly,
  306. domain=domain,
  307. path=path,
  308. secure=secure,
  309. samesite=samesite
  310. )