Back to index

moin  1.9.0~rc2
sessions.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 r"""
00003     werkzeug.contrib.sessions
00004     ~~~~~~~~~~~~~~~~~~~~~~~~~
00005 
00006     This module contains some helper classes that help one to add session
00007     support to a python WSGI application.  For full client-side session
00008     storage see :mod:`~werkzeug.contrib.securecookie` which implements a
00009     secure, client-side session storage.
00010 
00011 
00012     Application Integration
00013     =======================
00014 
00015     ::
00016 
00017         from werkzeug.contrib.sessions import SessionMiddleware, \
00018              FilesystemSessionStore
00019 
00020         app = SessionMiddleware(app, FilesystemSessionStore())
00021 
00022     The current session will then appear in the WSGI environment as
00023     `werkzeug.session`.  However it's recommended to not use the middleware
00024     but the stores directly in the application.  However for very simple
00025     scripts a middleware for sessions could be sufficient.
00026 
00027     This module does not implement methods or ways to check if a session is
00028     expired.  That should be done by a cronjob and storage specific.  For
00029     example to prune unused filesystem sessions one could check the modified
00030     time of the files.  It sessions are stored in the database the new()
00031     method should add an expiration timestamp for the session.
00032 
00033     For better flexibility it's recommended to not use the middleware but the
00034     store and session object directly in the application dispatching::
00035 
00036         session_store = FilesystemSessionStore()
00037 
00038         def application(environ, start_response):
00039             request = Request(environ)
00040             sid = request.cookie.get('cookie_name')
00041             if sid is None:
00042                 request.session = session_store.new()
00043             else:
00044                 request.session = session_store.get(sid)
00045             response = get_the_response_object(request)
00046             if request.session.should_save:
00047                 session_store.save(request.session)
00048                 response.set_cookie('cookie_name', request.session.sid)
00049             return response(environ, start_response)
00050 
00051     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00052     :license: BSD, see LICENSE for more details.
00053 """
00054 import re
00055 import os
00056 from os import path
00057 from time import time
00058 from random import random
00059 try:
00060     from hashlib import sha1
00061 except ImportError:
00062     from sha import new as sha1
00063 from cPickle import dump, load, HIGHEST_PROTOCOL
00064 
00065 from werkzeug.utils import ClosingIterator, dump_cookie, parse_cookie
00066 from werkzeug.datastructures import CallbackDict
00067 
00068 
00069 _sha1_re = re.compile(r'^[a-fA-F0-9]{40}$')
00070 
00071 
00072 def _urandom():
00073     if hasattr(os, 'urandom'):
00074         return os.urandom(30)
00075     return random()
00076 
00077 
00078 def generate_key(salt=None):
00079     return sha1('%s%s%s' % (salt, time(), _urandom())).hexdigest()
00080 
00081 
00082 class ModificationTrackingDict(CallbackDict):
00083     __slots__ = ('modified',)
00084 
00085     def __init__(self, *args, **kwargs):
00086         def on_update(self):
00087             self.modified = True
00088         self.modified = False
00089         CallbackDict.__init__(self, on_update=on_update)
00090         dict.update(self, *args, **kwargs)
00091 
00092     def copy(self):
00093         """Create a flat copy of the dict."""
00094         missing = object()
00095         result = object.__new__(self.__class__)
00096         for name in self.__slots__:
00097             val = getattr(self, name, missing)
00098             if val is not missing:
00099                 setattr(result, name, val)
00100         return result
00101 
00102     def __copy__(self):
00103         return self.copy()
00104 
00105 
00106 class Session(ModificationTrackingDict):
00107     """Subclass of a dict that keeps track of direct object changes.  Changes
00108     in mutable structures are not tracked, for those you have to set
00109     `modified` to `True` by hand.
00110     """
00111     __slots__ = ModificationTrackingDict.__slots__ + ('sid', 'new')
00112 
00113     def __init__(self, data, sid, new=False):
00114         ModificationTrackingDict.__init__(self, data)
00115         self.sid = sid
00116         self.new = new
00117 
00118     def __repr__(self):
00119         return '<%s %s%s>' % (
00120             self.__class__.__name__,
00121             dict.__repr__(self),
00122             self.should_save and '*' or ''
00123         )
00124 
00125     @property
00126     def should_save(self):
00127         """True if the session should be saved."""
00128         return self.modified or self.new
00129 
00130 
00131 class SessionStore(object):
00132     """Baseclass for all session stores.  The Werkzeug contrib module does not
00133     implement any useful stores besides the filesystem store, application
00134     developers are encouraged to create their own stores.
00135 
00136     :param session_class: The session class to use.  Defaults to
00137                           :class:`Session`.
00138     """
00139 
00140     def __init__(self, session_class=None):
00141         if session_class is None:
00142             session_class = Session
00143         self.session_class = session_class
00144 
00145     def is_valid_key(self, key):
00146         """Check if a key has the correct format."""
00147         return _sha1_re.match(key) is not None
00148 
00149     def generate_key(self, salt=None):
00150         """Simple function that generates a new session key."""
00151         return generate_key(salt)
00152 
00153     def new(self):
00154         """Generate a new session."""
00155         return self.session_class({}, self.generate_key(), True)
00156 
00157     def save(self, session):
00158         """Save a session."""
00159 
00160     def save_if_modified(self, session):
00161         """Save if a session class wants an update."""
00162         if session.should_save:
00163             self.save(session)
00164 
00165     def delete(self, session):
00166         """Delete a session."""
00167 
00168     def get(self, sid):
00169         """Get a session for this sid or a new session object.  This method
00170         has to check if the session key is valid and create a new session if
00171         that wasn't the case.
00172         """
00173         return self.session_class({}, sid, True)
00174 
00175 
00176 class FilesystemSessionStore(SessionStore):
00177     """Simple example session store that saves sessions in the filesystem like
00178     PHP does.
00179 
00180     :param path: the path to the folder used for storing the sessions.
00181                  If not provided the default temporary directory is used.
00182     :param filename_template: a string template used to give the session
00183                               a filename.  ``%s`` is replaced with the
00184                               session id.
00185     :param session_class: The session class to use.  Defaults to
00186                           :class:`Session`.
00187     """
00188 
00189     def __init__(self, path=None, filename_template='werkzeug_%s.sess',
00190                  session_class=None):
00191         SessionStore.__init__(self, session_class)
00192         if path is None:
00193             from tempfile import gettempdir
00194             path = gettempdir()
00195         self.path = path
00196         self.filename_template = filename_template
00197 
00198     def get_session_filename(self, sid):
00199         return path.join(self.path, self.filename_template % sid)
00200 
00201     def save(self, session):
00202         f = file(self.get_session_filename(session.sid), 'wb')
00203         try:
00204             dump(dict(session), f, HIGHEST_PROTOCOL)
00205         finally:
00206             f.close()
00207 
00208     def delete(self, session):
00209         fn = self.get_session_filename(session.sid)
00210         try:
00211             # Late import because Google Appengine won't allow os.unlink
00212             from os import unlink
00213             unlink(fn)
00214         except OSError:
00215             pass
00216 
00217     def get(self, sid):
00218         fn = self.get_session_filename(sid)
00219         if not self.is_valid_key(sid) or not path.exists(fn):
00220             return self.new()
00221         else:
00222             f = file(fn, 'rb')
00223             try:
00224                 data = load(f)
00225             finally:
00226                 f.close()
00227         return self.session_class(data, sid, False)
00228 
00229 
00230 class SessionMiddleware(object):
00231     """A simple middleware that puts the session object of a store provided
00232     into the WSGI environ.  It automatically sets cookies and restores
00233     sessions.
00234 
00235     However a middleware is not the preferred solution because it won't be as
00236     fast as sessions managed by the application itself and will put a key into
00237     the WSGI environment only relevant for the application which is against
00238     the concept of WSGI.
00239 
00240     The cookie parameters are the same as for the :func:`~werkzeug.dump_cookie`
00241     function just prefixed with ``cookie_``.  Additionally `max_age` is
00242     called `cookie_age` and not `cookie_max_age` because of backwards
00243     compatibility.
00244     """
00245 
00246     def __init__(self, app, store, cookie_name='session_id',
00247                  cookie_age=None, cookie_expires=None, cookie_path='/',
00248                  cookie_domain=None, cookie_secure=None,
00249                  cookie_httponly=False, environ_key='werkzeug.session'):
00250         self.app = app
00251         self.store = store
00252         self.cookie_name = cookie_name
00253         self.cookie_age = cookie_age
00254         self.cookie_expires = cookie_expires
00255         self.cookie_path = cookie_path
00256         self.cookie_domain = cookie_domain
00257         self.cookie_secure = cookie_secure
00258         self.cookie_httponly = cookie_httponly
00259         self.environ_key = environ_key
00260 
00261     def __call__(self, environ, start_response):
00262         cookie = parse_cookie(environ.get('HTTP_COOKIE', ''))
00263         sid = cookie.get(self.cookie_name, None)
00264         if sid is None:
00265             session = self.store.new()
00266         else:
00267             session = self.store.get(sid)
00268         environ[self.environ_key] = session
00269 
00270         def injecting_start_response(status, headers, exc_info=None):
00271             if session.should_save:
00272                 self.store.save(session)
00273                 headers.append(('Set-Cookie', dump_cookie(self.cookie_name,
00274                                 session.sid, self.cookie_age,
00275                                 self.cookie_expires, self.cookie_path,
00276                                 self.cookie_domain, self.cookie_secure,
00277                                 self.cookie_httponly)))
00278             return start_response(status, headers, exc_info)
00279         return ClosingIterator(self.app(environ, injecting_start_response),
00280                                lambda: self.store.save_if_modified(session))