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   
 17  try: 
 18      # Python 2.5+ 
 19      from hashlib import sha1 as sha 
 20  except ImportError: 
 21      from sha import new as sha 
 22   
 23  import time 
 24  import threading 
 25  import types 
 26  from warnings import warn 
 27   
 28  import cherrypy 
 29  from cherrypy.lib import http 
 30   
 31   
 32  missing = object() 
 33   
34 -class Session(object):
35 """A CherryPy dict-like Session object (one per request).""" 36 37 __metaclass__ = cherrypy._AttributeDocstrings 38 39 _id = None 40 id_observers = None 41 id_observers__doc = "A list of callbacks to which to pass new id's." 42 43 id__doc = "The current session ID."
44 - def _get_id(self):
45 return self._id
46 - def _set_id(self, value):
47 self._id = value 48 for o in self.id_observers: 49 o(value)
50 id = property(_get_id, _set_id, doc=id__doc) 51 52 timeout = 60 53 timeout__doc = "Number of minutes after which to delete session data." 54 55 locked = False 56 locked__doc = """ 57 If True, this session instance has exclusive read/write access 58 to session data.""" 59 60 loaded = False 61 loaded__doc = """ 62 If True, data has been retrieved from storage. This should happen 63 automatically on the first attempt to access session data.""" 64 65 clean_thread = None 66 clean_thread__doc = "Class-level Monitor which calls self.clean_up." 67 68 clean_freq = 5 69 clean_freq__doc = "The poll rate for expired session cleanup in minutes." 70
71 - def __init__(self, id=None, **kwargs):
72 self.id_observers = [] 73 self._data = {} 74 75 for k, v in kwargs.iteritems(): 76 setattr(self, k, v) 77 78 if id is None: 79 self.regenerate() 80 else: 81 self.id = id 82 if not self._exists(): 83 # Expired or malicious session. Make a new one. 84 # See http://www.cherrypy.org/ticket/709. 85 self.id = None 86 self.regenerate()
87
88 - def regenerate(self):
89 """Replace the current session (with a new id).""" 90 if self.id is not None: 91 self.delete() 92 93 old_session_was_locked = self.locked 94 if old_session_was_locked: 95 self.release_lock() 96 97 self.id = None 98 while self.id is None: 99 self.id = self.generate_id() 100 # Assert that the generated id is not already stored. 101 if self._exists(): 102 self.id = None 103 104 if old_session_was_locked: 105 self.acquire_lock()
106
107 - def clean_up(self):
108 """Clean up expired sessions.""" 109 pass
110 111 try: 112 os.urandom(20) 113 except (AttributeError, NotImplementedError): 114 # os.urandom not available until Python 2.4. Fall back to random.random.
115 - def generate_id(self):
116 """Return a new session id.""" 117 return sha('%s' % random.random()).hexdigest()
118 else:
119 - def generate_id(self):
120 """Return a new session id.""" 121 return os.urandom(20).encode('hex')
122
123 - def save(self):
124 """Save session data.""" 125 try: 126 # If session data has never been loaded then it's never been 127 # accessed: no need to delete it 128 if self.loaded: 129 t = datetime.timedelta(seconds = self.timeout * 60) 130 expiration_time = datetime.datetime.now() + t 131 self._save(expiration_time) 132 133 finally: 134 if self.locked: 135 # Always release the lock if the user didn't release it 136 self.release_lock()
137
138 - def load(self):
139 """Copy stored session data into this session instance.""" 140 data = self._load() 141 # data is either None or a tuple (session_data, expiration_time) 142 if data is None or data[1] < datetime.datetime.now(): 143 # Expired session: flush session data 144 self._data = {} 145 else: 146 self._data = data[0] 147 self.loaded = True 148 149 # Stick the clean_thread in the class, not the instance. 150 # The instances are created and destroyed per-request. 151 cls = self.__class__ 152 if self.clean_freq and not cls.clean_thread: 153 # clean_up is in instancemethod and not a classmethod, 154 # so that tool config can be accessed inside the method. 155 t = cherrypy.process.plugins.Monitor( 156 cherrypy.engine, self.clean_up, self.clean_freq * 60) 157 t.subscribe() 158 cls.clean_thread = t 159 t.start()
160
161 - def delete(self):
162 """Delete stored session data.""" 163 self._delete()
164
165 - def __getitem__(self, key):
166 if not self.loaded: self.load() 167 return self._data[key]
168
169 - def __setitem__(self, key, value):
170 if not self.loaded: self.load() 171 self._data[key] = value
172
173 - def __delitem__(self, key):
174 if not self.loaded: self.load() 175 del self._data[key]
176
177 - def pop(self, key, default=missing):
178 """Remove the specified key and return the corresponding value. 179 If key is not found, default is returned if given, 180 otherwise KeyError is raised. 181 """ 182 if not self.loaded: self.load() 183 if default is missing: 184 return self._data.pop(key) 185 else: 186 return self._data.pop(key, default)
187
188 - def __contains__(self, key):
189 if not self.loaded: self.load() 190 return key in self._data
191
192 - def has_key(self, key):
193 """D.has_key(k) -> True if D has a key k, else False.""" 194 if not self.loaded: self.load() 195 return self._data.has_key(key)
196
197 - def get(self, key, default=None):
198 """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" 199 if not self.loaded: self.load() 200 return self._data.get(key, default)
201
202 - def update(self, d):
203 """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" 204 if not self.loaded: self.load() 205 self._data.update(d)
206
207 - def setdefault(self, key, default=None):
208 """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" 209 if not self.loaded: self.load() 210 return self._data.setdefault(key, default)
211
212 - def clear(self):
213 """D.clear() -> None. Remove all items from D.""" 214 if not self.loaded: self.load() 215 self._data.clear()
216
217 - def keys(self):
218 """D.keys() -> list of D's keys.""" 219 if not self.loaded: self.load() 220 return self._data.keys()
221
222 - def items(self):
223 """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" 224 if not self.loaded: self.load() 225 return self._data.items()
226
227 - def values(self):
228 """D.values() -> list of D's values.""" 229 if not self.loaded: self.load() 230 return self._data.values()
231 232
233 -class RamSession(Session):
234 235 # Class-level objects. Don't rebind these! 236 cache = {} 237 locks = {} 238
239 - def clean_up(self):
240 """Clean up expired sessions.""" 241 now = datetime.datetime.now() 242 for id, (data, expiration_time) in self.cache.items(): 243 if expiration_time < now: 244 try: 245 del self.cache[id] 246 except KeyError: 247 pass 248 try: 249 del self.locks[id] 250 except KeyError: 251 pass
252
253 - def _exists(self):
254 return self.id in self.cache
255
256 - def _load(self):
257 return self.cache.get(self.id)
258
259 - def _save(self, expiration_time):
260 self.cache[self.id] = (self._data, expiration_time)
261
262 - def _delete(self):
263 del self.cache[self.id]
264
265 - def acquire_lock(self):
266 """Acquire an exclusive lock on the currently-loaded session data.""" 267 self.locked = True 268 self.locks.setdefault(self.id, threading.RLock()).acquire()
269
270 - def release_lock(self):
271 """Release the lock on the currently-loaded session data.""" 272 self.locks[self.id].release() 273 self.locked = False
274
275 - def __len__(self):
276 """Return the number of active sessions.""" 277 return len(self.cache)
278 279
280 -class FileSession(Session):
281 """Implementation of the File backend for sessions 282 283 storage_path: the folder where session data will be saved. Each session 284 will be saved as pickle.dump(data, expiration_time) in its own file; 285 the filename will be self.SESSION_PREFIX + self.id. 286 """ 287 288 SESSION_PREFIX = 'session-' 289 LOCK_SUFFIX = '.lock' 290
291 - def __init__(self, id=None, **kwargs):
292 # The 'storage_path' arg is required for file-based sessions. 293 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) 294 Session.__init__(self, id=id, **kwargs)
295
296 - def setup(cls, **kwargs):
297 """Set up the storage system for file-based sessions. 298 299 This should only be called once per process; this will be done 300 automatically when using sessions.init (as the built-in Tool does). 301 """ 302 # The 'storage_path' arg is required for file-based sessions. 303 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) 304 305 for k, v in kwargs.iteritems(): 306 setattr(cls, k, v) 307 308 # Warn if any lock files exist at startup. 309 lockfiles = [fname for fname in os.listdir(cls.storage_path) 310 if (fname.startswith(cls.SESSION_PREFIX) 311 and fname.endswith(cls.LOCK_SUFFIX))] 312 if lockfiles: 313 plural = ('', 's')[len(lockfiles) > 1] 314 warn("%s session lockfile%s found at startup. If you are " 315 "only running one process, then you may need to " 316 "manually delete the lockfiles found at %r." 317 % (len(lockfiles), plural, cls.storage_path))
318 setup = classmethod(setup) 319
320 - def _get_file_path(self):
321 f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) 322 if not os.path.abspath(f).startswith(self.storage_path): 323 raise cherrypy.HTTPError(400, "Invalid session id in cookie.") 324 return f
325
326 - def _exists(self):
327 path = self._get_file_path() 328 return os.path.exists(path)
329
330 - def _load(self, path=None):
331 if path is None: 332 path = self._get_file_path() 333 try: 334 f = open(path, "rb") 335 try: 336 return pickle.load(f) 337 finally: 338 f.close() 339 except (IOError, EOFError): 340 return None
341
342 - def _save(self, expiration_time):
343 f = open(self._get_file_path(), "wb") 344 try: 345 pickle.dump((self._data, expiration_time), f) 346 finally: 347 f.close()
348
349 - def _delete(self):
350 try: 351 os.unlink(self._get_file_path()) 352 except OSError: 353 pass
354
355 - def acquire_lock(self, path=None):
356 """Acquire an exclusive lock on the currently-loaded session data.""" 357 if path is None: 358 path = self._get_file_path() 359 path += self.LOCK_SUFFIX 360 while True: 361 try: 362 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 363 except OSError: 364 time.sleep(0.1) 365 else: 366 os.close(lockfd) 367 break 368 self.locked = True
369
370 - def release_lock(self, path=None):
371 """Release the lock on the currently-loaded session data.""" 372 if path is None: 373 path = self._get_file_path() 374 os.unlink(path + self.LOCK_SUFFIX) 375 self.locked = False
376
377 - def clean_up(self):
378 """Clean up expired sessions.""" 379 now = datetime.datetime.now() 380 # Iterate over all session files in self.storage_path 381 for fname in os.listdir(self.storage_path): 382 if (fname.startswith(self.SESSION_PREFIX) 383 and not fname.endswith(self.LOCK_SUFFIX)): 384 # We have a session file: lock and load it and check 385 # if it's expired. If it fails, nevermind. 386 path = os.path.join(self.storage_path, fname) 387 self.acquire_lock(path) 388 try: 389 contents = self._load(path) 390 # _load returns None on IOError 391 if contents is not None: 392 data, expiration_time = contents 393 if expiration_time < now: 394 # Session expired: deleting it 395 os.unlink(path) 396 finally: 397 self.release_lock(path)
398
399 - def __len__(self):
400 """Return the number of active sessions.""" 401 return len([fname for fname in os.listdir(self.storage_path) 402 if (fname.startswith(self.SESSION_PREFIX) 403 and not fname.endswith(self.LOCK_SUFFIX))])
404 405
406 -class PostgresqlSession(Session):
407 """ Implementation of the PostgreSQL backend for sessions. It assumes 408 a table like this: 409 410 create table session ( 411 id varchar(40), 412 data text, 413 expiration_time timestamp 414 ) 415 416 You must provide your own get_db function. 417 """ 418
419 - def __init__(self, id=None, **kwargs):
420 Session.__init__(self, id, **kwargs) 421 self.cursor = self.db.cursor()
422
423 - def setup(cls, **kwargs):
424 """Set up the storage system for Postgres-based sessions. 425 426 This should only be called once per process; this will be done 427 automatically when using sessions.init (as the built-in Tool does). 428 """ 429 for k, v in kwargs.iteritems(): 430 setattr(cls, k, v) 431 432 self.db = self.get_db()
433 setup = classmethod(setup) 434
435 - def __del__(self):
436 if self.cursor: 437 self.cursor.close() 438 self.db.commit()
439
440 - def _exists(self):
441 # Select session data from table 442 self.cursor.execute('select data, expiration_time from session ' 443 'where id=%s', (self.id,)) 444 rows = self.cursor.fetchall() 445 return bool(rows)
446
447 - def _load(self):
448 # Select session data from table 449 self.cursor.execute('select data, expiration_time from session ' 450 'where id=%s', (self.id,)) 451 rows = self.cursor.fetchall() 452 if not rows: 453 return None 454 455 pickled_data, expiration_time = rows[0] 456 data = pickle.loads(pickled_data) 457 return data, expiration_time
458
459 - def _save(self, expiration_time):
460 pickled_data = pickle.dumps(self._data) 461 self.cursor.execute('update session set data = %s, ' 462 'expiration_time = %s where id = %s', 463 (pickled_data, expiration_time, self.id))
464
465 - def _delete(self):
466 self.cursor.execute('delete from session where id=%s', (self.id,))
467
468 - def acquire_lock(self):
469 """Acquire an exclusive lock on the currently-loaded session data.""" 470 # We use the "for update" clause to lock the row 471 self.locked = True 472 self.cursor.execute('select id from session where id=%s for update', 473 (self.id,))
474
475 - def release_lock(self):
476 """Release the lock on the currently-loaded session data.""" 477 # We just close the cursor and that will remove the lock 478 # introduced by the "for update" clause 479 self.cursor.close() 480 self.locked = False
481
482 - def clean_up(self):
483 """Clean up expired sessions.""" 484 self.cursor.execute('delete from session where expiration_time < %s', 485 (datetime.datetime.now(),))
486 487
488 -class MemcachedSession(Session):
489 490 # The most popular memcached client for Python isn't thread-safe. 491 # Wrap all .get and .set operations in a single lock. 492 mc_lock = threading.RLock() 493 494 # This is a seperate set of locks per session id. 495 locks = {} 496 497 servers = ['127.0.0.1:11211'] 498
499 - def setup(cls, **kwargs):
500 """Set up the storage system for memcached-based sessions. 501 502 This should only be called once per process; this will be done 503 automatically when using sessions.init (as the built-in Tool does). 504 """ 505 for k, v in kwargs.iteritems(): 506 setattr(cls, k, v) 507 508 import memcache 509 cls.cache = memcache.Client(cls.servers)
510 setup = classmethod(setup) 511
512 - def _exists(self):
513 self.mc_lock.acquire() 514 try: 515 return bool(self.cache.get(self.id)) 516 finally: 517 self.mc_lock.release()
518
519 - def _load(self):
520 self.mc_lock.acquire() 521 try: 522 return self.cache.get(self.id) 523 finally: 524 self.mc_lock.release()
525
526 - def _save(self, expiration_time):
527 # Send the expiration time as "Unix time" (seconds since 1/1/1970) 528 td = int(time.mktime(expiration_time.timetuple())) 529 self.mc_lock.acquire() 530 try: 531 if not self.cache.set(self.id, (self._data, expiration_time), td): 532 raise AssertionError("Session data for id %r not set." % self.id) 533 finally: 534 self.mc_lock.release()
535
536 - def _delete(self):
537 self.cache.delete(self.id)
538
539 - def acquire_lock(self):
540 """Acquire an exclusive lock on the currently-loaded session data.""" 541 self.locked = True 542 self.locks.setdefault(self.id, threading.RLock()).acquire()
543
544 - def release_lock(self):
545 """Release the lock on the currently-loaded session data.""" 546 self.locks[self.id].release() 547 self.locked = False
548
549 - def __len__(self):
550 """Return the number of active sessions.""" 551 raise NotImplementedError
552 553 554 # Hook functions (for CherryPy tools) 555
556 -def save():
557 """Save any changed session data.""" 558 559 if not hasattr(cherrypy.serving, "session"): 560 return 561 562 # Guard against running twice 563 if hasattr(cherrypy.request, "_sessionsaved"): 564 return 565 cherrypy.request._sessionsaved = True 566 567 if cherrypy.response.stream: 568 # If the body is being streamed, we have to save the data 569 # *after* the response has been written out 570 cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save) 571 else: 572 # If the body is not being streamed, we save the data now 573 # (so we can release the lock). 574 if isinstance(cherrypy.response.body, types.GeneratorType): 575 cherrypy.response.collapse_body() 576 cherrypy.session.save()
577 save.failsafe = True 578
579 -def close():
580 """Close the session object for this request.""" 581 sess = getattr(cherrypy.serving, "session", None) 582 if getattr(sess, "locked", False): 583 # If the session is still locked we release the lock 584 sess.release_lock()
585 close.failsafe = True 586 close.priority = 90 587 588
589 -def init(storage_type='ram', path=None, path_header=None, name='session_id', 590 timeout=60, domain=None, secure=False, clean_freq=5, **kwargs):
591 """Initialize session object (using cookies). 592 593 storage_type: one of 'ram', 'file', 'postgresql'. This will be used 594 to look up the corresponding class in cherrypy.lib.sessions 595 globals. For example, 'file' will use the FileSession class. 596 path: the 'path' value to stick in the response cookie metadata. 597 path_header: if 'path' is None (the default), then the response 598 cookie 'path' will be pulled from request.headers[path_header]. 599 name: the name of the cookie. 600 timeout: the expiration timeout (in minutes) for both the cookie and 601 stored session data. 602 domain: the cookie domain. 603 secure: if False (the default) the cookie 'secure' value will not 604 be set. If True, the cookie 'secure' value will be set (to 1). 605 clean_freq (minutes): the poll rate for expired session cleanup. 606 607 Any additional kwargs will be bound to the new Session instance, 608 and may be specific to the storage type. See the subclass of Session 609 you're using for more information. 610 """ 611 612 request = cherrypy.request 613 614 # Guard against running twice 615 if hasattr(request, "_session_init_flag"): 616 return 617 request._session_init_flag = True 618 619 # Check if request came with a session ID 620 id = None 621 if name in request.cookie: 622 id = request.cookie[name].value 623 624 # Find the storage class and call setup (first time only). 625 storage_class = storage_type.title() + 'Session' 626 storage_class = globals()[storage_class] 627 if not hasattr(cherrypy, "session"): 628 if hasattr(storage_class, "setup"): 629 storage_class.setup(**kwargs) 630 631 # Create and attach a new Session instance to cherrypy.serving. 632 # It will possess a reference to (and lock, and lazily load) 633 # the requested session data. 634 kwargs['timeout'] = timeout 635 kwargs['clean_freq'] = clean_freq 636 cherrypy.serving.session = sess = storage_class(id, **kwargs) 637 def update_cookie(id): 638 """Update the cookie every time the session id changes.""" 639 cherrypy.response.cookie[name] = id
640 sess.id_observers.append(update_cookie) 641 642 # Create cherrypy.session which will proxy to cherrypy.serving.session 643 if not hasattr(cherrypy, "session"): 644 cherrypy.session = cherrypy._ThreadLocalProxy('session') 645 646 set_response_cookie(path=path, path_header=path_header, name=name, 647 timeout=timeout, domain=domain, secure=secure) 648 649 680 681
682 -def expire():
683 """Expire the current session cookie.""" 684 name = cherrypy.request.config.get('tools.sessions.name', 'session_id') 685 one_year = 60 * 60 * 24 * 365 686 exp = time.gmtime(time.time() - one_year) 687 t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp) 688 cherrypy.response.cookie[name]['expires'] = t
689