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
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
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."
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
87
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
101 if self._exists():
102 self.id = None
103
104 if old_session_was_locked:
105 self.acquire_lock()
106
108 """Clean up expired sessions."""
109 pass
110
111 try:
112 os.urandom(20)
113 except (AttributeError, NotImplementedError):
114
116 """Return a new session id."""
117 return sha('%s' % random.random()).hexdigest()
118 else:
120 """Return a new session id."""
121 return os.urandom(20).encode('hex')
122
124 """Save session data."""
125 try:
126
127
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
136 self.release_lock()
137
160
162 """Delete stored session data."""
163 self._delete()
164
168
172
176
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
191
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
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
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
213 """D.clear() -> None. Remove all items from D."""
214 if not self.loaded: self.load()
215 self._data.clear()
216
218 """D.keys() -> list of D's keys."""
219 if not self.loaded: self.load()
220 return self._data.keys()
221
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
228 """D.values() -> list of D's values."""
229 if not self.loaded: self.load()
230 return self._data.values()
231
232
234
235
236 cache = {}
237 locks = {}
238
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
255
258
259 - def _save(self, expiration_time):
260 self.cache[self.id] = (self._data, expiration_time)
261
264
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
271 """Release the lock on the currently-loaded session data."""
272 self.locks[self.id].release()
273 self.locked = False
274
276 """Return the number of active sessions."""
277 return len(self.cache)
278
279
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
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
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
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
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
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
350 try:
351 os.unlink(self._get_file_path())
352 except OSError:
353 pass
354
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
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
378 """Clean up expired sessions."""
379 now = datetime.datetime.now()
380
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
385
386 path = os.path.join(self.storage_path, fname)
387 self.acquire_lock(path)
388 try:
389 contents = self._load(path)
390
391 if contents is not None:
392 data, expiration_time = contents
393 if expiration_time < now:
394
395 os.unlink(path)
396 finally:
397 self.release_lock(path)
398
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
436 if self.cursor:
437 self.cursor.close()
438 self.db.commit()
439
441
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
448
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
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
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
478
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
489
490
491
492 mc_lock = threading.RLock()
493
494
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
518
525
526 - def _save(self, expiration_time):
527
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
538
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
545 """Release the lock on the currently-loaded session data."""
546 self.locks[self.id].release()
547 self.locked = False
548
550 """Return the number of active sessions."""
551 raise NotImplementedError
552
553
554
555
577 save.failsafe = True
578
580 """Close the session object for this request."""
581 sess = getattr(cherrypy.serving, "session", None)
582 if getattr(sess, "locked", False):
583
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
615 if hasattr(request, "_session_init_flag"):
616 return
617 request._session_init_flag = True
618
619
620 id = None
621 if name in request.cookie:
622 id = request.cookie[name].value
623
624
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
632
633
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
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
650 -def set_response_cookie(path=None, path_header=None, name='session_id',
651 timeout=60, domain=None, secure=False):
652 """Set a response cookie for the client.
653
654 path: the 'path' value to stick in the response cookie metadata.
655 path_header: if 'path' is None (the default), then the response
656 cookie 'path' will be pulled from request.headers[path_header].
657 name: the name of the cookie.
658 timeout: the expiration timeout for the cookie.
659 domain: the cookie domain.
660 secure: if False (the default) the cookie 'secure' value will not
661 be set. If True, the cookie 'secure' value will be set (to 1).
662 """
663
664 cookie = cherrypy.response.cookie
665 cookie[name] = cherrypy.serving.session.id
666 cookie[name]['path'] = (path or cherrypy.request.headers.get(path_header)
667 or '/')
668
669
670
671
672
673
674 if timeout:
675 cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
676 if domain is not None:
677 cookie[name]['domain'] = domain
678 if secure:
679 cookie[name]['secure'] = 1
680
681
689