summaryrefslogtreecommitdiff
path: root/flup/middleware/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'flup/middleware/session.py')
-rw-r--r--flup/middleware/session.py770
1 files changed, 0 insertions, 770 deletions
diff --git a/flup/middleware/session.py b/flup/middleware/session.py
deleted file mode 100644
index 3c67422..0000000
--- a/flup/middleware/session.py
+++ /dev/null
@@ -1,770 +0,0 @@
-# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import os
-import errno
-import string
-import time
-import weakref
-import atexit
-import shelve
-import cPickle as pickle
-
-try:
- import threading
-except ImportError:
- import dummy_threading as threading
-
-__all__ = ['Session',
- 'SessionStore',
- 'MemorySessionStore',
- 'ShelveSessionStore',
- 'DiskSessionStore',
- 'SessionMiddleware']
-
-class Session(dict):
- """
- Session objects, basically dictionaries.
- """
- identifierLength = 32
- # Would be nice if len(identifierChars) were some power of 2.
- identifierChars = string.digits + string.ascii_letters + '-_'
-
- def __init__(self, identifier):
- super(Session, self).__init__()
-
- assert self.isIdentifierValid(identifier)
- self._identifier = identifier
-
- self._creationTime = self._lastAccessTime = time.time()
- self._isValid = True
-
- def _get_identifier(self):
- return self._identifier
- identifier = property(_get_identifier, None, None,
- 'Unique identifier for Session within its Store')
-
- def _get_creationTime(self):
- return self._creationTime
- creationTime = property(_get_creationTime, None, None,
- 'Time when Session was created')
-
- def _get_lastAccessTime(self):
- return self._lastAccessTime
- lastAccessTime = property(_get_lastAccessTime, None, None,
- 'Time when Session was last accessed')
-
- def _get_isValid(self):
- return self._isValid
- isValid = property(_get_isValid, None, None,
- 'Whether or not this Session is valid')
-
- def touch(self):
- """Update Session's access time."""
- self._lastAccessTime = time.time()
-
- def invalidate(self):
- """Invalidate this Session."""
- self.clear()
- self._creationTime = self._lastAccessTime = 0
- self._isValid = False
-
- def isIdentifierValid(cls, ident):
- """
- Returns whether or not the given string *could be* a valid session
- identifier.
- """
- if type(ident) is str and len(ident) == cls.identifierLength:
- for c in ident:
- if c not in cls.identifierChars:
- return False
- return True
- return False
- isIdentifierValid = classmethod(isIdentifierValid)
-
- def generateIdentifier(cls):
- """
- Generate a random session identifier.
- """
- raw = os.urandom(cls.identifierLength)
-
- sessId = ''
- for c in raw:
- # So we lose 2 bits per random byte...
- sessId += cls.identifierChars[ord(c) % len(cls.identifierChars)]
- return sessId
- generateIdentifier = classmethod(generateIdentifier)
-
-def _shutdown(ref):
- store = ref()
- if store is not None:
- store.shutdown()
-
-class SessionStore(object):
- """
- Abstract base class for session stores. You first acquire a session by
- calling createSession() or checkOutSession(). After using the session,
- you must call checkInSession(). You must not keep references to sessions
- outside of a check in/check out block. Always obtain a fresh reference.
-
- Some external mechanism must be set up to call periodic() periodically
- (perhaps every 5 minutes).
-
- After timeout minutes of inactivity, sessions are deleted.
- """
- _sessionClass = Session
-
- def __init__(self, timeout=60, sessionClass=None):
- self._lock = threading.Condition()
-
- # Timeout in minutes
- self._sessionTimeout = timeout
-
- if sessionClass is not None:
- self._sessionClass = sessionClass
-
- self._checkOutList = {}
- self._shutdownRan = False
-
- # Ensure shutdown is called.
- atexit.register(_shutdown, weakref.ref(self))
-
- # Public interface.
-
- def createSession(self):
- """
- Create a new session with a unique identifier. Should never fail.
- (Will raise a RuntimeError in the rare event that it does.)
-
- The newly-created session should eventually be released by
- a call to checkInSession().
- """
- assert not self._shutdownRan
- self._lock.acquire()
- try:
- attempts = 0
- while attempts < 10000:
- sessId = self._sessionClass.generateIdentifier()
- sess = self._createSession(sessId)
- if sess is not None: break
- attempts += 1
-
- if attempts >= 10000:
- raise RuntimeError, self.__class__.__name__ + \
- '.createSession() failed'
-
- assert sess.identifier not in self._checkOutList
- self._checkOutList[sess.identifier] = sess
- return sess
- finally:
- self._lock.release()
-
- def checkOutSession(self, identifier):
- """
- Checks out a session for use. Returns the session if it exists,
- otherwise returns None. If this call succeeds, the session
- will be touch()'ed and locked from use by other processes.
- Therefore, it should eventually be released by a call to
- checkInSession().
- """
- assert not self._shutdownRan
-
- if not self._sessionClass.isIdentifierValid(identifier):
- return None
-
- self._lock.acquire()
- try:
- # If we know it's already checked out, block.
- while identifier in self._checkOutList:
- self._lock.wait()
- sess = self._loadSession(identifier)
- if sess is not None:
- if sess.isValid:
- assert sess.identifier not in self._checkOutList
- self._checkOutList[sess.identifier] = sess
- sess.touch()
- else:
- # No longer valid (same as not existing). Delete/unlock
- # the session.
- self._deleteSession(sess.identifier)
- sess = None
- return sess
- finally:
- self._lock.release()
-
- def checkInSession(self, session):
- """
- Returns the session for use by other threads/processes. Safe to
- pass None.
- """
- assert not self._shutdownRan
-
- if session is None:
- return
-
- self._lock.acquire()
- try:
- assert session.identifier in self._checkOutList
- if session.isValid:
- self._saveSession(session)
- else:
- self._deleteSession(session.identifier)
- del self._checkOutList[session.identifier]
- self._lock.notify()
- finally:
- self._lock.release()
-
- def shutdown(self):
- """Clean up outstanding sessions."""
- self._lock.acquire()
- try:
- if not self._shutdownRan:
- # Save or delete any sessions that are still out there.
- for key,sess in self._checkOutList.items():
- if sess.isValid:
- self._saveSession(sess)
- else:
- self._deleteSession(sess.identifier)
- self._checkOutList.clear()
- self._shutdown()
- self._shutdownRan = True
- finally:
- self._lock.release()
-
- def __del__(self):
- self.shutdown()
-
- def periodic(self):
- """Timeout old sessions. Should be called periodically."""
- self._lock.acquire()
- try:
- if not self._shutdownRan:
- self._periodic()
- finally:
- self._lock.release()
-
- # To be implemented by subclasses. self._lock will be held whenever
- # these are called and for methods that take an identifier,
- # the identifier will be guaranteed to be valid (but it will not
- # necessarily exist).
-
- def _createSession(self, identifier):
- """
- Attempt to create the session with the given identifier. If
- successful, return the newly-created session, which must
- also be implicitly locked from use by other processes. (The
- session returned should be an instance of self._sessionClass.)
- If unsuccessful, return None.
- """
- raise NotImplementedError, self.__class__.__name__ + '._createSession'
-
- def _loadSession(self, identifier):
- """
- Load the session with the identifier from secondary storage returning
- None if it does not exist. If the load is successful, the session
- must be locked from use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._loadSession'
-
- def _saveSession(self, session):
- """
- Store the session into secondary storage. Also implicitly releases
- the session for use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._saveSession'
-
- def _deleteSession(self, identifier):
- """
- Deletes the session from secondary storage. Must be OK to pass
- in an invalid (non-existant) identifier. If the session did exist,
- it must be released for use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._deleteSession'
-
- def _periodic(self):
- """Remove timedout sessions from secondary storage."""
- raise NotImplementedError, self.__class__.__name__ + '._periodic'
-
- def _shutdown(self):
- """Performs necessary shutdown actions for secondary store."""
- raise NotImplementedError, self.__class__.__name__ + '._shutdown'
-
- # Utilities
-
- def _isSessionTimedout(self, session, now=time.time()):
- return (session.lastAccessTime + self._sessionTimeout * 60) < now
-
-class MemorySessionStore(SessionStore):
- """
- Memory-based session store. Great for persistent applications, terrible
- for one-shot ones. :)
- """
- def __init__(self, *a, **kw):
- super(MemorySessionStore, self).__init__(*a, **kw)
-
- # Our "secondary store".
- self._secondaryStore = {}
-
- def _createSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- return None
- sess = self._sessionClass(identifier)
- self._secondaryStore[sess.identifier] = sess
- return sess
-
- def _loadSession(self, identifier):
- return self._secondaryStore.get(identifier, None)
-
- def _saveSession(self, session):
- self._secondaryStore[session.identifier] = session
-
- def _deleteSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- del self._secondaryStore[identifier]
-
- def _periodic(self):
- now = time.time()
- for key,sess in self._secondaryStore.items():
- if self._isSessionTimedout(sess, now):
- del self._secondaryStore[key]
-
- def _shutdown(self):
- pass
-
-class ShelveSessionStore(SessionStore):
- """
- Session store based on Python "shelves." Only use if you can guarantee
- that storeFile will NOT be accessed concurrently by other instances.
- (In other processes, threads, anywhere!)
- """
- def __init__(self, storeFile='sessions', *a, **kw):
- super(ShelveSessionStore, self).__init__(*a, **kw)
-
- self._secondaryStore = shelve.open(storeFile,
- protocol=pickle.HIGHEST_PROTOCOL)
-
- def _createSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- return None
- sess = self._sessionClass(identifier)
- self._secondaryStore[sess.identifier] = sess
- return sess
-
- def _loadSession(self, identifier):
- return self._secondaryStore.get(identifier, None)
-
- def _saveSession(self, session):
- self._secondaryStore[session.identifier] = session
-
- def _deleteSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- del self._secondaryStore[identifier]
-
- def _periodic(self):
- now = time.time()
- for key,sess in self._secondaryStore.items():
- if self._isSessionTimedout(sess, now):
- del self._secondaryStore[key]
-
- def _shutdown(self):
- self._secondaryStore.close()
-
-class DiskSessionStore(SessionStore):
- """
- Disk-based session store that stores each session as its own file
- within a specified directory. Should be safe for concurrent use.
- (As long as the underlying OS/filesystem respects create()'s O_EXCL.)
- """
- def __init__(self, storeDir='sessions', *a, **kw):
- super(DiskSessionStore, self).__init__(*a, **kw)
-
- self._sessionDir = storeDir
- if not os.access(self._sessionDir, os.F_OK):
- # Doesn't exist, try to create it.
- os.mkdir(self._sessionDir)
-
- def _filenameForSession(self, identifier):
- return os.path.join(self._sessionDir, identifier + '.sess')
-
- def _lockSession(self, identifier, block=True):
- # Release SessionStore lock so we don't deadlock.
- self._lock.release()
- try:
- fn = self._filenameForSession(identifier) + '.lock'
- while True:
- try:
- fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- except OSError, e:
- if e.errno != errno.EEXIST:
- raise
- else:
- os.close(fd)
- break
-
- if not block:
- return False
-
- # See if the lock is stale. If so, remove it.
- try:
- now = time.time()
- mtime = os.path.getmtime(fn)
- if (mtime + 60) < now:
- os.unlink(fn)
- except OSError, e:
- if e.errno != errno.ENOENT:
- raise
-
- time.sleep(0.1)
-
- return True
- finally:
- self._lock.acquire()
-
- def _unlockSession(self, identifier):
- fn = self._filenameForSession(identifier) + '.lock'
- os.unlink(fn) # Need to catch errors?
-
- def _createSession(self, identifier):
- fn = self._filenameForSession(identifier)
- lfn = fn + '.lock'
- # Attempt to create the file's *lock* first.
- lfd = fd = -1
- try:
- lfd = os.open(lfn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- except OSError, e:
- if e.errno == errno.EEXIST:
- if lfd >= 0:
- # Remove lockfile.
- os.close(lfd)
- os.unlink(lfn)
- return None
- raise
- else:
- # Success.
- os.close(fd)
- os.close(lfd)
- return self._sessionClass(identifier)
-
- def _loadSession(self, identifier, block=True):
- if not self._lockSession(identifier, block):
- return None
- try:
- return pickle.load(open(self._filenameForSession(identifier)))
- except:
- self._unlockSession(identifier)
- return None
-
- def _saveSession(self, session):
- f = open(self._filenameForSession(session.identifier), 'w+')
- pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
- f.close()
- self._unlockSession(session.identifier)
-
- def _deleteSession(self, identifier):
- try:
- os.unlink(self._filenameForSession(identifier))
- except:
- pass
- self._unlockSession(identifier)
-
- def _periodic(self):
- now = time.time()
- sessions = os.listdir(self._sessionDir)
- for name in sessions:
- if not name.endswith('.sess'):
- continue
- identifier = name[:-5]
- if not self._sessionClass.isIdentifierValid(identifier):
- continue
- # Not very efficient.
- sess = self._loadSession(identifier, block=False)
- if sess is None:
- continue
- if self._isSessionTimedout(sess, now):
- self._deleteSession(identifier)
- else:
- self._unlockSession(identifier)
-
- def _shutdown(self):
- pass
-
-# SessionMiddleware stuff.
-
-from Cookie import SimpleCookie
-import cgi
-import urlparse
-
-class SessionService(object):
- """
- WSGI extension API passed to applications as
- environ['com.saddi.service.session'].
-
- Public API: (assume service = environ['com.saddi.service.session'])
- service.session - Returns the Session associated with the client.
- service.hasSession - True if the client is currently associated with
- a Session.
- service.isSessionNew - True if the Session was created in this
- transaction.
- service.hasSessionExpired - True if the client is associated with a
- non-existent Session.
- service.encodesSessionInURL - True if the Session ID should be encoded in
- the URL. (read/write)
- service.encodeURL(url) - Returns url encoded with Session ID (if
- necessary).
- service.cookieAttributes - Dictionary of additional RFC2109 attributes
- to be added to the generated cookie.
- service.forceCookieOutput - Normally False. Set to True to force
- output of the Set-Cookie header during this request.
- """
- _expiredSessionIdentifier = 'expired session'
-
- def __init__(self, store, environ,
- cookieName='_SID_',
- cookieExpiration=None, # Deprecated
- cookieAttributes={},
- fieldName='_SID_'):
- self._store = store
- self._cookieName = cookieName
- self._cookieExpiration = cookieExpiration
- self.cookieAttributes = dict(cookieAttributes)
- self.forceCookieOutput = False
- self._fieldName = fieldName
-
- self._session = None
- self._newSession = False
- self._expired = False
- self.encodesSessionInURL = False
-
- if __debug__: self._closed = False
-
- self._loadExistingSession(environ)
-
- def _loadSessionFromCookie(self, environ):
- """
- Attempt to load the associated session using the identifier from
- the cookie.
- """
- C = SimpleCookie(environ.get('HTTP_COOKIE'))
- morsel = C.get(self._cookieName, None)
- if morsel is not None:
- self._session = self._store.checkOutSession(morsel.value)
- self._expired = self._session is None
-
- def _loadSessionFromQueryString(self, environ):
- """
- Attempt to load the associated session using the identifier from
- the query string.
- """
- qs = cgi.parse_qsl(environ.get('QUERY_STRING', ''))
- for name,value in qs:
- if name == self._fieldName:
- self._session = self._store.checkOutSession(value)
- self._expired = self._session is None
- self.encodesSessionInURL = True
- break
-
- def _loadExistingSession(self, environ):
- """Attempt to associate with an existing Session."""
- # Try cookie first.
- self._loadSessionFromCookie(environ)
-
- # Next, try query string.
- if self._session is None:
- self._loadSessionFromQueryString(environ)
-
- def _sessionIdentifier(self):
- """Returns the identifier of the current session."""
- assert self._session is not None
- return self._session.identifier
-
- def _shouldAddCookie(self):
- """
- Returns True if the session cookie should be added to the header
- (if not encoding the session ID in the URL). The cookie is added if
- one of these three conditions are true: a) the session was just
- created, b) the session is no longer valid, or c) the client is
- associated with a non-existent session.
- """
- return self._newSession or \
- (self._session is not None and not self._session.isValid) or \
- (self._session is None and self._expired)
-
- def addCookie(self, headers):
- """Adds Set-Cookie header if needed."""
- if not self.encodesSessionInURL and \
- (self._shouldAddCookie() or self.forceCookieOutput):
- if self._session is not None:
- sessId = self._sessionIdentifier()
- expireCookie = not self._session.isValid
- else:
- sessId = self._expiredSessionIdentifier
- expireCookie = True
-
- C = SimpleCookie()
- name = self._cookieName
- C[name] = sessId
- C[name]['path'] = '/'
- if self._cookieExpiration is not None:
- C[name]['expires'] = self._cookieExpiration
- C[name].update(self.cookieAttributes)
- if expireCookie:
- # Expire cookie
- C[name]['expires'] = -365*24*60*60
- C[name]['max-age'] = 0
- headers.append(('Set-Cookie', C[name].OutputString()))
-
- def close(self):
- """Checks session back into session store."""
- if self._session is None:
- return
- # Check the session back in and get rid of our reference.
- self._store.checkInSession(self._session)
- self._session = None
- if __debug__: self._closed = True
-
- # Public API
-
- def _get_session(self):
- if __debug__: assert not self._closed
- if self._session is None:
- self._session = self._store.createSession()
- self._newSession = True
-
- assert self._session is not None
- return self._session
- session = property(_get_session, None, None,
- 'Returns the Session object associated with this '
- 'client')
-
- def _get_hasSession(self):
- if __debug__: assert not self._closed
- return self._session is not None
- hasSession = property(_get_hasSession, None, None,
- 'True if a Session currently exists for this client')
-
- def _get_isSessionNew(self):
- if __debug__: assert not self._closed
- return self._newSession
- isSessionNew = property(_get_isSessionNew, None, None,
- 'True if the Session was created in this '
- 'transaction')
-
- def _get_hasSessionExpired(self):
- if __debug__: assert not self._closed
- return self._expired
- hasSessionExpired = property(_get_hasSessionExpired, None, None,
- 'True if the client was associated with a '
- 'non-existent Session')
-
- # Utilities
-
- def encodeURL(self, url):
- """Encodes session ID in URL, if necessary."""
- if __debug__: assert not self._closed
- if not self.encodesSessionInURL or self._session is None:
- return url
- u = list(urlparse.urlsplit(url))
- q = '%s=%s' % (self._fieldName, self._sessionIdentifier())
- if u[3]:
- u[3] = q + '&' + u[3]
- else:
- u[3] = q
- return urlparse.urlunsplit(u)
-
-def _addClose(appIter, closeFunc):
- """
- Wraps an iterator so that its close() method calls closeFunc. Respects
- the existence of __len__ and the iterator's own close() method.
-
- Need to use metaclass magic because __len__ and next are not
- recognized unless they're part of the class. (Can't assign at
- __init__ time.)
- """
- class metaIterWrapper(type):
- def __init__(cls, name, bases, clsdict):
- super(metaIterWrapper, cls).__init__(name, bases, clsdict)
- if hasattr(appIter, '__len__'):
- cls.__len__ = appIter.__len__
- cls.next = iter(appIter).next
- if hasattr(appIter, 'close'):
- def _close(self):
- appIter.close()
- closeFunc()
- cls.close = _close
- else:
- cls.close = closeFunc
-
- class iterWrapper(object):
- __metaclass__ = metaIterWrapper
- def __iter__(self):
- return self
-
- return iterWrapper()
-
-class SessionMiddleware(object):
- """
- WSGI middleware that adds a session service. A SessionService instance
- is passed to the application in environ['com.saddi.service.session'].
- A references to this instance should not be saved. (A new instance is
- instantiated with every call to the application.)
- """
- _serviceClass = SessionService
-
- def __init__(self, store, application, serviceClass=None, **kw):
- self._store = store
- self._application = application
- if serviceClass is not None:
- self._serviceClass = serviceClass
- self._serviceKW = kw
-
- def __call__(self, environ, start_response):
- service = self._serviceClass(self._store, environ, **self._serviceKW)
- environ['com.saddi.service.session'] = service
-
- def my_start_response(status, headers, exc_info=None):
- service.addCookie(headers)
- return start_response(status, headers, exc_info)
-
- try:
- result = self._application(environ, my_start_response)
- except:
- # If anything goes wrong, ensure the session is checked back in.
- service.close()
- raise
-
- # The iterator must be unconditionally wrapped, just in case it
- # is a generator. (In which case, we may not know that a Session
- # has been checked out until completion of the first iteration.)
- return _addClose(result, service.close)
-
-if __name__ == '__main__':
- mss = MemorySessionStore(timeout=5)
-# sss = ShelveSessionStore(timeout=5)
- dss = DiskSessionStore(timeout=5)