CherryPy / lib /

"""Session implementation for CherryPy.

We use cherrypy.request to store some convenient variables as
well as data about the session for the current request. Instead of
polluting cherrypy.request we use a Session object bound to
cherrypy.session to store these variables.

import datetime
import os
    import cPickle as pickle
except ImportError:
    import pickle
import random
import sha
import time
import threading
import types

import cherrypy
from cherrypy.lib import http

class Session(object):
    """A CherryPy dict-like Session object (one per request).
    id: current session ID.
    expiration_time (datetime): when the current session will expire.
    timeout (minutes): used to calculate expiration_time from now.
    clean_freq (minutes): the poll rate for expired session cleanup.
    locked: If True, this session instance has exclusive read/write access
        to session data.
    loaded: If True, data has been retrieved from storage. This should
        happen automatically on the first attempt to access session data.
    clean_thread = None
    def __init__(self, id=None, **kwargs):
        self.locked = False
        self.loaded = False
        self._data = {}
        for k, v in kwargs.iteritems():
            setattr(self, k, v)
        = id
        while is None:
   = self.generate_id()
            # Assert that the generated id is not already stored.
            if self._load() is not None:
       = None
    def clean_cycle(self):
        """Clean up expired sessions at regular intervals."""
        # clean_thread is both a cancelable Timer and a flag.
        if self.clean_thread:
            t = threading.Timer(self.clean_freq, self.clean_cycle)
            self.__class__.clean_thread = t
    def clean_interrupt(cls):
        """Stop the expired-session cleaning cycle."""
        if cls.clean_thread:
            cls.clean_thread = None
    clean_interrupt = classmethod(clean_interrupt)
    def clean_up(self):
        """Clean up expired sessions."""
    except (AttributeError, NotImplementedError):
        # os.urandom not available until Python 2.4. Fall back to random.random.
        def generate_id(self):
            """Return a new session id."""
            return'%s' % random.random()).hexdigest()
        def generate_id(self):
            """Return a new session id."""
            return os.urandom(20).encode('hex')
    def save(self):
        """Save session data."""
        # If session data has never been loaded then it's never been
        #   accessed: no need to delete it
        if self.loaded:
            t = datetime.timedelta(seconds = self.timeout * 60)
            expiration_time = + t
        if self.locked:
            # Always release the lock if the user didn't release it
    def load(self):
        """Copy stored session data into this session instance."""
        data = self._load()
        # data is either None or a tuple (session_data, expiration_time)
        if data is None or data[1] <
            # Expired session: flush session data (but keep the same id)
            self._data = {}
            self._data = data[0]
        self.loaded = True
        cls = self.__class__
        if not cls.clean_thread:
            # Use the instance to call clean_cycle so tool config
            # can be accessed inside the method.
            cls.clean_thread = t = threading.Timer(self.clean_freq,
    def __getitem__(self, key):
        if not self.loaded: self.load()
        return self._data[key]
    def __setitem__(self, key, value):
        if not self.loaded: self.load()
        self._data[key] = value
    def __delitem__(self, key):
        if not self.loaded: self.load()
        del self._data[key]
    def __contains__(self, key):
        if not self.loaded: self.load()
        return key in self._data
    def has_key(self, key):
        if not self.loaded: self.load()
        return self._data.has_key(key)
    def get(self, key, default=None):
        if not self.loaded: self.load()
        return self._data.get(key, default)
    def update(self, d):
        if not self.loaded: self.load()
    def setdefault(self, key, default=None):
        if not self.loaded: self.load()
        return self._data.setdefault(key, default)
    def clear(self):
        if not self.loaded: self.load()
    def keys(self):
        if not self.loaded: self.load()
        return self._data.keys()
    def items(self):
        if not self.loaded: self.load()
        return self._data.items()
    def values(self):
        if not self.loaded: self.load()
        return self._data.values()

class RamSession(Session):
    # Class-level objects. Don't rebind these!
    cache = {}
    locks = {}
    def clean_up(self):
        """Clean up expired sessions."""
        now =
        for id, (data, expiration_time) in self.cache.items():
            if expiration_time < now:
                    del self.cache[id]
                except KeyError:
    def _load(self):
        return self.cache.get(
    def _save(self, expiration_time):
        self.cache[] = (self._data, expiration_time)
    def acquire_lock(self):
        self.locked = True
        self.locks.setdefault(, threading.Semaphore()).acquire()
    def release_lock(self):
        self.locked = False

class FileSession(Session):
    """ Implementation of the File backend for sessions """
    SESSION_PREFIX = 'session-'
    LOCK_SUFFIX = '.lock'
    def _get_file_path(self):
        return os.path.join(self.storage_path, self.SESSION_PREFIX +
    def _load(self, path=None):
        if path is None:
            path = self._get_file_path()
            f = open(path, "rb")
                return pickle.load(f)
        except (IOError, EOFError):
            return None
    def _save(self, expiration_time):
        f = open(self._get_file_path(), "wb")
            pickle.dump((self._data, expiration_time), f)
    def acquire_lock(self, path=None):
        if path is None:
            path = self._get_file_path()
        path += self.LOCK_SUFFIX
        while True:
                lockfd =, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
            except OSError:
        self.locked = True
    def release_lock(self, path=None):
        if path is None:
            path = self._get_file_path()
        os.unlink(path + self.LOCK_SUFFIX)
        self.locked = False
    def clean_up(self):
        """Clean up expired sessions."""
        now =
        # Iterate over all session files in self.storage_path
        for fname in os.listdir(self.storage_path):
            if (fname.startswith(self.SESSION_PREFIX)
                and not fname.endswith(self.LOCK_SUFFIX)):
                # We have a session file: lock and load it and check
                #   if it's expired. If it fails, nevermind.
                path = os.path.join(self.storage_path, fname)
                    contents = self._load(path)
                    # _load returns None on IOError
                    if contents is not None:
                        data, expiration_time = contents
                        if expiration_time < now:
                            # Session expired: deleting it

class PostgresqlSession(Session):
    """ Implementation of the PostgreSQL backend for sessions. It assumes
        a table like this:

            create table session (
                id varchar(40),
                data text,
                expiration_time timestamp
    You must provide your own get_db function.
    def __init__(self):
        self.db = self.get_db()
        self.cursor = self.db.cursor()
    def __del__(self):
        if self.cursor:
    def _load(self):
        # Select session data from table
        self.cursor.execute('select data, expiration_time from session '
                            'where id=%s', (,))
        rows = self.cursor.fetchall()
        if not rows:
            return None
        pickled_data, expiration_time = rows[0]
        data = pickle.loads(pickled_data)
        return data, expiration_time
    def _save(self, expiration_time):
        self.cursor.execute('delete from session where id=%s', (,))
        pickled_data = pickle.dumps(self._data)
            'insert into session (id, data, expiration_time) values (%s, %s, %s)',
            (, pickled_data, expiration_time))
    def acquire_lock(self):
        # We use the "for update" clause to lock the row
        self.cursor.execute('select id from session where id=%s for update',
    def release_lock(self):
        # We just close the cursor and that will remove the lock
        #   introduced by the "for update" clause
    def clean_up(self):
        """Clean up expired sessions."""
        self.cursor.execute('delete from session where expiration_time < %s',

# Hook functions (for CherryPy tools)

def save():
    """Save any changed session data."""
    def wrap_body(body):
        """Response.body wrapper which saves session data."""
        if isinstance(body, types.GeneratorType):
            # If the body is a generator, we have to save the data
            #   *after* the generator has been consumed
            for line in body:
                yield line
            # If the body is not a generator, we save the data
            #   before the body is returned (so we can release the lock).
            for line in body:
                yield line
    cherrypy.response.body = wrap_body(cherrypy.response.body)

def close():
    """Close the session object for this request."""
    sess = cherrypy.session
    if sess.locked:
        # If the session is still locked we release the lock

def init(storage_type='ram', path=None, path_header=None, name='session_id',
         timeout=60, domain=None, secure=False, locking='implicit',
         clean_freq=5, **kwargs):
    """Initialize session object (using cookies).
    Any additional kwargs will be bound to the new Session instance.
    request = cherrypy.request
    # Check if request came with a session ID
    id = None
    if name in request.simple_cookie:
        id = request.simple_cookie[name].value
    if not hasattr(cherrypy, "session"):
        cherrypy.session = cherrypy._ThreadLocalProxy('session')
    # Create and attach a new Session instance to cherrypy.request.
    # It will possess a reference to (and lock, and lazily load)
    # the requested session data.
    storage_class = storage_type.title() + 'Session'
    kwargs['timeout'] = timeout
    kwargs['clean_freq'] = clean_freq
    cherrypy.serving.session = sess = globals()[storage_class](id, **kwargs)
    if locking == 'implicit':
    # Set response cookie
    cookie = cherrypy.response.simple_cookie
    cookie[name] =
    cookie[name]['path'] = path or request.headers.get(path_header) or '/'
    # We'd like to use the "max-age" param as indicated in
    # but IE doesn't
    # save it to disk and the session is lost if people close
    # the browser. So we have to use the old "expires" ... sigh ...
##    cookie[name]['max-age'] = timeout * 60
    if timeout:
        cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
    if domain is not None:
        cookie[name]['domain'] = domain
    if secure:
        cookie[name]['secure'] = 1