#!/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 your database using the user id stored in the session. 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 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 the shelf. 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:]