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 f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) 264 if not os.path.normpath(f).startswith(self.storage_path): 265 raise cherrypy.HTTPError(400, "Invalid session id in cookie.") 266 return f
267
268 - def _load(self, path=None):
269 if path is None: 270 path = self._get_file_path() 271 try: 272 f = open(path, "rb") 273 try: 274 return pickle.load(f) 275 finally: 276 f.close() 277 except (IOError, EOFError): 278 return None
279
280 - def _save(self, expiration_time):
281 f = open(self._get_file_path(), "wb") 282 try: 283 pickle.dump((self._data, expiration_time), f) 284 finally: 285 f.close()
286
287 - def _delete(self):
288 try: 289 os.unlink(self._get_file_path()) 290 except OSError: 291 pass
292
293 - def acquire_lock(self, path=None):
294 if path is None: 295 path = self._get_file_path() 296 path += self.LOCK_SUFFIX 297 while True: 298 try: 299 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 300 except OSError: 301 time.sleep(0.1) 302 else: 303 os.close(lockfd) 304 break 305 self.locked = True
306
307 - def release_lock(self, path=None):
308 if path is None: 309 path = self._get_file_path() 310 os.unlink(path + self.LOCK_SUFFIX) 311 self.locked = False
312
313 - def clean_up(self):
314 """Clean up expired sessions.""" 315 now = datetime.datetime.now() 316 # Iterate over all session files in self.storage_path 317 for fname in os.listdir(self.storage_path): 318 if (fname.startswith(self.SESSION_PREFIX) 319 and not fname.endswith(self.LOCK_SUFFIX)): 320 # We have a session file: lock and load it and check 321 # if it's expired. If it fails, nevermind. 322 path = os.path.join(self.storage_path, fname) 323 self.acquire_lock(path) 324 try: 325 contents = self._load(path) 326 # _load returns None on IOError 327 if contents is not None: 328 data, expiration_time = contents 329 if expiration_time < now: 330 # Session expired: deleting it 331 os.unlink(path) 332 finally: 333 self.release_lock(path)
334 335
336 -class PostgresqlSession(Session):
337 """ Implementation of the PostgreSQL backend for sessions. It assumes 338 a table like this: 339 340 create table session ( 341 id varchar(40), 342 data text, 343 expiration_time timestamp 344 ) 345 346 You must provide your own get_db function. 347 """ 348
349 - def __init__(self):
350 self.db = self.get_db() 351 self.cursor = self.db.cursor()
352
353 - def __del__(self):
354 if self.cursor: 355 self.cursor.close() 356 self.db.commit()
357
358 - def _load(self):
359 # Select session data from table 360 self.cursor.execute('select data, expiration_time from session ' 361 'where id=%s', (self.id,)) 362 rows = self.cursor.fetchall() 363 if not rows: 364 return None 365 366 pickled_data, expiration_time = rows[0] 367 data = pickle.loads(pickled_data) 368 return data, expiration_time
369
370 - def _save(self, expiration_time):
371 pickled_data = pickle.dumps(self._data) 372 self.cursor.execute('update session set data = %s, ' 373 'expiration_time = %s where id = %s', 374 (pickled_data, expiration_time, self.id))
375
376 - def _delete(self):
377 self.cursor.execute('delete from session where id=%s', (self.id,))
378
379 - def acquire_lock(self):
380 # We use the "for update" clause to lock the row 381 self.locked = True 382 self.cursor.execute('select id from session where id=%s for update', 383 (self.id,))
384
385 - def release_lock(self):
386 # We just close the cursor and that will remove the lock 387 # introduced by the "for update" clause 388 self.cursor.close() 389 self.locked = False
390
391 - def clean_up(self):
392 """Clean up expired sessions.""" 393 self.cursor.execute('delete from session where expiration_time < %s', 394 (datetime.datetime.now(),))
395 396 397 # Hook functions (for CherryPy tools) 398
399 -def save():
400 """Save any changed session data.""" 401 # Guard against running twice 402 if hasattr(cherrypy.request, "_sessionsaved"): 403 return 404 cherrypy.request._sessionsaved = True 405 406 if cherrypy.response.stream: 407 # If the body is being streamed, we have to save the data 408 # *after* the response has been written out 409 cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save) 410 else: 411 # If the body is not being streamed, we save the data now 412 # (so we can release the lock). 413 if isinstance(cherrypy.response.body, types.GeneratorType): 414 cherrypy.response.collapse_body() 415 cherrypy.session.save()
416 save.failsafe = True 417
418 -def close():
419 """Close the session object for this request.""" 420 sess = cherrypy.session 421 if sess.locked: 422 # If the session is still locked we release the lock 423 sess.release_lock()
424 close.failsafe = True 425 close.priority = 90 426 427
428 -def init(storage_type='ram', path=None, path_header=None, name='session_id', 429 timeout=60, domain=None, secure=False, clean_freq=5, **kwargs):
430 """Initialize session object (using cookies). 431 432 storage_type: one of 'ram', 'file', 'postgresql'. This will be used 433 to look up the corresponding class in cherrypy.lib.sessions 434 globals. For example, 'file' will use the FileSession class. 435 path: the 'path' value to stick in the response cookie metadata. 436 path_header: if 'path' is None (the default), then the response 437 cookie 'path' will be pulled from request.headers[path_header]. 438 name: the name of the cookie. 439 timeout: the expiration timeout for the cookie. 440 domain: the cookie domain. 441 secure: if False (the default) the cookie 'secure' value will not 442 be set. If True, the cookie 'secure' value will be set (to 1). 443 clean_freq (minutes): the poll rate for expired session cleanup. 444 445 Any additional kwargs will be bound to the new Session instance, 446 and may be specific to the storage type. See the subclass of Session 447 you're using for more information. 448 """ 449 450 request = cherrypy.request 451 452 # Guard against running twice 453 if hasattr(request, "_session_init_flag"): 454 return 455 request._session_init_flag = True 456 457 # Check if request came with a session ID 458 id = None 459 if name in request.cookie: 460 id = request.cookie[name].value 461 462 # Create and attach a new Session instance to cherrypy._serving. 463 # It will possess a reference to (and lock, and lazily load) 464 # the requested session data. 465 storage_class = storage_type.title() + 'Session' 466 kwargs['timeout'] = timeout 467 kwargs['clean_freq'] = clean_freq 468 cherrypy._serving.session = sess = globals()[storage_class](id, **kwargs) 469 470 if not hasattr(cherrypy, "session"): 471 cherrypy.session = cherrypy._ThreadLocalProxy('session') 472 if hasattr(sess, "setup"): 473 sess.setup() 474 475 # Set response cookie 476 cookie = cherrypy.response.cookie 477 cookie[name] = sess.id 478 cookie[name]['path'] = path or request.headers.get(path_header) or '/' 479 480 # We'd like to use the "max-age" param as indicated in 481 # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't 482 # save it to disk and the session is lost if people close 483 # the browser. So we have to use the old "expires" ... sigh ... 484 ## cookie[name]['max-age'] = timeout * 60 485 if timeout: 486 cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60)) 487 if domain is not None: 488 cookie[name]['domain'] = domain 489 if secure: 490 cookie[name]['secure'] = 1
491
492 -def expire():
493 """Expire the current session cookie.""" 494 name = cherrypy.request.config.get('tools.sessions.name', 'session_id') 495 one_year = 60 * 60 * 24 * 365 496 exp = time.gmtime(time.time() - one_year) 497 t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp) 498 cherrypy.response.cookie[name]['expires'] = t
499