1
# -*- coding: utf-8 -*-
3
werkzeug.contrib.sessions
4
~~~~~~~~~~~~~~~~~~~~~~~~~
6
This module contains some helper classes that help one to add session
7
support to a python WSGI application. For full client-side session
8
storage see :mod:`~werkzeug.contrib.securecookie` which implements a
9
secure, client-side session storage.
12
Application Integration
13
=======================
17
from werkzeug.contrib.sessions import SessionMiddleware, \
18
FilesystemSessionStore
20
app = SessionMiddleware(app, FilesystemSessionStore())
22
The current session will then appear in the WSGI environment as
23
`werkzeug.session`. However it's recommended to not use the middleware
24
but the stores directly in the application. However for very simple
25
scripts a middleware for sessions could be sufficient.
27
This module does not implement methods or ways to check if a session is
28
expired. That should be done by a cronjob and storage specific. For
29
example to prune unused filesystem sessions one could check the modified
30
time of the files. It sessions are stored in the database the new()
31
method should add an expiration timestamp for the session.
33
For better flexibility it's recommended to not use the middleware but the
34
store and session object directly in the application dispatching::
36
session_store = FilesystemSessionStore()
38
def application(environ, start_response):
39
request = Request(environ)
40
sid = request.cookie.get('cookie_name')
42
request.session = session_store.new()
44
request.session = session_store.get(sid)
45
response = get_the_response_object(request)
46
if request.session.should_save:
47
session_store.save(request.session)
48
response.set_cookie('cookie_name', request.session.sid)
49
return response(environ, start_response)
51
:copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
52
:license: BSD, see LICENSE for more details.
58
from random import random
60
from hashlib import sha1
62
from sha import new as sha1
63
from cPickle import dump, load, HIGHEST_PROTOCOL
65
from werkzeug.utils import ClosingIterator, dump_cookie, parse_cookie
66
from werkzeug.datastructures import CallbackDict
69
_sha1_re = re.compile(r'^[a-fA-F0-9]{40}$')
73
if hasattr(os, 'urandom'):
78
def generate_key(salt=None):
79
return sha1('%s%s%s' % (salt, time(), _urandom())).hexdigest()
82
class ModificationTrackingDict(CallbackDict):
83
__slots__ = ('modified',)
85
def __init__(self, *args, **kwargs):
89
CallbackDict.__init__(self, on_update=on_update)
90
dict.update(self, *args, **kwargs)
93
"""Create a flat copy of the dict."""
95
result = object.__new__(self.__class__)
96
for name in self.__slots__:
97
val = getattr(self, name, missing)
98
if val is not missing:
99
setattr(result, name, val)
106
class Session(ModificationTrackingDict):
107
"""Subclass of a dict that keeps track of direct object changes. Changes
108
in mutable structures are not tracked, for those you have to set
109
`modified` to `True` by hand.
111
__slots__ = ModificationTrackingDict.__slots__ + ('sid', 'new')
113
def __init__(self, data, sid, new=False):
114
ModificationTrackingDict.__init__(self, data)
119
return '<%s %s%s>' % (
120
self.__class__.__name__,
122
self.should_save and '*' or ''
126
def should_save(self):
127
"""True if the session should be saved."""
128
return self.modified or self.new
131
class SessionStore(object):
132
"""Baseclass for all session stores. The Werkzeug contrib module does not
133
implement any useful stores besides the filesystem store, application
134
developers are encouraged to create their own stores.
136
:param session_class: The session class to use. Defaults to
140
def __init__(self, session_class=None):
141
if session_class is None:
142
session_class = Session
143
self.session_class = session_class
145
def is_valid_key(self, key):
146
"""Check if a key has the correct format."""
147
return _sha1_re.match(key) is not None
149
def generate_key(self, salt=None):
150
"""Simple function that generates a new session key."""
151
return generate_key(salt)
154
"""Generate a new session."""
155
return self.session_class({}, self.generate_key(), True)
157
def save(self, session):
158
"""Save a session."""
160
def save_if_modified(self, session):
161
"""Save if a session class wants an update."""
162
if session.should_save:
165
def delete(self, session):
166
"""Delete a session."""
169
"""Get a session for this sid or a new session object. This method
170
has to check if the session key is valid and create a new session if
171
that wasn't the case.
173
return self.session_class({}, sid, True)
176
class FilesystemSessionStore(SessionStore):
177
"""Simple example session store that saves sessions in the filesystem like
180
:param path: the path to the folder used for storing the sessions.
181
If not provided the default temporary directory is used.
182
:param filename_template: a string template used to give the session
183
a filename. ``%s`` is replaced with the
185
:param session_class: The session class to use. Defaults to
189
def __init__(self, path=None, filename_template='werkzeug_%s.sess',
191
SessionStore.__init__(self, session_class)
193
from tempfile import gettempdir
196
self.filename_template = filename_template
198
def get_session_filename(self, sid):
199
return path.join(self.path, self.filename_template % sid)
201
def save(self, session):
202
f = file(self.get_session_filename(session.sid), 'wb')
204
dump(dict(session), f, HIGHEST_PROTOCOL)
208
def delete(self, session):
209
fn = self.get_session_filename(session.sid)
211
# Late import because Google Appengine won't allow os.unlink
212
from os import unlink
218
fn = self.get_session_filename(sid)
219
if not self.is_valid_key(sid) or not path.exists(fn):
227
return self.session_class(data, sid, False)
230
class SessionMiddleware(object):
231
"""A simple middleware that puts the session object of a store provided
232
into the WSGI environ. It automatically sets cookies and restores
235
However a middleware is not the preferred solution because it won't be as
236
fast as sessions managed by the application itself and will put a key into
237
the WSGI environment only relevant for the application which is against
240
The cookie parameters are the same as for the :func:`~werkzeug.dump_cookie`
241
function just prefixed with ``cookie_``. Additionally `max_age` is
242
called `cookie_age` and not `cookie_max_age` because of backwards
246
def __init__(self, app, store, cookie_name='session_id',
247
cookie_age=None, cookie_expires=None, cookie_path='/',
248
cookie_domain=None, cookie_secure=None,
249
cookie_httponly=False, environ_key='werkzeug.session'):
252
self.cookie_name = cookie_name
253
self.cookie_age = cookie_age
254
self.cookie_expires = cookie_expires
255
self.cookie_path = cookie_path
256
self.cookie_domain = cookie_domain
257
self.cookie_secure = cookie_secure
258
self.cookie_httponly = cookie_httponly
259
self.environ_key = environ_key
261
def __call__(self, environ, start_response):
262
cookie = parse_cookie(environ.get('HTTP_COOKIE', ''))
263
sid = cookie.get(self.cookie_name, None)
265
session = self.store.new()
267
session = self.store.get(sid)
268
environ[self.environ_key] = session
270
def injecting_start_response(status, headers, exc_info=None):
271
if session.should_save:
272
self.store.save(session)
273
headers.append(('Set-Cookie', dump_cookie(self.cookie_name,
274
session.sid, self.cookie_age,
275
self.cookie_expires, self.cookie_path,
276
self.cookie_domain, self.cookie_secure,
277
self.cookie_httponly)))
278
return start_response(status, headers, exc_info)
279
return ClosingIterator(self.app(environ, injecting_start_response),
280
lambda: self.store.save_if_modified(session))