Package cherrypy :: Package lib :: Module sessions
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.sessions

  1  """Session implementation for CherryPy. 
  2   
  3  We use cherrypy.request to store some convenient variables as 
  4  well as data about the session for the current request. Instead of 
  5  polluting cherrypy.request we use a Session object bound to 
  6  cherrypy.session to store these variables. 
  7  """ 
  8   
  9  import datetime 
 10  import os 
 11  try: 
 12      import cPickle as pickle 
 13  except ImportError: 
 14      import pickle 
 15  import random 
 16  import sha 
 17  import time 
 18  import threading 
 19  import types 
 20  from warnings import warn 
 21   
 22  import cherrypy 
 23  from cherrypy.lib import http 
 24   
 25   
26 -class PerpetualTimer(threading._Timer):
27
28 - def run(self):
29 while True: 30 self.finished.wait(self.interval) 31 if self.finished.isSet(): 32 return 33 self.function(*self.args, **self.kwargs)
34 35 36 missing = object() 37
38 -class Session(object):
39 """A CherryPy dict-like Session object (one per request).""" 40 41 __metaclass__ = cherrypy._AttributeDocstrings 42 43 id = None 44 id__doc = "The current session ID." 45 46 timeout = 60 47 timeout__doc = "Number of minutes after which to delete session data." 48 49 locked = False 50 locked__doc = """ 51 If True, this session instance has exclusive read/write access 52 to session data.""" 53 54 loaded = False 55 loaded__doc = """ 56 If True, data has been retrieved from storage. This should happen 57 automatically on the first attempt to access session data.""" 58 59 clean_thread = None 60 clean_thread__doc = "Class-level PerpetualTimer which calls self.clean_up." 61 62 clean_freq = 5 63 clean_freq__doc = "The poll rate for expired session cleanup in minutes." 64
65 - def __init__(self, id=None, **kwargs):
66 self._data = {} 67 68 for k, v in kwargs.iteritems(): 69 setattr(self, k, v) 70 71 self.id = id 72 while self.id is None: 73 self.id = self.generate_id() 74 # Assert that the generated id is not already stored. 75 if self._load() is not None: 76 self.id = None
77
78 - def clean_interrupt(cls):
79 """Stop the expired-session cleaning timer.""" 80 if cls.clean_thread: 81 cls.clean_thread.cancel() 82 cls.clean_thread.join() 83 cls.clean_thread = None
84 clean_interrupt = classmethod(clean_interrupt) 85
86 - def clean_up(self):
87 """Clean up expired sessions.""" 88 pass
89 90 try: 91 os.urandom(20) 92 except (AttributeError, NotImplementedError): 93 # os.urandom not available until Python 2.4. Fall back to random.random.
94 - def generate_id(self):
95 """Return a new session id.""" 96 return sha.new('%s' % random.random()).hexdigest()
97 else:
98 - def generate_id(self):
99 """Return a new session id.""" 100 return os.urandom(20).encode('hex')
101
102 - def save(self):
103 """Save session data.""" 104 try: 105 # If session data has never been loaded then it's never been 106 # accessed: no need to delete it 107 if self.loaded: 108 t = datetime.timedelta(seconds = self.timeout * 60) 109 expiration_time = datetime.datetime.now() + t 110 self._save(expiration_time) 111 112 finally: 113 if self.locked: 114 # Always release the lock if the user didn't release it 115 self.release_lock()
116
117 - def load(self):
118 """Copy stored session data into this session instance.""" 119 data = self._load() 120 # data is either None or a tuple (session_data, expiration_time) 121 if data is None or data[1] < datetime.datetime.now(): 122 # Expired session: flush session data (but keep the same id) 123 self._data = {} 124 else: 125 self._data = data[0] 126 self.loaded = True 127 128 # Stick the clean_thread in the class, not the instance. 129 # The instances are created and destroyed per-request. 130 cls = self.__class__ 131 if not cls.clean_thread: 132 cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt) 133 # clean_up is in instancemethod and not a classmethod, 134 # so tool config can be accessed inside the method. 135 t = PerpetualTimer(self.clean_freq, self.clean_up) 136 t.setName("CP Session Cleanup") 137 cls.clean_thread = t 138 t.start()
139
140 - def delete(self):
141 """Delete stored session data.""" 142 self._delete()
143
144 - def __getitem__(self, key):
145 if not self.loaded: self.load() 146 return self._data[key]
147
148 - def __setitem__(self, key, value):
149 if not self.loaded: self.load() 150 self._data[key] = value
151
152 - def __delitem__(self, key):
153 if not self.loaded: self.load() 154 del self._data[key]
155
156 - def pop(self, key, default=missing):
157 if not self.loaded: self.load() 158 if default is missing: 159 return self._data.pop(key) 160 else: 161 return self._data.pop(key, default)
162
163 - def __contains__(self, key):
164 if not self.loaded: self.load() 165 return key in self._data
166
167 - def has_key(self, key):
168 if not self.loaded: self.load() 169 return self._data.has_key(key)
170
171 - def get(self, key, default=None):
172 if not self.loaded: self.load() 173 return self._data.get(key, default)
174
175 - def update(self, d):
176 if not self.loaded: self.load() 177 self._data.update(d)
178
179 - def setdefault(self, key, default=None):
180 if not self.loaded: self.load() 181 return self._data.setdefault(key, default)
182
183 - def clear(self):
184 if not self.loaded: self.load() 185 self._data.clear()
186
187 - def keys(self):
188 if not self.loaded: self.load() 189 return self._data.keys()
190
191 - def items(self):
192 if not self.loaded: self.load() 193 return self._data.items()
194
195 - def values(self):
196 if not self.loaded: self.load() 197 return self._data.values()
198 199
200 -class RamSession(Session):
201 202 # Class-level objects. Don't rebind these! 203 cache = {} 204 locks = {} 205
206 - def clean_up(self):
207 """Clean up expired sessions.""" 208 now = datetime.datetime.now() 209 for id, (data, expiration_time) in self.cache.items(): 210 if expiration_time < now: 211 try: 212 del self.cache[id] 213 except KeyError: 214 pass 215 try: 216 del self.locks[id] 217 except KeyError: 218 pass
219
220 - def _load(self):
221 return self.cache.get(self.id)
222
223 - def _save(self, expiration_time):
224 self.cache[self.id] = (self._data, expiration_time)
225
226 - def _delete(self):
227 del self.cache[self.id]
228
229 - def acquire_lock(self):
230 self.locked = True 231 self.locks.setdefault(self.id, threading.RLock()).acquire()
232
233 - def release_lock(self):
234 self.locks[self.id].release() 235 self.locked = False
236 237
238 -class FileSession(Session):
239 """ Implementation of the File backend for sessions 240 241 storage_path: the folder where session data will be saved. Each session 242 will be saved as pickle.dump(data, expiration_time) in its own file; 243 the filename will be self.SESSION_PREFIX + self.id. 244 """ 245 246 SESSION_PREFIX = 'session-' 247 LOCK_SUFFIX = '.lock' 248
249 - def setup(self):
250 # Warn if any lock files exist at startup. 251 lockfiles = [fname for fname in os.listdir(self.storage_path) 252 if (fname.startswith(self.SESSION_PREFIX) 253 and fname.endswith(self.LOCK_SUFFIX))] 254 if lockfiles: 255 plural = ('', 's')[len(lockfiles) > 1] 256 warn("%s session lockfile%s found at startup. If you are " 257 "only running one process, then you may need to " 258 "manually delete the lockfiles found at %r." 259 % (len(lockfiles), plural, 260 os.path.abspath(self.storage_path)))
261
262 - def _get_file_path(self):
263 return os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
264
265 - def _load(self, path=None):
266 if path is None: 267 path = self._get_file_path() 268 try: 269 f = open(path, "rb") 270 try: 271 return pickle.load(f) 272 finally: 273 f.close() 274 except (IOError, EOFError): 275 return None
276
277 - def _save(self, expiration_time):
278 f = open(self._get_file_path(), "wb") 279 try: 280 pickle.dump((self._data, expiration_time), f) 281 finally: 282 f.close()
283
284 - def _delete(self):
285 try: 286 os.unlink(self._get_file_path()) 287 except OSError: 288 pass
289
290 - def acquire_lock(self, path=None):
291 if path is None: 292 path = self._get_file_path() 293 path += self.LOCK_SUFFIX 294 while True: 295 try: 296 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 297 except OSError: 298 time.sleep(0.1) 299 else: 300 os.close(lockfd) 301 break 302 self.locked = True
303
304 - def release_lock(self, path=None):
305 if path is None: 306 path = self._get_file_path() 307 os.unlink(path + self.LOCK_SUFFIX) 308 self.locked = False
309
310 - def clean_up(self):
311 """Clean up expired sessions.""" 312 now = datetime.datetime.now() 313 # Iterate over all session files in self.storage_path 314 for fname in os.listdir(self.storage_path): 315 if (fname.startswith(self.SESSION_PREFIX) 316 and not fname.endswith(self.LOCK_SUFFIX)): 317 # We have a session file: lock and load it and check 318 # if it's expired. If it fails, nevermind. 319 path = os.path.join(self.storage_path, fname) 320 self.acquire_lock(path) 321 try: 322 contents = self._load(path) 323 # _load returns None on IOError 324 if contents is not None: 325 data, expiration_time = contents 326 if expiration_time < now: 327 # Session expired: deleting it 328 os.unlink(path) 329 finally: 330 self.release_lock(path)
331 332
333 -class PostgresqlSession(Session):
334 """ Implementation of the PostgreSQL backend for sessions. It assumes 335 a table like this: 336 337 create table session ( 338 id varchar(40), 339 data text, 340 expiration_time timestamp 341 ) 342 343 You must provide your own get_db function. 344 """ 345
346 - def __init__(self):
347 self.db = self.get_db() 348 self.cursor = self.db.cursor()
349
350 - def __del__(self):
351 if self.cursor: 352 self.cursor.close() 353 self.db.commit()
354
355 - def _load(self):
356 # Select session data from table 357 self.cursor.execute('select data, expiration_time from session ' 358 'where id=%s', (self.id,)) 359 rows = self.cursor.fetchall() 360 if not rows: 361 return None 362 363 pickled_data, expiration_time = rows[0] 364 data = pickle.loads(pickled_data) 365 return data, expiration_time
366
367 - def _save(self, expiration_time):
368 pickled_data = pickle.dumps(self._data) 369 self.cursor.execute('update session set data = %s, ' 370 'expiration_time = %s where id = %s', 371 (pickled_data, expiration_time, self.id))
372
373 - def _delete(self):
374 self.cursor.execute('delete from session where id=%s', (self.id,))
375
376 - def acquire_lock(self):
377 # We use the "for update" clause to lock the row 378 self.locked = True 379 self.cursor.execute('select id from session where id=%s for update', 380 (self.id,))
381
382 - def release_lock(self):
383 # We just close the cursor and that will remove the lock 384 # introduced by the "for update" clause 385 self.cursor.close() 386 self.locked = False
387
388 - def clean_up(self):
389 """Clean up expired sessions.""" 390 self.cursor.execute('delete from session where expiration_time < %s', 391 (datetime.datetime.now(),))
392 393 394 # Hook functions (for CherryPy tools) 395
396 -def save():
397 """Save any changed session data.""" 398 # Guard against running twice 399 if hasattr(cherrypy.request, "_sessionsaved"): 400 return 401 cherrypy.request._sessionsaved = True 402 403 if cherrypy.response.stream: 404 # If the body is being streamed, we have to save the data 405 # *after* the response has been written out 406 cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save) 407 else: 408 # If the body is not being streamed, we save the data now 409 # (so we can release the lock). 410 if isinstance(cherrypy.response.body, types.GeneratorType): 411 cherrypy.response.collapse_body() 412 cherrypy.session.save()
413 save.failsafe = True 414
415 -def close():
416 """Close the session object for this request.""" 417 sess = cherrypy.session 418 if sess.locked: 419 # If the session is still locked we release the lock 420 sess.release_lock()
421 close.failsafe = True 422 close.priority = 90 423 424
425 -def init(storage_type='ram', path=None, path_header=None, name='session_id', 426 timeout=60, domain=None, secure=False, clean_freq=5, **kwargs):
427 """Initialize session object (using cookies). 428 429 storage_type: one of 'ram', 'file', 'postgresql'. This will be used 430 to look up the corresponding class in cherrypy.lib.sessions 431 globals. For example, 'file' will use the FileSession class. 432 path: the 'path' value to stick in the response cookie metadata. 433 path_header: if 'path' is None (the default), then the response 434 cookie 'path' will be pulled from request.headers[path_header]. 435 name: the name of the cookie. 436 timeout: the expiration timeout for the cookie. 437 domain: the cookie domain. 438 secure: if False (the default) the cookie 'secure' value will not 439 be set. If True, the cookie 'secure' value will be set (to 1). 440 clean_freq (minutes): the poll rate for expired session cleanup. 441 442 Any additional kwargs will be bound to the new Session instance, 443 and may be specific to the storage type. See the subclass of Session 444 you're using for more information. 445 """ 446 447 request = cherrypy.request 448 449 # Guard against running twice 450 if hasattr(request, "_session_init_flag"): 451 return 452 request._session_init_flag = True 453 454 # Check if request came with a session ID 455 id = None 456 if name in request.cookie: 457 id = request.cookie[name].value 458 459 # Create and attach a new Session instance to cherrypy._serving. 460 # It will possess a reference to (and lock, and lazily load) 461 # the requested session data. 462 storage_class = storage_type.title() + 'Session' 463 kwargs['timeout'] = timeout 464 kwargs['clean_freq'] = clean_freq 465 cherrypy._serving.session = sess = globals()[storage_class](id, **kwargs) 466 467 if not hasattr(cherrypy, "session"): 468 cherrypy.session = cherrypy._ThreadLocalProxy('session') 469 if hasattr(sess, "setup"): 470 sess.setup() 471 472 # Set response cookie 473 cookie = cherrypy.response.cookie 474 cookie[name] = sess.id 475 cookie[name]['path'] = path or request.headers.get(path_header) or '/' 476 477 # We'd like to use the "max-age" param as indicated in 478 # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't 479 # save it to disk and the session is lost if people close 480 # the browser. So we have to use the old "expires" ... sigh ... 481 ## cookie[name]['max-age'] = timeout * 60 482 if timeout: 483 cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60)) 484 if domain is not None: 485 cookie[name]['domain'] = domain 486 if secure: 487 cookie[name]['secure'] = 1
488
489 -def expire():
490 """Expire the current session cookie.""" 491 name = cherrypy.request.config.get('tools.sessions.name', 'session_id') 492 one_year = 60 * 60 * 24 * 365 493 exp = time.gmtime(time.time() - one_year) 494 t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp) 495 cherrypy.response.cookie[name]['expires'] = t
496