Back to index

python-weblib  1.3.9
session.py
Go to the documentation of this file.
00001 """
00002 pyweblib.session - server-side web session handling
00003 (C) 2001 by Michael Stroeder <michael@stroeder.com>
00004 
00005 This module implements server side session handling stored in
00006 arbitrary string-keyed dictionary objects
00007 
00008 This module is distributed under the terms of the
00009 GPL (GNU GENERAL PUBLIC LICENSE) Version 2
00010 (see http://www.gnu.org/copyleft/gpl.html)
00011 
00012 $Id: session.py,v 1.28 2010/10/27 08:27:10 michael Exp $
00013 """
00014 
00015 __version__ = '0.3.5'
00016 
00017 import string,re,random,time,pickle
00018 
00019 SESSION_ID_CHARS=string.letters+string.digits+'-._'
00020 
00021 SESSION_CROSSCHECKVARS = (
00022   """
00023   List of environment variables assumed to be constant throughout
00024   web sessions with the same ID if existent.
00025   These env vars are cross-checked each time when restoring an
00026   web session to reduce the risk of session-hijacking.
00027 
00028   Note: REMOTE_ADDR and REMOTE_HOST might not be constant if the client
00029   access comes through a network of web proxy siblings.
00030   """
00031   # REMOTE_ADDR and REMOTE_HOST might not be constant if the client
00032   # access comes through a network of web proxy siblings.
00033   'REMOTE_ADDR','REMOTE_HOST',
00034   'REMOTE_IDENT','REMOTE_USER',
00035   # If the proxy sets them but can be easily spoofed
00036   'FORWARDED_FOR','HTTP_X_FORWARDED_FOR',
00037   # These two are not really secure
00038   'HTTP_USER_AGENT','HTTP_ACCEPT_CHARSET',
00039   # SSL session ID if running on SSL server capable
00040   # of reusing SSL sessions
00041   'SSL_SESSION_ID',
00042   # env vars of client certs used for SSL strong authentication
00043   'SSL_CLIENT_V_START','SSL_CLIENT_V_END',
00044   'SSL_CLIENT_I_DN','SSL_CLIENT_IDN',
00045   'SSL_CLIENT_S_DN','SSL_CLIENT_SDN',
00046   'SSL_CLIENT_M_SERIAL','SSL_CLIENT_CERT_SERIAL',
00047 )
00048 
00049 ##############################################################################
00050 # Exception classes
00051 ##############################################################################
00052 
00053 class SessionException(Exception):
00054   """Raised if """
00055   def __init__(self, *args):
00056     self.args = args
00057 
00058 class CorruptData(SessionException):
00059   """Raised if data was corrupt, e.g. UnpicklingError occured"""
00060   def __str__(self):
00061     return "Error during retrieving corrupted session data. Session deleted."
00062 
00063 class GenerateIDError(SessionException):
00064   """Raised if generation of unique session ID failed."""
00065   def __init__(self, maxtry):
00066     self.maxtry = maxtry
00067   def __str__(self):
00068     return "Could not create new session id. Tried %d times." % (self.maxtry)
00069 
00070 class SessionExpired(SessionException):
00071   """Raised if session is expired."""
00072   def __init__(self, timestamp, session_data):
00073     self.timestamp = timestamp
00074     self.session_data = session_data
00075   def __str__(self):
00076     return "Session expired %s." % (time.strftime('%Y-%m-%d %H:%M:%S',time.gmtime(self.timestamp)))
00077 
00078 class SessionHijacked(SessionException):
00079   """Raised if hijacking of session was detected."""
00080   def __init__(self, failed_vars):
00081     self.failed_vars = failed_vars
00082   def __str__(self):
00083     return "Crosschecking of the following env vars failed: %s." % (
00084       self.failed_vars
00085     )
00086 
00087 class MaxSessionCountExceeded(SessionException):
00088   """Raised if maximum number of sessions is exceeded."""
00089   def __init__(self, max_session_count):
00090     self.max_session_count = max_session_count
00091   def __str__(self):
00092     return "Maximum number of sessions exceeded. Limit is %d." % (
00093       self.max_session_count
00094     )
00095 
00096 class BadSessionId(SessionException):
00097   """Raised if session ID not found in session dictionary."""
00098   def __init__(self, session_id):
00099     self.session_id = session_id
00100   def __str__(self):
00101     return "No session with key %s." % (self.session_id)
00102 
00103 class InvalidSessionId(SessionException):
00104   """Raised if session ID not found in session dictionary."""
00105   def __init__(self, session_id):
00106     self.session_id = session_id
00107   def __str__(self):
00108     return "No session with key %s." % (self.session_id)
00109 
00110 try:
00111   import threading
00112   from threading import Lock as ThreadingLock
00113 
00114 except ImportError:
00115   # Python installation has no thread support
00116   class ThreadingLock:
00117     """
00118     mimikri for threading.Lock()
00119     """
00120     def acquire(self):
00121       pass
00122     def release(self):
00123       pass
00124 
00125 else:
00126 
00127   class CleanUpThread(threading.Thread):
00128     """
00129     Thread class for clean-up thread
00130     """
00131     def __init__(self,sessionInstance,interval=60):
00132       self._sessionInstance = sessionInstance
00133       self._interval = interval
00134       self._stop_event = threading.Event()
00135       self._removed = 0
00136       threading.Thread.__init__(self,name=self.__class__.__module__+self.__class__.__name__)
00137 
00138     def run(self):
00139       """Thread function for cleaning up session database"""
00140       while not self._stop_event.isSet():
00141         self._removed += self._sessionInstance.cleanUp()
00142         self._stop_event.wait(self._interval)
00143 
00144     def __repr__(self):
00145       return '%s: %d sessions removed' % (
00146         self.getName(),self._removed
00147       )
00148 
00149     def join(self,timeout=0.0):
00150       self._stop_event.set()
00151       threading.Thread.join(self,timeout)
00152 
00153 
00154 class WebSession:
00155   """
00156   The session class which handles storing and retrieving of session data
00157   in a dictionary-like sessiondict object.
00158   """
00159 
00160   def __init__(
00161     self,
00162     dictobj=None,
00163     expireDeactivate=0,
00164     expireRemove=0,
00165     crossCheckVars=None,
00166     maxSessionCount=None,
00167     sessionIDLength=12,
00168     sessionIDChars=None,
00169   ):
00170     """
00171     dictobj
00172         has to be a instance of a dictionary-like object
00173         (e.g. derived from UserDict or shelve)
00174     expireDeactivate
00175         amount of time (secs) after which a session
00176         expires and a SessionExpired exception is
00177         raised which contains the session data.
00178     expireRemove
00179         Amount of time (secs) after which a session
00180         expires and the session data is silently deleted.
00181         A InvalidSessionId exception is raised in this case if
00182         the application trys to access the session ID again.
00183     crossCheckVars
00184         List of keys of variables cross-checked for each
00185         retrieval of session data in retrieveSession(). If None
00186         SESSION_CROSSCHECKVARS is used.
00187     maxSessionCount
00188         Maximum number of valid sessions. This affects
00189         behaviour of retrieveSession() which raises.
00190         None means unlimited number of sessions.
00191     sessionIDLength
00192         Exact integer length of the session ID generated
00193     sessionIDChars
00194         String containing the valid chars for session IDs
00195         (if this is zero-value the default is SESSION_ID_CHARS)
00196     """
00197     if dictobj is None:
00198       self.sessiondict = {}
00199     else:
00200       self.sessiondict = dictobj
00201     self.expireDeactivate = expireDeactivate
00202     self.expireRemove = expireRemove
00203     self._session_lock = ThreadingLock()
00204     if crossCheckVars is None:
00205       crossCheckVars = SESSION_CROSSCHECKVARS
00206     self.crossCheckVars = crossCheckVars
00207     self.maxSessionCount = maxSessionCount
00208     self.sessionCounter = 0
00209     self.session_id_len = sessionIDLength
00210     self.session_id_chars = sessionIDChars or SESSION_ID_CHARS
00211     self.session_id_re = re.compile('^[%s]+$' % (re.escape(self.session_id_chars)))
00212     return # __init__()
00213 
00214   def sync(self):
00215     """
00216     Call sync if self.sessiondict has .sync() method
00217     """
00218     if hasattr(self.sessiondict,'sync'):
00219       self.sessiondict.sync()
00220 
00221   def close(self):
00222     """
00223     Call close() if self.sessiondict has .close() method
00224     """
00225     if hasattr(self.sessiondict,'close'):
00226       # Close e.g. a database
00227       self.sessiondict.close()
00228     else:
00229       # Make sessiondict inaccessible
00230       self.sessiondict = None
00231 
00232   def _validateSessionIdFormat(self,session_id):
00233     """
00234     Validate the format of session_id. Implementation
00235     has to match IDs produced in method _generateSessionID()
00236     """
00237     if len(session_id)!=self.session_id_len or self.session_id_re.match(session_id) is None:
00238       raise BadSessionId(session_id)
00239     return
00240 
00241   def _crosscheckSessionEnv(self,stored_env,current_env):
00242     """
00243     Returns a list of keys of items which differ in
00244     stored_env and current_env.
00245     """
00246     return [
00247       k
00248       for k in stored_env.keys()
00249       if stored_env[k]!=current_env.get(k,None)
00250     ]
00251 
00252   def _generateCrosscheckEnv(self,current_env):
00253     """
00254     Generate a dictionary of env vars for session cross-checking
00255     """
00256     crosscheckenv = {}
00257     for k in self.crossCheckVars:
00258       if current_env.has_key(k):
00259         crosscheckenv[k] = current_env[k]
00260     return crosscheckenv
00261 
00262   def _generateSessionID(self,maxtry=1):
00263     """
00264     Generate a new random and unique session id string
00265     """
00266     def choice_id():
00267       return ''.join([ random.choice(SESSION_ID_CHARS) for i in range(self.session_id_len) ])
00268     newid = choice_id()
00269     tried = 0
00270     while self.sessiondict.has_key(newid) and (not maxtry or tried<maxtry):
00271       newid = choice_id()
00272       tried = tried+1
00273     if maxtry and tried>=maxtry:
00274       raise GenerateIDError(maxtry)
00275     else:
00276       return newid
00277 
00278   def storeSession(self,session_id,session_data):
00279     """
00280     Store session_data under session_id.
00281     """
00282     self._session_lock.acquire()
00283     try:
00284       # Store session data with timestamp
00285       self.sessiondict[session_id] = (time.time(),session_data)
00286       self.sync()
00287     finally:
00288       self._session_lock.release()
00289     return session_id
00290 
00291   def deleteSession(self,session_id):
00292     """
00293     Delete session_data referenced by session_id.
00294     """
00295     # Delete the session data
00296     self._session_lock.acquire()
00297     try:
00298       if self.sessiondict.has_key(session_id):
00299         del self.sessiondict[session_id]
00300       if self.sessiondict.has_key('__session_checkvars__'+session_id):
00301         del self.sessiondict['__session_checkvars__'+session_id]
00302       self.sync()
00303     finally:
00304       self._session_lock.release()
00305     return session_id
00306 
00307   def retrieveSession(self,session_id,env={}):
00308     """
00309     Retrieve session data
00310     """
00311     self._validateSessionIdFormat(session_id)
00312     session_vars_key = '__session_checkvars__'+session_id
00313     # Check if session id exists
00314     if not (
00315       self.sessiondict.has_key(session_id) and \
00316       self.sessiondict.has_key(session_vars_key)
00317     ):
00318       raise InvalidSessionId(session_id)
00319     # Read the timestamped session data
00320     try:
00321       self._session_lock.acquire()
00322       try:
00323         session_checkvars = self.sessiondict[session_vars_key]
00324         timestamp,session_data = self.sessiondict[session_id]
00325       finally:
00326         self._session_lock.release()
00327     except pickle.UnpicklingError:
00328       self.deleteSession(session_id)
00329       raise CorruptData
00330     current_time = time.time()
00331     # Check if session data is already expired
00332     if self.expireDeactivate and \
00333        (current_time>timestamp+self.expireDeactivate):
00334       # Remove session entry
00335       self.deleteSession(session_id)
00336       # Check if application should be able to allow relogin
00337       if self.expireRemove and \
00338          (current_time>timestamp+self.expireRemove):
00339         raise InvalidSessionId(session_id)
00340       else:
00341         raise SessionExpired(timestamp,session_data)
00342     failed_vars = self._crosscheckSessionEnv(session_checkvars,env)
00343     if failed_vars:
00344       # Remove session entry
00345       raise SessionHijacked(failed_vars)
00346     # Everything's ok => return the session data
00347     return session_data
00348 
00349   def newSession(self,env={}):
00350     """
00351     Store session data under session id
00352     """
00353     if self.maxSessionCount and len(self.sessiondict)/2+1>self.maxSessionCount:
00354       raise MaxSessionCountExceeded(self.maxSessionCount)
00355     self._session_lock.acquire()
00356     try:
00357       # generate completely new session data entry
00358       session_id=self._generateSessionID(maxtry=3)
00359       # Store session data with timestamp if session ID
00360       # was created successfully
00361       self.sessiondict[session_id] = (
00362         # Current time
00363         time.time(),
00364         # Store a dummy string first
00365         '_created_',
00366       )
00367       self.sessiondict['__session_checkvars__'+session_id] = self._generateCrosscheckEnv(env)
00368       self.sync()
00369       self.sessionCounter += 1
00370     finally:
00371       self._session_lock.release()
00372     return session_id
00373 
00374   def cleanUp(self):
00375     """
00376     Search for expired session entries and delete them.
00377 
00378     Returns integer counter of deleted sessions as result.
00379     """
00380     current_time = time.time()
00381     result = 0
00382     for session_id in self.sessiondict.keys():
00383       if not session_id.startswith('__'):
00384         try:
00385           session_timestamp = self.sessiondict[session_id][0]
00386         except InvalidSessionId:
00387           # Avoid race condition. The session might have been
00388           # deleted in the meantime. But make sure everything is deleted.
00389           self.deleteSession(session_id)
00390         else:
00391           # Check expiration time
00392           if session_timestamp+self.expireRemove<current_time:
00393             self.deleteSession(session_id)
00394             result += 1
00395     return result
00396 
00397 # Initialization
00398 random.seed()