Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 295 lines (243 sloc) 11.522 kb
10c99e9 @mitsuhiko Added missing sessions module
authored
1 # -*- coding: utf-8 -*-
2 """
3 flask.sessions
4 ~~~~~~~~~~~~~~
5
261c4a6 @mitsuhiko Updated documentation for the new sessions
authored
6 Implements cookie based sessions based on itsdangerous.
10c99e9 @mitsuhiko Added missing sessions module
authored
7
261c4a6 @mitsuhiko Updated documentation for the new sessions
authored
8 :copyright: (c) 2012 by Armin Ronacher.
10c99e9 @mitsuhiko Added missing sessions module
authored
9 :license: BSD, see LICENSE for more details.
10 """
11
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
12 import hashlib
10c99e9 @mitsuhiko Added missing sessions module
authored
13 from datetime import datetime
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
14 from werkzeug.http import http_date, parse_date
3f82d1b @mitsuhiko Switch to itsdangerous
authored
15 from werkzeug.datastructures import CallbackDict
b146d82 @mitsuhiko Added wrapper module around simplejson/json for much simplified custo…
authored
16 from . import Markup, json
10c99e9 @mitsuhiko Added missing sessions module
authored
17
3f82d1b @mitsuhiko Switch to itsdangerous
authored
18 from itsdangerous import URLSafeTimedSerializer, BadSignature
19
10c99e9 @mitsuhiko Added missing sessions module
authored
20
de5038f @mitsuhiko Added total_seconds() helper for pythons before 2.7
authored
21 def total_seconds(td):
22 return td.days * 60 * 60 * 24 + td.seconds
23
24
10c99e9 @mitsuhiko Added missing sessions module
authored
25 class SessionMixin(object):
26 """Expands a basic dictionary with an accessors that are expected
27 by Flask extensions and users for the session.
28 """
29
30 def _get_permanent(self):
31 return self.get('_permanent', False)
32
33 def _set_permanent(self, value):
34 self['_permanent'] = bool(value)
35
7290981 @mitsuhiko More docstrings for the session mixin
authored
36 #: this reflects the ``'_permanent'`` key in the dict.
10c99e9 @mitsuhiko Added missing sessions module
authored
37 permanent = property(_get_permanent, _set_permanent)
38 del _get_permanent, _set_permanent
39
7290981 @mitsuhiko More docstrings for the session mixin
authored
40 #: some session backends can tell you if a session is new, but that is
10f8dc1 @mitsuhiko More doc updates
authored
41 #: not necessarily guaranteed. Use with caution. The default mixin
42 #: implementation just hardcodes `False` in.
10c99e9 @mitsuhiko Added missing sessions module
authored
43 new = False
7290981 @mitsuhiko More docstrings for the session mixin
authored
44
45 #: for some backends this will always be `True`, but some backends will
46 #: default this to false and detect changes in the dictionary for as
47 #: long as changes do not happen on mutable structures in the session.
10f8dc1 @mitsuhiko More doc updates
authored
48 #: The default mixin implementation just hardcodes `True` in.
10c99e9 @mitsuhiko Added missing sessions module
authored
49 modified = True
50
51
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
52 class TaggedJSONSerializer(object):
53 """A customized JSON serializer that supports a few extra types that
54 we take for granted when serializing (tuples, markup objects, datetime).
55 """
56
57 def dumps(self, value):
58 def _tag(value):
59 if isinstance(value, tuple):
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
60 return {' t': [_tag(x) for x in value]}
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
61 elif callable(getattr(value, '__html__', None)):
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
62 return {' m': unicode(value.__html__())}
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
63 elif isinstance(value, list):
64 return [_tag(x) for x in value]
65 elif isinstance(value, datetime):
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
66 return {' d': http_date(value)}
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
67 elif isinstance(value, dict):
68 return dict((k, _tag(v)) for k, v in value.iteritems())
69 return value
70 return json.dumps(_tag(value), separators=(',', ':'))
71
72 def loads(self, value):
73 def object_hook(obj):
74 if len(obj) != 1:
75 return obj
76 the_key, the_value = obj.iteritems().next()
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
77 if the_key == ' t':
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
78 return tuple(the_value)
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
79 elif the_key == ' m':
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
80 return Markup(the_value)
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
81 elif the_key == ' d':
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
82 return parse_date(the_value)
83 return obj
84 return json.loads(value, object_hook=object_hook)
85
86
87 session_json_serializer = TaggedJSONSerializer()
88
89
3f82d1b @mitsuhiko Switch to itsdangerous
authored
90 class SecureCookieSession(CallbackDict, SessionMixin):
91 """Baseclass for sessions based on signed cookies."""
4df3bf2 @mitsuhiko Implemented experimental JSON based sessions
authored
92
3f82d1b @mitsuhiko Switch to itsdangerous
authored
93 def __init__(self, initial=None):
94 def on_update(self):
95 self.modified = True
96 CallbackDict.__init__(self, initial, on_update)
97 self.modified = False
10c99e9 @mitsuhiko Added missing sessions module
authored
98
99
100 class NullSession(SecureCookieSession):
101 """Class used to generate nicer error messages if sessions are not
102 available. Will still allow read-only access to the empty session
103 but fail on setting.
104 """
105
106 def _fail(self, *args, **kwargs):
107 raise RuntimeError('the session is unavailable because no secret '
108 'key was set. Set the secret_key on the '
109 'application to something unique and secret.')
110 __setitem__ = __delitem__ = clear = pop = popitem = \
111 update = setdefault = _fail
112 del _fail
113
114
115 class SessionInterface(object):
116 """The basic interface you have to implement in order to replace the
117 default session interface which uses werkzeug's securecookie
118 implementation. The only methods you have to implement are
119 :meth:`open_session` and :meth:`save_session`, the others have
120 useful defaults which you don't need to change.
121
122 The session object returned by the :meth:`open_session` method has to
123 provide a dictionary like interface plus the properties and methods
124 from the :class:`SessionMixin`. We recommend just subclassing a dict
125 and adding that mixin::
126
127 class Session(dict, SessionMixin):
128 pass
129
130 If :meth:`open_session` returns `None` Flask will call into
131 :meth:`make_null_session` to create a session that acts as replacement
132 if the session support cannot work because some requirement is not
133 fulfilled. The default :class:`NullSession` class that is created
134 will complain that the secret key was not set.
135
136 To replace the session interface on an application all you have to do
137 is to assign :attr:`flask.Flask.session_interface`::
138
139 app = Flask(__name__)
140 app.session_interface = MySessionInterface()
141
deb513c @mitsuhiko The session interface is new in 0.8, not 0.7
authored
142 .. versionadded:: 0.8
10c99e9 @mitsuhiko Added missing sessions module
authored
143 """
144
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
150
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
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
157
10c99e9 @mitsuhiko Added missing sessions module
authored
158 def make_null_session(self, app):
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.
165
166 This creates an instance of :attr:`null_session_class` by default.
167 """
168 return self.null_session_class()
169
170 def is_null_session(self, obj):
171 """Checks if a given object is a null session. Null sessions are
172 not asked to be saved.
173
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)
178
179 def get_cookie_domain(self, app):
180 """Helpful helper method that returns the cookie domain that should
181 be used for the session cookie if session cookies are used.
182 """
ccf4641 @mitsuhiko Added finer control over the session cookie parameters
authored
183 if app.config['SESSION_COOKIE_DOMAIN'] is not None:
184 return app.config['SESSION_COOKIE_DOMAIN']
10c99e9 @mitsuhiko Added missing sessions module
authored
185 if app.config['SERVER_NAME'] is not None:
186 # chop of the port which is usually not supported by browsers
187 return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
188
c844d02 @mitsuhiko Added the APPLICATION_ROOT configuration variable which is used by se…
authored
189 def get_cookie_path(self, app):
190 """Returns the path for which the cookie should be valid. The
ccf4641 @mitsuhiko Added finer control over the session cookie parameters
authored
191 default implementation uses the value from the SESSION_COOKIE_PATH``
192 config var if it's set, and falls back to ``APPLICATION_ROOT`` or
193 uses ``/`` if it's `None`.
c844d02 @mitsuhiko Added the APPLICATION_ROOT configuration variable which is used by se…
authored
194 """
ccf4641 @mitsuhiko Added finer control over the session cookie parameters
authored
195 return app.config['SESSION_COOKIE_PATH'] or \
196 app.config['APPLICATION_ROOT'] or '/'
197
198 def get_cookie_httponly(self, app):
199 """Returns True if the session cookie should be httponly. This
200 currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
201 config var.
202 """
203 return app.config['SESSION_COOKIE_HTTPONLY']
204
205 def get_cookie_secure(self, app):
206 """Returns True if the cookie should be secure. This currently
207 just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
208 """
209 return app.config['SESSION_COOKIE_SECURE']
c844d02 @mitsuhiko Added the APPLICATION_ROOT configuration variable which is used by se…
authored
210
10c99e9 @mitsuhiko Added missing sessions module
authored
211 def get_expiration_time(self, app, session):
212 """A helper method that returns an expiration date for the session
213 or `None` if the session is linked to the browser session. The
214 default implementation returns now + the permanent session
215 lifetime configured on the application.
216 """
217 if session.permanent:
218 return datetime.utcnow() + app.permanent_session_lifetime
219
220 def open_session(self, app, request):
221 """This method has to be implemented and must either return `None`
222 in case the loading failed because of a configuration error or an
223 instance of a session object which implements a dictionary like
224 interface + the methods and attributes on :class:`SessionMixin`.
225 """
226 raise NotImplementedError()
227
228 def save_session(self, app, session, response):
229 """This is called for actual sessions returned by :meth:`open_session`
230 at the end of the request. This is still called during a request
231 context so if you absolutely need access to the request you can do
232 that.
233 """
234 raise NotImplementedError()
235
236
237 class SecureCookieSessionInterface(SessionInterface):
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
238 """The default session interface that stores sessions in signed cookies
239 through the :mod:`itsdangerous` module.
240 """
241 #: the salt that should be applied on top of the secret key for the
242 #: signing of cookie based sessions.
3f82d1b @mitsuhiko Switch to itsdangerous
authored
243 salt = 'cookie-session'
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
244 #: the hash function to use for the signature. The default is sha1
245 digest_method = staticmethod(hashlib.sha1)
246 #: the name of the itsdangerous supported key derivation. The default
247 #: is hmac.
248 key_derivation = 'hmac'
249 #: A python serializer for the payload. The default is a compact
250 #: JSON derived serializer with support for some extra Python types
251 #: such as datetime objects or tuples.
3f82d1b @mitsuhiko Switch to itsdangerous
authored
252 serializer = session_json_serializer
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
253 session_class = SecureCookieSession
3f82d1b @mitsuhiko Switch to itsdangerous
authored
254
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
255 def get_signing_serializer(self, app):
3f82d1b @mitsuhiko Switch to itsdangerous
authored
256 if not app.secret_key:
257 return None
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
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)
10c99e9 @mitsuhiko Added missing sessions module
authored
265
266 def open_session(self, app, request):
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
267 s = self.get_signing_serializer(app)
3f82d1b @mitsuhiko Switch to itsdangerous
authored
268 if s is None:
269 return None
270 val = request.cookies.get(app.session_cookie_name)
271 if not val:
272 return self.session_class()
de5038f @mitsuhiko Added total_seconds() helper for pythons before 2.7
authored
273 max_age = total_seconds(app.permanent_session_lifetime)
3f82d1b @mitsuhiko Switch to itsdangerous
authored
274 try:
275 data = s.loads(val, max_age=max_age)
276 return self.session_class(data)
277 except BadSignature:
278 return self.session_class()
10c99e9 @mitsuhiko Added missing sessions module
authored
279
280 def save_session(self, app, session, response):
281 domain = self.get_cookie_domain(app)
c844d02 @mitsuhiko Added the APPLICATION_ROOT configuration variable which is used by se…
authored
282 path = self.get_cookie_path(app)
3f82d1b @mitsuhiko Switch to itsdangerous
authored
283 if not session:
284 if session.modified:
285 response.delete_cookie(app.session_cookie_name,
286 domain=domain, path=path)
287 return
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
288 httponly = self.get_cookie_httponly(app)
289 secure = self.get_cookie_secure(app)
3f82d1b @mitsuhiko Switch to itsdangerous
authored
290 expires = self.get_expiration_time(app, session)
fe85970 @mitsuhiko Various improvements in regards to the itsdangerous usage, bumped to …
authored
291 val = self.get_signing_serializer(app).dumps(dict(session))
3f82d1b @mitsuhiko Switch to itsdangerous
authored
292 response.set_cookie(app.session_cookie_name, val,
293 expires=expires, httponly=httponly,
294 domain=domain, path=path, secure=secure)
Something went wrong with that request. Please try again.