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
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
288 try:
289 os.unlink(self._get_file_path())
290 except OSError:
291 pass
292
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
312
314 """Clean up expired sessions."""
315 now = datetime.datetime.now()
316
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
321
322 path = os.path.join(self.storage_path, fname)
323 self.acquire_lock(path)
324 try:
325 contents = self._load(path)
326
327 if contents is not None:
328 data, expiration_time = contents
329 if expiration_time < now:
330
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
354 if self.cursor:
355 self.cursor.close()
356 self.db.commit()
357
359
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
377 self.cursor.execute('delete from session where id=%s', (self.id,))
378
379 - def acquire_lock(self):
380
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
387
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
398
416 save.failsafe = True
417
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
453 if hasattr(request, "_session_init_flag"):
454 return
455 request._session_init_flag = True
456
457
458 id = None
459 if name in request.cookie:
460 id = request.cookie[name].value
461
462
463
464
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
476 cookie = cherrypy.response.cookie
477 cookie[name] = sess.id
478 cookie[name]['path'] = path or request.headers.get(path_header) or '/'
479
480
481
482
483
484
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
499