sessions.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. # -*- coding: utf-8 -*-
  2. """
  3. flask.sessions
  4. ~~~~~~~~~~~~~~
  5. Implements cookie based sessions based on itsdangerous.
  6. :copyright: (c) 2015 by Armin Ronacher.
  7. :license: BSD, see LICENSE for more details.
  8. """
  9. import uuid
  10. import hashlib
  11. from base64 import b64encode, b64decode
  12. from datetime import datetime
  13. from werkzeug.http import http_date, parse_date
  14. from werkzeug.datastructures import CallbackDict
  15. from . import Markup, json
  16. from ._compat import iteritems, text_type
  17. from .helpers import total_seconds
  18. from itsdangerous import URLSafeTimedSerializer, BadSignature
  19. class SessionMixin(object):
  20. """Expands a basic dictionary with an accessors that are expected
  21. by Flask extensions and users for the session.
  22. """
  23. def _get_permanent(self):
  24. return self.get('_permanent', False)
  25. def _set_permanent(self, value):
  26. self['_permanent'] = bool(value)
  27. #: this reflects the ``'_permanent'`` key in the dict.
  28. permanent = property(_get_permanent, _set_permanent)
  29. del _get_permanent, _set_permanent
  30. #: some session backends can tell you if a session is new, but that is
  31. #: not necessarily guaranteed. Use with caution. The default mixin
  32. #: implementation just hardcodes ``False`` in.
  33. new = False
  34. #: for some backends this will always be ``True``, but some backends will
  35. #: default this to false and detect changes in the dictionary for as
  36. #: long as changes do not happen on mutable structures in the session.
  37. #: The default mixin implementation just hardcodes ``True`` in.
  38. modified = True
  39. def _tag(value):
  40. if isinstance(value, tuple):
  41. return {' t': [_tag(x) for x in value]}
  42. elif isinstance(value, uuid.UUID):
  43. return {' u': value.hex}
  44. elif isinstance(value, bytes):
  45. return {' b': b64encode(value).decode('ascii')}
  46. elif callable(getattr(value, '__html__', None)):
  47. return {' m': text_type(value.__html__())}
  48. elif isinstance(value, list):
  49. return [_tag(x) for x in value]
  50. elif isinstance(value, datetime):
  51. return {' d': http_date(value)}
  52. elif isinstance(value, dict):
  53. return dict((k, _tag(v)) for k, v in iteritems(value))
  54. elif isinstance(value, str):
  55. try:
  56. return text_type(value)
  57. except UnicodeError:
  58. from flask.debughelpers import UnexpectedUnicodeError
  59. raise UnexpectedUnicodeError(u'A byte string with '
  60. u'non-ASCII data was passed to the session system '
  61. u'which can only store unicode strings. Consider '
  62. u'base64 encoding your string (String was %r)' % value)
  63. return value
  64. class TaggedJSONSerializer(object):
  65. """A customized JSON serializer that supports a few extra types that
  66. we take for granted when serializing (tuples, markup objects, datetime).
  67. """
  68. def dumps(self, value):
  69. return json.dumps(_tag(value), separators=(',', ':'))
  70. def loads(self, value):
  71. def object_hook(obj):
  72. if len(obj) != 1:
  73. return obj
  74. the_key, the_value = next(iteritems(obj))
  75. if the_key == ' t':
  76. return tuple(the_value)
  77. elif the_key == ' u':
  78. return uuid.UUID(the_value)
  79. elif the_key == ' b':
  80. return b64decode(the_value)
  81. elif the_key == ' m':
  82. return Markup(the_value)
  83. elif the_key == ' d':
  84. return parse_date(the_value)
  85. return obj
  86. return json.loads(value, object_hook=object_hook)
  87. session_json_serializer = TaggedJSONSerializer()
  88. class SecureCookieSession(CallbackDict, SessionMixin):
  89. """Base class for sessions based on signed cookies."""
  90. def __init__(self, initial=None):
  91. def on_update(self):
  92. self.modified = True
  93. CallbackDict.__init__(self, initial, on_update)
  94. self.modified = False
  95. class 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. """
  100. def _fail(self, *args, **kwargs):
  101. raise RuntimeError('The session is unavailable because no secret '
  102. 'key was set. Set the secret_key on the '
  103. 'application to something unique and secret.')
  104. __setitem__ = __delitem__ = clear = pop = popitem = \
  105. update = setdefault = _fail
  106. del _fail
  107. class SessionInterface(object):
  108. """The basic interface you have to implement in order to replace the
  109. default session interface which uses werkzeug's securecookie
  110. implementation. The only methods you have to implement are
  111. :meth:`open_session` and :meth:`save_session`, the others have
  112. useful defaults which you don't need to change.
  113. The session object returned by the :meth:`open_session` method has to
  114. provide a dictionary like interface plus the properties and methods
  115. from the :class:`SessionMixin`. We recommend just subclassing a dict
  116. and adding that mixin::
  117. class Session(dict, SessionMixin):
  118. pass
  119. If :meth:`open_session` returns ``None`` Flask will call into
  120. :meth:`make_null_session` to create a session that acts as replacement
  121. if the session support cannot work because some requirement is not
  122. fulfilled. The default :class:`NullSession` class that is created
  123. will complain that the secret key was not set.
  124. To replace the session interface on an application all you have to do
  125. is to assign :attr:`flask.Flask.session_interface`::
  126. app = Flask(__name__)
  127. app.session_interface = MySessionInterface()
  128. .. versionadded:: 0.8
  129. """
  130. #: :meth:`make_null_session` will look here for the class that should
  131. #: be created when a null session is requested. Likewise the
  132. #: :meth:`is_null_session` method will perform a typecheck against
  133. #: this type.
  134. null_session_class = NullSession
  135. #: A flag that indicates if the session interface is pickle based.
  136. #: This can be used by flask extensions to make a decision in regards
  137. #: to how to deal with the session object.
  138. #:
  139. #: .. versionadded:: 0.10
  140. pickle_based = False
  141. def make_null_session(self, app):
  142. """Creates a null session which acts as a replacement object if the
  143. real session support could not be loaded due to a configuration
  144. error. This mainly aids the user experience because the job of the
  145. null session is to still support lookup without complaining but
  146. modifications are answered with a helpful error message of what
  147. failed.
  148. This creates an instance of :attr:`null_session_class` by default.
  149. """
  150. return self.null_session_class()
  151. def is_null_session(self, obj):
  152. """Checks if a given object is a null session. Null sessions are
  153. not asked to be saved.
  154. This checks if the object is an instance of :attr:`null_session_class`
  155. by default.
  156. """
  157. return isinstance(obj, self.null_session_class)
  158. def get_cookie_domain(self, app):
  159. """Helpful helper method that returns the cookie domain that should
  160. be used for the session cookie if session cookies are used.
  161. """
  162. if app.config['SESSION_COOKIE_DOMAIN'] is not None:
  163. return app.config['SESSION_COOKIE_DOMAIN']
  164. if app.config['SERVER_NAME'] is not None:
  165. # chop off the port which is usually not supported by browsers
  166. rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
  167. # Google chrome does not like cookies set to .localhost, so
  168. # we just go with no domain then. Flask documents anyways that
  169. # cross domain cookies need a fully qualified domain name
  170. if rv == '.localhost':
  171. rv = None
  172. # If we infer the cookie domain from the server name we need
  173. # to check if we are in a subpath. In that case we can't
  174. # set a cross domain cookie.
  175. if rv is not None:
  176. path = self.get_cookie_path(app)
  177. if path != '/':
  178. rv = rv.lstrip('.')
  179. return rv
  180. def get_cookie_path(self, app):
  181. """Returns the path for which the cookie should be valid. The
  182. default implementation uses the value from the ``SESSION_COOKIE_PATH``
  183. config var if it's set, and falls back to ``APPLICATION_ROOT`` or
  184. uses ``/`` if it's ``None``.
  185. """
  186. return app.config['SESSION_COOKIE_PATH'] or \
  187. app.config['APPLICATION_ROOT'] or '/'
  188. def get_cookie_httponly(self, app):
  189. """Returns True if the session cookie should be httponly. This
  190. currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
  191. config var.
  192. """
  193. return app.config['SESSION_COOKIE_HTTPONLY']
  194. def get_cookie_secure(self, app):
  195. """Returns True if the cookie should be secure. This currently
  196. just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
  197. """
  198. return app.config['SESSION_COOKIE_SECURE']
  199. def get_expiration_time(self, app, session):
  200. """A helper method that returns an expiration date for the session
  201. or ``None`` if the session is linked to the browser session. The
  202. default implementation returns now + the permanent session
  203. lifetime configured on the application.
  204. """
  205. if session.permanent:
  206. return datetime.utcnow() + app.permanent_session_lifetime
  207. def should_set_cookie(self, app, session):
  208. """Indicates whether a cookie should be set now or not. This is
  209. used by session backends to figure out if they should emit a
  210. set-cookie header or not. The default behavior is controlled by
  211. the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If
  212. it's set to ``False`` then a cookie is only set if the session is
  213. modified, if set to ``True`` it's always set if the session is
  214. permanent.
  215. This check is usually skipped if sessions get deleted.
  216. .. versionadded:: 0.11
  217. """
  218. if session.modified:
  219. return True
  220. save_each = app.config['SESSION_REFRESH_EACH_REQUEST']
  221. return save_each and session.permanent
  222. def open_session(self, app, request):
  223. """This method has to be implemented and must either return ``None``
  224. in case the loading failed because of a configuration error or an
  225. instance of a session object which implements a dictionary like
  226. interface + the methods and attributes on :class:`SessionMixin`.
  227. """
  228. raise NotImplementedError()
  229. def save_session(self, app, session, response):
  230. """This is called for actual sessions returned by :meth:`open_session`
  231. at the end of the request. This is still called during a request
  232. context so if you absolutely need access to the request you can do
  233. that.
  234. """
  235. raise NotImplementedError()
  236. class SecureCookieSessionInterface(SessionInterface):
  237. """The default session interface that stores sessions in signed cookies
  238. through the :mod:`itsdangerous` module.
  239. """
  240. #: the salt that should be applied on top of the secret key for the
  241. #: signing of cookie based sessions.
  242. salt = 'cookie-session'
  243. #: the hash function to use for the signature. The default is sha1
  244. digest_method = staticmethod(hashlib.sha1)
  245. #: the name of the itsdangerous supported key derivation. The default
  246. #: is hmac.
  247. key_derivation = 'hmac'
  248. #: A python serializer for the payload. The default is a compact
  249. #: JSON derived serializer with support for some extra Python types
  250. #: such as datetime objects or tuples.
  251. serializer = session_json_serializer
  252. session_class = SecureCookieSession
  253. def get_signing_serializer(self, app):
  254. if not app.secret_key:
  255. return None
  256. signer_kwargs = dict(
  257. key_derivation=self.key_derivation,
  258. digest_method=self.digest_method
  259. )
  260. return URLSafeTimedSerializer(app.secret_key, salt=self.salt,
  261. serializer=self.serializer,
  262. signer_kwargs=signer_kwargs)
  263. def open_session(self, app, request):
  264. s = self.get_signing_serializer(app)
  265. if s is None:
  266. return None
  267. val = request.cookies.get(app.session_cookie_name)
  268. if not val:
  269. return self.session_class()
  270. max_age = total_seconds(app.permanent_session_lifetime)
  271. try:
  272. data = s.loads(val, max_age=max_age)
  273. return self.session_class(data)
  274. except BadSignature:
  275. return self.session_class()
  276. def save_session(self, app, session, response):
  277. domain = self.get_cookie_domain(app)
  278. path = self.get_cookie_path(app)
  279. # Delete case. If there is no session we bail early.
  280. # If the session was modified to be empty we remove the
  281. # whole cookie.
  282. if not session:
  283. if session.modified:
  284. response.delete_cookie(app.session_cookie_name,
  285. domain=domain, path=path)
  286. return
  287. # Modification case. There are upsides and downsides to
  288. # emitting a set-cookie header each request. The behavior
  289. # is controlled by the :meth:`should_set_cookie` method
  290. # which performs a quick check to figure out if the cookie
  291. # should be set or not. This is controlled by the
  292. # SESSION_REFRESH_EACH_REQUEST config flag as well as
  293. # the permanent flag on the session itself.
  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. expires = self.get_expiration_time(app, session)
  299. val = self.get_signing_serializer(app).dumps(dict(session))
  300. response.set_cookie(app.session_cookie_name, val,
  301. expires=expires, httponly=httponly,
  302. domain=domain, path=path, secure=secure)