#!/usr/bin/python
#
# Copyright (c) 2006 Conan C. Albrecht, Jonathan Ellis
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is furnished
# to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
'''
Module Use
==========
Use the session by calling the get(), put(), and delete() methods. The
following example stores, uses, and then deletes the username:
In other words, it is very similar to a dictionary. You can pretend the session
module is just a dictionary specific to the user accessing the page. The session
"dictionary" contents will change automatically to match the user's values
for the browser accessing your page.
IMPORTANT NOTE 1:
You should only put small, temporary objects into the session. While
the session will take anything that can be pickled, remember that
it must be stored in a dbm hash. Large objects will slow things down
considerably; avoid them. Store strings, ints, and simple objects.
For example, if you want to store a User object, store the primary
key (user id) to the User object. On each request, reload the User
object from yourdatabaseusingtheuseridstoredthesession. import
IMPORTANT NOTE 2:
This module adds a _lastaccess key to each session. It uses this
value to know when the session needs to be cleaned out. Don't
use this key name. In other words, don't call
session['_lastaccess'] = anything.
IMPORTANT NOTE 3:
If you are running Spyce in mod_python, cgi, or fastcgi installation
modes, you *should not* use the memory option. Use the dbm method
for these installation modes. While dbm is slower and takes disk
space, an in-memory cache will cause update errors since you'll
get multiple caches! This is because mod_python, cgi, and fastcgi
create multiple instances of your application -- you'll have an
cache in *each* instance.
In addition, dbm-method sessions survive a server reboot, memory-method
sessions do not.
The Internals
=============
Sessions are regular Python dictionaries. The standard python
"shelve" module is used to save these dictionaries to disk.
Since sessions are temporary and shouldn't be moved from installation import
to installation, the module uses the highest possible pickle
protocol for speed.
Whenever a session is saved to a file, the last checked
timestamp is used to see if the file should be cleaned.
If enough time has passed, all old sessions are deleted
from theshelf. import
The module cleans every CLEAN_INTERVAL seconds (set to every
24 hours right now). If you want a different CLEAN_INTERVAL,
just change this constant at the top of the file. I didn't make
this accessible publicly because I don't think most users
care how often it cleans sessions out. Note that files are
not actually ever cleaned until something is set within them.
Sessions are never actually created until your program puts
data in them. In other words, they are created just in time.
Since many site visitors may never get session data set, this
provides a great efficiency since no disk access is required
for them. Your program will think they have a session, but
as long as you don't call put(), they won't get a session
internally.
The module is thread-safe. A series of file locks is used
to lock shelves when they are being used or cleaned. However,
if multiple instances of Spyce are created, it is remotely
possible that sessions might blast each other. I'm not
sure how to solve this without additional disk access. If anyone
wants to help here please do. (note that the regular session
module has this problem, too.)
'''
import types, sys, shelve, pickle, os, anydbm
from UserDict import DictMixin
from spyceModule import spyceModule,moduleFinder
import spyce, spyceLock
########################################################
### globals
# how often to check for and clean old sessions
CLEAN_INTERVAL = 60 * 60 * 2 # clean every 2 hours
########################################################
### Session stores
class SessionNotFoundError(Exception): pass
class SessionStore:
def __init__(self):
self._last_cleaned = time.time()
def load(self, sessionid, expires):
# when creating a new session object, its expiration should be set to "expires"
raise NotImplementedError()
def save(self, sessionid, session, expires):
raise NotImplementedError()
def clear(self, sessionid):
raise NotImplementedError()
def is_expired(self, sessionid):
raise NotImplementedError()
def list(self):
raise NotImplementedError()
memory_cache = {}
class MemoryStore(SessionStore):
def load(self, sessionid, expires):
# (python interpreter lock takes care of locking)
return memory_cache.get(sessionid, {'_expires': expires})
def save(self, sessionid, session):
session['_lastaccess'] = time.time()
memory_cache[sessionid] = session
def is_expired(self, sessionid):
try:
s = memory_cache[sessionid]
except KeyError:
raise SessionNotFoundError()
return s['_lastaccess'] < time.time() - s['_expires']
def clear(self, sessionid):
try:
del memory_cache[sessionid]
except KeyError:
raise SessionNotFoundError()
def list(self):
return memory_cache.keys()
class DbmStore(SessionStore):
def __init__(self, dir):
SessionStore.__init__(self)
self.dir = dir
def _init(self):
# can't perform these in __init__ b/c spyce's server object
# doesn't exist yet when __init__ is called from spyceconf
server = spyce.getServer()
gen = lambda i: server.createLock('sessionlock%d' % i)
self.lock = spyceLock.MultiLock(100, gen)
def _filename(self, sessionid):
return os.path.join(self.dir, 'spysession' + sessionid)
def load(self, sessionid, expires):
"""
(Conan's original session2 used a fixed, configurable, number of
shelve files for all session objects. This works poorly for highly
concurrent access, though: shelve files serialize write access.
So as aesthetically messy as it may be, one-file-per-session seems
to be the way to go.)
"""
if not hasattr(self, 'lock'): self._init()
fname = self._filename(sessionid)
self.lock.acquire(fname)
try:
session = shelve.open(fname, writeback=True, protocol=pickle.HIGHEST_PROTOCOL)
finally:
self.lock.release(fname)
if '_expires' not in session:
session['_expires'] = expires
return session
def save(self, sessionid, session):
if not hasattr(self, 'lock'): self._init()
fname = self._filename(sessionid)
self.lock.acquire(fname)
try:
session.close()
finally:
self.lock.release(fname)
def is_expired(self, sessionid):
self.lock.acquire(fname)
try:
try:
s = shelve.open(fname, flag='r')
except anydbm.error, e:
# "need 'c' or 'n' flag to open new db"
if 'new db' in e.args[0]:
raise SessionNotFoundError()
raise
finally:
self.lock.release(fname)
# (shelve updates mtime even when nothing changes)
return os.path.getmtime(self._filename(sessionid)) < time.time() - s['_expires']
def clear(self, sessionid):
try:
os.unlink(self._filename(sessionid))
except OSError, e:
if e.args[0] == 2:
# OSError: [Errno 2] No such file or directory: 'foo.bar'
raise SessionNotFoundError()
elif e.args[0] == 13:
# OSError: [Errno 13] Permission denied: 'foo.sh'
# usually means in-use by another thread/process
pass
else:
raise
def list(self):
import glob
n = len('spysession')
return [fname[n:] for fname in
glob.glob(os.path.join(self.dir, 'spysession*'))]
def clean_store(self):
store = server.config.session_store
for sessionid in store.list():
try:
if store.is_expired(sessionid):
store.clear(sessionid)
except SessionNotFoundError:
pass
########################################################
### The session module
class session(spyceModule, DictMixin):
'''Manages the sessions of the web site in memory or a dbm (shelve) file.'''
def start(self, *args, **kargs):
self.mf = moduleFinder(self._api)
server = spyce.getServer()
self.store = server.config.session_store
def get_option(name):
if kargs.has_key(name):
return kargs[name]
return getattr(server.config, 'session_' + name)
self.path = get_option('path')
self.expire = get_option('expire')
self.sessionid = self._find_sessionid()
self.session = self.store.load(self.sessionid, self.expire)
def finish(self, err):
# refresh the cookie in the browser and restart expiration timer
self._api.getModule('cookie').set('sessionid', self.sessionid, expire=self.expire, path=self.path)
self.store.save(self.sessionid, self.session)
###################################################
### Dictionary-like methods
def __setitem__(self, key, value):
self.session[key] = value
def __delitem__(self, key):
del self.session[key]
def __getitem__(self, key):
return self.session[key]
def keys(self):
return self.session.keys()
def __contains__(self, key):
return key in self.session
def __iter__(self):
return iter(self.session)
def iteritems(self):
return iter(self.session.iteritems())
#####################################################
### Private methods
def _find_sessionid(self):
return self._api.getModule('cookie').get('sessionid') or newtoken(self.mf)
############################################################################
seed = 'spycesessionseed'
import sha, time, random
def newtoken(mf):
global seed
s = sha.sha(seed)
s.update(str(time.time()))
s.update(str(random.random()))
s.update(mf.request.getHeader('User-Agent') or '')
s.update(mf.request.filename())
h = s.hexdigest()
seed = h[:10]
return h[10:]
|