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
27
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
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
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
75 if self._load() is not None:
76 self.id = None
77
84 clean_interrupt = classmethod(clean_interrupt)
85
87 """Clean up expired sessions."""
88 pass
89
90 try:
91 os.urandom(20)
92 except (AttributeError, NotImplementedError):
93
95 """Return a new session id."""
96 return sha.new('%s' % random.random()).hexdigest()
97 else:
99 """Return a new session id."""
100 return os.urandom(20).encode('hex')
101
103 """Save session data."""
104 try:
105
106
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
115 self.release_lock()
116
139
141 """Delete stored session data."""
142 self._delete()
143
147
151
155
162
166
170
171 - def get(self, key, default=None):
174
178
182
186
190
194
198
199
201
202
203 cache = {}
204 locks = {}
205
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
222
223 - def _save(self, expiration_time):
224 self.cache[self.id] = (self._data, expiration_time)
225
228
232
236
237
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
250
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
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
285 try:
286 os.unlink(self._get_file_path())
287 except OSError:
288 pass
289
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
309
311 """Clean up expired sessions."""
312 now = datetime.datetime.now()
313
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
318
319 path = os.path.join(self.storage_path, fname)
320 self.acquire_lock(path)
321 try:
322 contents = self._load(path)
323
324 if contents is not None:
325 data, expiration_time = contents
326 if expiration_time < now:
327
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
351 if self.cursor:
352 self.cursor.close()
353 self.db.commit()
354
356
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
374 self.cursor.execute('delete from session where id=%s', (self.id,))
375
376 - def acquire_lock(self):
377
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
384
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
395
413 save.failsafe = True
414
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
450 if hasattr(request, "_session_init_flag"):
451 return
452 request._session_init_flag = True
453
454
455 id = None
456 if name in request.cookie:
457 id = request.cookie[name].value
458
459
460
461
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
473 cookie = cherrypy.response.cookie
474 cookie[name] = sess.id
475 cookie[name]['path'] = path or request.headers.get(path_header) or '/'
476
477
478
479
480
481
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
496