Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/branches/cherrypy-2.x/cherrypy/filters/sessionfilter.py

Revision 2662 (checked in by fs, 5 months ago)

remove DeprecationWarnings? on Python 2.6+ by importing hashlib first (instead of the md5/sha modules)

Patch by Toshio Kuratomi, imported from Fedora CVS, licensed under the same terms as CherryPy 2.3

  • Property svn:eol-style set to native
Line 
1 """ Session implementation for CherryPy.
2 We use cherrypy.request to store some convenient variables as
3 well as data about the session for the current request. Instead of
4 polluting cherrypy.request we use a dummy object called
5 cherrypy.request._session (sess) to store these variables.
6
7 Variables used to store config options:
8     - sess.session_timeout: timeout delay for the session
9     - sess.session_locking: mechanism used to lock the session ('implicit' or 'explicit')
10
11 Variables used to store temporary variables:
12     - sess.session_storage (instance of the class implementing the backend)
13
14
15 Variables used to store the session for the current request:
16     - sess.session_data: dictionary containing the actual session data
17     - sess.session_id: current session ID
18     - sess.expiration_time: date/time when the current session will expire
19
20 Global variables (RAM backend only):
21     - cherrypy._session_lock_dict: dictionary containing the locks for all session_id
22     - cherrypy._session_data_holder: dictionary containing the data for all sessions
23
24 """
25
26 import datetime
27 import os
28 try:
29     import cPickle as pickle
30 except ImportError:
31     import pickle
32 import random
33 try:
34     from hashlib import sha1
35 except ImportError:
36     from sha import new as sha1
37
38 import time
39 import thread
40 import threading
41 import types
42
43 import cherrypy
44 import basefilter
45 from cherrypy.lib import httptools
46
47
48 class EmptyClass:
49     """ An empty class """
50     pass
51
52
53 class SessionDeadlockError(Exception):
54     """ The session could not acquire a lock after a certain time """
55     pass
56
57
58 class SessionNotEnabledError(Exception):
59     """ User forgot to set session_filter.on to True """
60     pass
61
62 class SessionStoragePathNotConfiguredError(Exception):
63     """
64     User set storage_type to file but forgot to set the storage_path
65     """
66     pass
67
68
69 class SessionFilter(basefilter.BaseFilter):
70
71     def on_start_resource(self):
72         cherrypy.request._session = EmptyClass()
73    
74     def before_request_body(self):
75         conf = cherrypy.config.get
76        
77         sess = cherrypy.request._session
78         if not conf('session_filter.on', False):
79             sess.session_storage = None
80             return
81
82         sess.locked = False # Not locked by default
83         sess.to_be_loaded = True
84        
85         # Read config options
86         sess.session_timeout = conf('session_filter.timeout', 60)
87         sess.session_locking = conf('session_filter.locking', 'explicit')
88         sess.on_create_session = conf('session_filter.on_create_session',
89                 lambda data: None)
90         sess.on_renew_session = conf('session_filter.on_renew_session',
91                 lambda data: None)
92         sess.on_delete_session = conf('session_filter.on_delete_session',
93                 lambda data: None)
94         sess.generate_session_id = conf('session_filter.generate_session_id',
95                 generate_session_id)
96        
97         clean_up_delay = conf('session_filter.clean_up_delay', 5)
98         clean_up_delay = datetime.timedelta(seconds = clean_up_delay * 60)
99
100         cookie_name = conf('session_filter.cookie_name', 'session_id')
101         cookie_domain = conf('session_filter.cookie_domain', None)
102         cookie_secure = conf('session_filter.cookie_secure', False)
103         cookie_path = conf('session_filter.cookie_path', None)
104
105         if cookie_path is None:
106             cookie_path_header = conf('session_filter.cookie_path_from_header', None)
107             if cookie_path_header is not None:
108                 cookie_path = cherrypy.request.headerMap.get(cookie_path_header, None)
109             if cookie_path is None:
110                 cookie_path = '/'
111
112         sess.deadlock_timeout = conf('session_filter.deadlock_timeout', 30)
113        
114         storage = conf('session_filter.storage_type', 'Ram')
115         storage = storage[0].upper() + storage[1:]
116        
117         # People can set their own custom class
118         #   through session_filter.storage_class
119         sess.session_storage = conf('session_filter.storage_class', None)
120         if sess.session_storage is None:
121             sess.session_storage = globals()[storage + 'Storage']()
122         else:
123             sess.session_storage = sess.session_storage()
124        
125         now = datetime.datetime.now()
126         # Check if we need to clean up old sessions
127         if cherrypy._session_last_clean_up_time + clean_up_delay < now:
128             cherrypy._session_last_clean_up_time = now
129             # Run clean_up function in other thread to avoid blocking
130             #   this request
131             thread.start_new_thread(sess.session_storage.clean_up, (sess,))
132        
133         # Check if request came with a session ID
134         if cookie_name in cherrypy.request.simple_cookie:
135             # It did: we mark the data as needing to be loaded
136             sess.session_id = cherrypy.request.simple_cookie[cookie_name].value
137            
138             # If using implicit locking, acquire lock
139             if sess.session_locking == 'implicit':
140                 sess.session_data = {'_id': sess.session_id}
141                 sess.session_storage.acquire_lock()
142            
143             sess.to_be_loaded = True
144
145         else:
146             # No session_id yet
147             id = None
148             while id is None:
149                 id = sess.generate_session_id()
150                 # Assert that the generated id is not already stored.
151                 if sess.session_storage.load(id) is not None:
152                     id = None
153             sess.session_id = id
154            
155             sess.session_data = {'_id': sess.session_id}
156             sess.on_create_session(sess.session_data)
157         # Set response cookie
158         cookie = cherrypy.response.simple_cookie
159         cookie[cookie_name] = sess.session_id
160         cookie[cookie_name]['path'] = cookie_path
161         # We'd like to use the "max-age" param as
162         #   http://www.faqs.org/rfcs/rfc2109.html indicates but IE doesn't
163         #   save it to disk and the session is lost if people close
164         #   the browser
165         #   So we have to use the old "expires" ... sigh ...
166         #cookie[cookie_name]['max-age'] = sess.session_timeout * 60
167         if sess.session_timeout:
168             gmt_expiration_time = time.gmtime(time.time() +
169                                               (sess.session_timeout * 60))
170             cookie[cookie_name]['expires'] = httptools.HTTPDate(gmt_expiration_time)
171         if cookie_domain is not None:
172             cookie[cookie_name]['domain'] = cookie_domain
173         if cookie_secure is True:
174             cookie[cookie_name]['secure'] = 1
175    
176     def before_finalize(self):
177         def saveData(body, sess):
178             # If the body is a generator, we have to save the data
179             #   *after* the generator has been consumed
180             if isinstance(body, types.GeneratorType):
181                 for line in body:
182                     yield line
183            
184             # Save session data
185             if sess.to_be_loaded is False:
186                 t = datetime.timedelta(seconds = sess.session_timeout * 60)
187                 expiration_time = datetime.datetime.now() + t
188                 sess.session_storage.save(sess.session_id,
189                         sess.session_data, expiration_time)
190             else:
191                 # If session data has never been loaded then it's never been
192                 #   accesses: not need to delete it
193                 pass
194             if sess.locked:
195                 # Always release the lock if the user didn't release it
196                 sess.session_storage.release_lock()
197            
198             # If the body is not a generator, we save the data
199             #   before the body is returned
200             if not isinstance(body, types.GeneratorType):
201                 for line in body:
202                     yield line
203        
204         sess = cherrypy.request._session
205         if not getattr(sess, 'session_storage', None):
206             # Sessions are not enabled: do nothing
207             return
208        
209         # Make a wrapper around the body in order to save the session
210         #   either before or after the body is returned
211         cherrypy.response.body = saveData(cherrypy.response.body, sess)
212    
213     def on_end_request(self):
214         sess = cherrypy.request._session
215         if not getattr(sess, 'session_storage', None):
216             # Sessions are not enabled: do nothing
217             return
218         if getattr(sess, 'locked', None):
219             # If the session is still locked we release the lock
220             sess.session_storage.release_lock()
221         if getattr(sess, 'session_storage', None):
222             del sess.session_storage
223
224
225 class RamStorage:
226     """ Implementation of the RAM backend for sessions """
227    
228     def load(self, id):
229         return cherrypy._session_data_holder.get(id)
230    
231     def save(self, id, data, expiration_time):
232         cherrypy._session_data_holder[id] = (data, expiration_time)
233
234     def delete(self, id=None):
235         if id is None:
236             id = cherrypy.session.id
237         del cherrypy._session_data_holder[id]
238
239     def acquire_lock(self):
240         sess = cherrypy.request._session
241         id = cherrypy.session.id
242         lock = cherrypy._session_lock_dict.get(id)
243         if lock is None:
244             lock = threading.Lock()
245             cherrypy._session_lock_dict[id] = lock
246         startTime = time.time()
247         while True:
248             if lock.acquire(False):
249                 break
250             if time.time() - startTime > sess.deadlock_timeout:
251                 raise SessionDeadlockError()
252             time.sleep(0.5)
253         sess.locked = True
254    
255     def release_lock(self):
256         sess = cherrypy.request._session
257         id = cherrypy.session['_id']
258         cherrypy._session_lock_dict[id].release()
259         sess.locked = False
260    
261     def clean_up(self, sess):
262         to_be_deleted = []
263         now = datetime.datetime.now()
264         for id, (data, expiration_time) in cherrypy._session_data_holder.iteritems():
265             if expiration_time < now:
266                 to_be_deleted.append(id)
267         for id in to_be_deleted:
268             try:
269                 deleted_session = cherrypy._session_data_holder[id]
270                 del cherrypy._session_data_holder[id]
271                 sess.on_delete_session(deleted_session)
272             except KeyError:
273                 # The session probably got deleted by a concurrent thread
274                 #   Safe to ignore this case
275                 pass
276
277
278 class FileStorage:
279     """ Implementation of the File backend for sessions """
280    
281     SESSION_PREFIX = 'session-'
282     LOCK_SUFFIX = '.lock'
283    
284     def load(self, id):
285         file_path = self._get_file_path(id)
286         try:
287             f = open(file_path, "rb")
288             data = pickle.load(f)
289             f.close()
290             return data
291         except (IOError, EOFError):
292             return None
293    
294     def save(self, id, data, expiration_time):
295         file_path = self._get_file_path(id)
296         f = open(file_path, "wb")
297         pickle.dump((data, expiration_time), f)
298         f.close()
299    
300     def delete(self, id=None):
301         if id is None:
302             id = cherrypy.session.id
303         file_path = self._get_file_path(id)
304         try:
305             os.unlink(file_path)
306         except:
307             pass
308        
309     def acquire_lock(self):
310         sess = cherrypy.request._session
311         if not sess.locked:
312             file_path = self._get_file_path(cherrypy.session.id)
313             self._lock_file(file_path + self.LOCK_SUFFIX)
314             sess.locked = True
315    
316     def release_lock(self):
317         sess = cherrypy.request._session
318         file_path = self._get_file_path(cherrypy.session.id)
319         self._unlock_file(file_path + self.LOCK_SUFFIX)
320         sess.locked = False
321    
322     def clean_up(self, sess):
323         storage_path = cherrypy.config.get('session_filter.storage_path')
324         if storage_path is None:
325             return
326         now = datetime.datetime.now()
327         # Iterate over all files in the dir/ and exclude non session files
328         #   and lock files
329         for fname in os.listdir(storage_path):
330             if (fname.startswith(self.SESSION_PREFIX)
331                 and not fname.endswith(self.LOCK_SUFFIX)):
332                 # We have a session file: try to load it and check
333                 #   if it's expired. If it fails, nevermind.
334                 file_path = os.path.join(storage_path, fname)
335                 try:
336                     f = open(file_path, "rb")
337                     data, expiration_time = pickle.load(f)
338                     f.close()
339                     if expiration_time < now:
340                         # Session expired: deleting it
341                         id = fname[len(self.SESSION_PREFIX):]
342                         sess.on_delete_session(data)
343                         os.unlink(file_path)
344                 except:
345                     # We can't access the file ... nevermind
346                     pass
347    
348     def _get_file_path(self, id):
349         storage_path = cherrypy.config.get('session_filter.storage_path')
350         if storage_path is None:
351             raise SessionStoragePathNotConfiguredError()
352         fileName = self.SESSION_PREFIX + id
353         file_path = os.path.join(storage_path, fileName)
354         if not os.path.normpath(file_path).startswith(storage_path):
355             raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
356         return file_path
357    
358     def _lock_file(self, path):
359         sess = cherrypy.request._session
360         startTime = time.time()
361         while True:
362             try:
363                 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
364             except OSError:
365                 if time.time() - startTime > sess.deadlock_timeout:
366                     raise SessionDeadlockError()
367                 time.sleep(0.5)
368             else:
369                 os.close(lockfd)
370                 break
371    
372     def _unlock_file(self, path):
373         os.unlink(path)
374
375
376 class PostgreSQLStorage:
377     """ Implementation of the PostgreSQL backend for sessions. It assumes
378         a table like this:
379
380             create table session (
381                 id varchar(40),
382                 data text,
383                 expiration_time timestamp
384             )
385     """
386    
387     def __init__(self):
388         self.db = cherrypy.config.get('session_filter.get_db')()
389         self.cursor = self.db.cursor()
390    
391     def __del__(self):
392         if self.cursor:
393             self.cursor.close()
394         self.db.commit()
395    
396     def load(self, id):
397         # Select session data from table
398         self.cursor.execute(
399             'select data, expiration_time from session where id=%s',
400             (id,))
401         rows = self.cursor.fetchall()
402         if not rows:
403             return None
404         pickled_data, expiration_time = rows[0]
405         # Unpickle data
406         data = pickle.loads(pickled_data)
407         return (data, expiration_time)
408
409     def delete(self, id=None):
410         if id is None:
411             id = cherrypy.session.id
412         self.cursor.execute('delete from session where id=%s', (id,))
413    
414     def save(self, id, data, expiration_time):
415         # Try to delete session if it was already there
416         self.cursor.execute(
417             'delete from session where id=%s',
418             (id,))
419         # Pickle data
420         pickled_data = pickle.dumps(data)
421         # Insert new session data
422         self.cursor.execute(
423             'insert into session (id, data, expiration_time) values (%s, %s, %s)',
424             (id, pickled_data, expiration_time))
425    
426     def acquire_lock(self):
427         # We use the "for update" clause to lock the row
428         self.cursor.execute(
429             'select id from session where id=%s for update',
430             (cherrypy.session.id,))
431    
432     def release_lock(self):
433         # We just close the cursor and that will remove the lock
434         #   introduced by the "for update" clause
435         self.cursor.close()
436         self.cursor = None
437    
438     def clean_up(self, sess):
439         now = datetime.datetime.now()
440         self.cursor.execute(
441             'select data from session where expiration_time < %s',
442             (now,))
443         rows = self.cursor.fetchall()
444         for row in rows:
445             sess.on_delete_session(row[0])
446         self.cursor.execute(
447             'delete from session where expiration_time < %s',
448             (now,))
449
450
451 try:
452     os.urandom(20)
453 except (AttributeError, NotImplementedError):
454     # os.urandom not available until Python 2.4. Fall back to random.random.
455     def generate_session_id():
456         """Return a new session id."""
457         return sha1('%s' % random.random()).hexdigest()
458 else:
459     def generate_session_id():
460         """Return a new session id."""
461         return os.urandom(20).encode('hex')
462
463 generateSessionID = generate_session_id
464
465 # Users access sessions through cherrypy.session, but we want this
466 #   to be thread-specific so we use a special wrapper that forwards
467 #   calls to cherrypy.session to a thread-specific dictionary called
468 #   cherrypy.request._session.session_data
469 class SessionWrapper:
470    
471     def __getattr__(self, name):
472         sess = cherrypy.request._session
473         if sess.session_storage is None:
474             raise SessionNotEnabledError()
475         # Create thread-specific dictionary if needed
476         session_data = getattr(sess, 'session_data', None)
477         if session_data is None:
478             sess.session_data = {}
479         if name == 'acquire_lock':
480             return sess.session_storage.acquire_lock
481         elif name == 'release_lock':
482             return sess.session_storage.release_lock
483         elif name == 'id':
484             return sess.session_id
485         elif name == 'delete':
486             return sess.session_storage.delete
487
488         if sess.to_be_loaded:
489             data = sess.session_storage.load(sess.session_id)
490             # data is either None or a tuple (session_data, expiration_time)
491             if data is None or data[1] < datetime.datetime.now():
492                 # Expired session:
493                 # flush session data (but keep the same session_id)
494                 sess.session_data = {'_id': sess.session_id}
495                 if not (data is None):
496                     sess.on_renew_session(sess.session_data)
497             else:
498                 sess.session_data = data[0]
499             sess.to_be_loaded = False
500
501         return getattr(sess.session_data, name)
502
503 def expire():
504     """Expire the current session cookie."""
505     name = cherrypy.config.get('session_filter.cookie_name', 'session_id')
506     one_year = 60 * 60 * 24 * 365
507     exp = time.gmtime(time.time() - one_year)
508     t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp)
509     cherrypy.response.simple_cookie[name]['expires'] = t
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets