OLD | NEW |
(Empty) | |
| 1 """Session implementation for CherryPy. |
| 2 |
| 3 You need to edit your config file to use sessions. Here's an example:: |
| 4 |
| 5 [/] |
| 6 tools.sessions.on = True |
| 7 tools.sessions.storage_type = "file" |
| 8 tools.sessions.storage_path = "/home/site/sessions" |
| 9 tools.sessions.timeout = 60 |
| 10 |
| 11 This sets the session to be stored in files in the directory /home/site/sessions
, |
| 12 and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions |
| 13 will be saved in RAM. ``tools.sessions.on`` is the only required line for |
| 14 working sessions, the rest are optional. |
| 15 |
| 16 By default, the session ID is passed in a cookie, so the client's browser must |
| 17 have cookies enabled for your site. |
| 18 |
| 19 To set data for the current session, use |
| 20 ``cherrypy.session['fieldname'] = 'fieldvalue'``; |
| 21 to get data use ``cherrypy.session.get('fieldname')``. |
| 22 |
| 23 ================ |
| 24 Locking sessions |
| 25 ================ |
| 26 |
| 27 By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means |
| 28 the session is locked early and unlocked late. If you want to control when the |
| 29 session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``
. |
| 30 Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_loc
k()``. |
| 31 Regardless of which mode you use, the session is guaranteed to be unlocked when |
| 32 the request is complete. |
| 33 |
| 34 ================= |
| 35 Expiring Sessions |
| 36 ================= |
| 37 |
| 38 You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. |
| 39 Simply call that function at the point you want the session to expire, and it |
| 40 will cause the session cookie to expire client-side. |
| 41 |
| 42 =========================== |
| 43 Session Fixation Protection |
| 44 =========================== |
| 45 |
| 46 If CherryPy receives, via a request cookie, a session id that it does not |
| 47 recognize, it will reject that id and create a new one to return in the |
| 48 response cookie. This `helps prevent session fixation attacks |
| 49 <http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_
. |
| 50 However, CherryPy "recognizes" a session id by looking up the saved session |
| 51 data for that id. Therefore, if you never save any session data, |
| 52 **you will get a new session id for every request**. |
| 53 |
| 54 ================ |
| 55 Sharing Sessions |
| 56 ================ |
| 57 |
| 58 If you run multiple instances of CherryPy (for example via mod_python behind |
| 59 Apache prefork), you most likely cannot use the RAM session backend, since each |
| 60 instance of CherryPy will have its own memory space. Use a different backend |
| 61 instead, and verify that all instances are pointing at the same file or db |
| 62 location. Alternately, you might try a load balancer which makes sessions |
| 63 "sticky". Google is your friend, there. |
| 64 |
| 65 ================ |
| 66 Expiration Dates |
| 67 ================ |
| 68 |
| 69 The response cookie will possess an expiration date to inform the client at |
| 70 which point to stop sending the cookie back in requests. If the server time |
| 71 and client time differ, expect sessions to be unreliable. **Make sure the |
| 72 system time of your server is accurate**. |
| 73 |
| 74 CherryPy defaults to a 60-minute session timeout, which also applies to the |
| 75 cookie which is sent to the client. Unfortunately, some versions of Safari |
| 76 ("4 public beta" on Windows XP at least) appear to have a bug in their parsing |
| 77 of the GMT expiration date--they appear to interpret the date as one hour in |
| 78 the past. Sixty minutes minus one hour is pretty close to zero, so you may |
| 79 experience this bug as a new session id for every request, unless the requests |
| 80 are less than one second apart. To fix, try increasing the session.timeout. |
| 81 |
| 82 On the other extreme, some users report Firefox sending cookies after their |
| 83 expiration date, although this was on a system with an inaccurate system time. |
| 84 Maybe FF doesn't trust system time. |
| 85 """ |
| 86 |
| 87 import datetime |
| 88 import os |
| 89 import random |
| 90 import time |
| 91 import threading |
| 92 import types |
| 93 from warnings import warn |
| 94 |
| 95 import cherrypy |
| 96 from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr |
| 97 from cherrypy.lib import httputil |
| 98 |
| 99 |
| 100 missing = object() |
| 101 |
| 102 class Session(object): |
| 103 """A CherryPy dict-like Session object (one per request).""" |
| 104 |
| 105 _id = None |
| 106 |
| 107 id_observers = None |
| 108 "A list of callbacks to which to pass new id's." |
| 109 |
| 110 def _get_id(self): |
| 111 return self._id |
| 112 def _set_id(self, value): |
| 113 self._id = value |
| 114 for o in self.id_observers: |
| 115 o(value) |
| 116 id = property(_get_id, _set_id, doc="The current session ID.") |
| 117 |
| 118 timeout = 60 |
| 119 "Number of minutes after which to delete session data." |
| 120 |
| 121 locked = False |
| 122 """ |
| 123 If True, this session instance has exclusive read/write access |
| 124 to session data.""" |
| 125 |
| 126 loaded = False |
| 127 """ |
| 128 If True, data has been retrieved from storage. This should happen |
| 129 automatically on the first attempt to access session data.""" |
| 130 |
| 131 clean_thread = None |
| 132 "Class-level Monitor which calls self.clean_up." |
| 133 |
| 134 clean_freq = 5 |
| 135 "The poll rate for expired session cleanup in minutes." |
| 136 |
| 137 originalid = None |
| 138 "The session id passed by the client. May be missing or unsafe." |
| 139 |
| 140 missing = False |
| 141 "True if the session requested by the client did not exist." |
| 142 |
| 143 regenerated = False |
| 144 """ |
| 145 True if the application called session.regenerate(). This is not set by |
| 146 internal calls to regenerate the session id.""" |
| 147 |
| 148 debug=False |
| 149 |
| 150 def __init__(self, id=None, **kwargs): |
| 151 self.id_observers = [] |
| 152 self._data = {} |
| 153 |
| 154 for k, v in kwargs.items(): |
| 155 setattr(self, k, v) |
| 156 |
| 157 self.originalid = id |
| 158 self.missing = False |
| 159 if id is None: |
| 160 if self.debug: |
| 161 cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') |
| 162 self._regenerate() |
| 163 else: |
| 164 self.id = id |
| 165 if not self._exists(): |
| 166 if self.debug: |
| 167 cherrypy.log('Expired or malicious session %r; ' |
| 168 'making a new one' % id, 'TOOLS.SESSIONS') |
| 169 # Expired or malicious session. Make a new one. |
| 170 # See http://www.cherrypy.org/ticket/709. |
| 171 self.id = None |
| 172 self.missing = True |
| 173 self._regenerate() |
| 174 |
| 175 def now(self): |
| 176 """Generate the session specific concept of 'now'. |
| 177 |
| 178 Other session providers can override this to use alternative, |
| 179 possibly timezone aware, versions of 'now'. |
| 180 """ |
| 181 return datetime.datetime.now() |
| 182 |
| 183 def regenerate(self): |
| 184 """Replace the current session (with a new id).""" |
| 185 self.regenerated = True |
| 186 self._regenerate() |
| 187 |
| 188 def _regenerate(self): |
| 189 if self.id is not None: |
| 190 self.delete() |
| 191 |
| 192 old_session_was_locked = self.locked |
| 193 if old_session_was_locked: |
| 194 self.release_lock() |
| 195 |
| 196 self.id = None |
| 197 while self.id is None: |
| 198 self.id = self.generate_id() |
| 199 # Assert that the generated id is not already stored. |
| 200 if self._exists(): |
| 201 self.id = None |
| 202 |
| 203 if old_session_was_locked: |
| 204 self.acquire_lock() |
| 205 |
| 206 def clean_up(self): |
| 207 """Clean up expired sessions.""" |
| 208 pass |
| 209 |
| 210 def generate_id(self): |
| 211 """Return a new session id.""" |
| 212 return random20() |
| 213 |
| 214 def save(self): |
| 215 """Save session data.""" |
| 216 try: |
| 217 # If session data has never been loaded then it's never been |
| 218 # accessed: no need to save it |
| 219 if self.loaded: |
| 220 t = datetime.timedelta(seconds = self.timeout * 60) |
| 221 expiration_time = self.now() + t |
| 222 if self.debug: |
| 223 cherrypy.log('Saving with expiry %s' % expiration_time, |
| 224 'TOOLS.SESSIONS') |
| 225 self._save(expiration_time) |
| 226 |
| 227 finally: |
| 228 if self.locked: |
| 229 # Always release the lock if the user didn't release it |
| 230 self.release_lock() |
| 231 |
| 232 def load(self): |
| 233 """Copy stored session data into this session instance.""" |
| 234 data = self._load() |
| 235 # data is either None or a tuple (session_data, expiration_time) |
| 236 if data is None or data[1] < self.now(): |
| 237 if self.debug: |
| 238 cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS') |
| 239 self._data = {} |
| 240 else: |
| 241 self._data = data[0] |
| 242 self.loaded = True |
| 243 |
| 244 # Stick the clean_thread in the class, not the instance. |
| 245 # The instances are created and destroyed per-request. |
| 246 cls = self.__class__ |
| 247 if self.clean_freq and not cls.clean_thread: |
| 248 # clean_up is in instancemethod and not a classmethod, |
| 249 # so that tool config can be accessed inside the method. |
| 250 t = cherrypy.process.plugins.Monitor( |
| 251 cherrypy.engine, self.clean_up, self.clean_freq * 60, |
| 252 name='Session cleanup') |
| 253 t.subscribe() |
| 254 cls.clean_thread = t |
| 255 t.start() |
| 256 |
| 257 def delete(self): |
| 258 """Delete stored session data.""" |
| 259 self._delete() |
| 260 |
| 261 def __getitem__(self, key): |
| 262 if not self.loaded: self.load() |
| 263 return self._data[key] |
| 264 |
| 265 def __setitem__(self, key, value): |
| 266 if not self.loaded: self.load() |
| 267 self._data[key] = value |
| 268 |
| 269 def __delitem__(self, key): |
| 270 if not self.loaded: self.load() |
| 271 del self._data[key] |
| 272 |
| 273 def pop(self, key, default=missing): |
| 274 """Remove the specified key and return the corresponding value. |
| 275 If key is not found, default is returned if given, |
| 276 otherwise KeyError is raised. |
| 277 """ |
| 278 if not self.loaded: self.load() |
| 279 if default is missing: |
| 280 return self._data.pop(key) |
| 281 else: |
| 282 return self._data.pop(key, default) |
| 283 |
| 284 def __contains__(self, key): |
| 285 if not self.loaded: self.load() |
| 286 return key in self._data |
| 287 |
| 288 if hasattr({}, 'has_key'): |
| 289 def has_key(self, key): |
| 290 """D.has_key(k) -> True if D has a key k, else False.""" |
| 291 if not self.loaded: self.load() |
| 292 return key in self._data |
| 293 |
| 294 def get(self, key, default=None): |
| 295 """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" |
| 296 if not self.loaded: self.load() |
| 297 return self._data.get(key, default) |
| 298 |
| 299 def update(self, d): |
| 300 """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" |
| 301 if not self.loaded: self.load() |
| 302 self._data.update(d) |
| 303 |
| 304 def setdefault(self, key, default=None): |
| 305 """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" |
| 306 if not self.loaded: self.load() |
| 307 return self._data.setdefault(key, default) |
| 308 |
| 309 def clear(self): |
| 310 """D.clear() -> None. Remove all items from D.""" |
| 311 if not self.loaded: self.load() |
| 312 self._data.clear() |
| 313 |
| 314 def keys(self): |
| 315 """D.keys() -> list of D's keys.""" |
| 316 if not self.loaded: self.load() |
| 317 return self._data.keys() |
| 318 |
| 319 def items(self): |
| 320 """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" |
| 321 if not self.loaded: self.load() |
| 322 return self._data.items() |
| 323 |
| 324 def values(self): |
| 325 """D.values() -> list of D's values.""" |
| 326 if not self.loaded: self.load() |
| 327 return self._data.values() |
| 328 |
| 329 |
| 330 class RamSession(Session): |
| 331 |
| 332 # Class-level objects. Don't rebind these! |
| 333 cache = {} |
| 334 locks = {} |
| 335 |
| 336 def clean_up(self): |
| 337 """Clean up expired sessions.""" |
| 338 now = self.now() |
| 339 for id, (data, expiration_time) in copyitems(self.cache): |
| 340 if expiration_time <= now: |
| 341 try: |
| 342 del self.cache[id] |
| 343 except KeyError: |
| 344 pass |
| 345 try: |
| 346 del self.locks[id] |
| 347 except KeyError: |
| 348 pass |
| 349 |
| 350 # added to remove obsolete lock objects |
| 351 for id in list(self.locks): |
| 352 if id not in self.cache: |
| 353 self.locks.pop(id, None) |
| 354 |
| 355 def _exists(self): |
| 356 return self.id in self.cache |
| 357 |
| 358 def _load(self): |
| 359 return self.cache.get(self.id) |
| 360 |
| 361 def _save(self, expiration_time): |
| 362 self.cache[self.id] = (self._data, expiration_time) |
| 363 |
| 364 def _delete(self): |
| 365 self.cache.pop(self.id, None) |
| 366 |
| 367 def acquire_lock(self): |
| 368 """Acquire an exclusive lock on the currently-loaded session data.""" |
| 369 self.locked = True |
| 370 self.locks.setdefault(self.id, threading.RLock()).acquire() |
| 371 |
| 372 def release_lock(self): |
| 373 """Release the lock on the currently-loaded session data.""" |
| 374 self.locks[self.id].release() |
| 375 self.locked = False |
| 376 |
| 377 def __len__(self): |
| 378 """Return the number of active sessions.""" |
| 379 return len(self.cache) |
| 380 |
| 381 |
| 382 class FileSession(Session): |
| 383 """Implementation of the File backend for sessions |
| 384 |
| 385 storage_path |
| 386 The folder where session data will be saved. Each session |
| 387 will be saved as pickle.dump(data, expiration_time) in its own file; |
| 388 the filename will be self.SESSION_PREFIX + self.id. |
| 389 |
| 390 """ |
| 391 |
| 392 SESSION_PREFIX = 'session-' |
| 393 LOCK_SUFFIX = '.lock' |
| 394 pickle_protocol = pickle.HIGHEST_PROTOCOL |
| 395 |
| 396 def __init__(self, id=None, **kwargs): |
| 397 # The 'storage_path' arg is required for file-based sessions. |
| 398 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) |
| 399 Session.__init__(self, id=id, **kwargs) |
| 400 |
| 401 def setup(cls, **kwargs): |
| 402 """Set up the storage system for file-based sessions. |
| 403 |
| 404 This should only be called once per process; this will be done |
| 405 automatically when using sessions.init (as the built-in Tool does). |
| 406 """ |
| 407 # The 'storage_path' arg is required for file-based sessions. |
| 408 kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) |
| 409 |
| 410 for k, v in kwargs.items(): |
| 411 setattr(cls, k, v) |
| 412 |
| 413 # Warn if any lock files exist at startup. |
| 414 lockfiles = [fname for fname in os.listdir(cls.storage_path) |
| 415 if (fname.startswith(cls.SESSION_PREFIX) |
| 416 and fname.endswith(cls.LOCK_SUFFIX))] |
| 417 if lockfiles: |
| 418 plural = ('', 's')[len(lockfiles) > 1] |
| 419 warn("%s session lockfile%s found at startup. If you are " |
| 420 "only running one process, then you may need to " |
| 421 "manually delete the lockfiles found at %r." |
| 422 % (len(lockfiles), plural, cls.storage_path)) |
| 423 setup = classmethod(setup) |
| 424 |
| 425 def _get_file_path(self): |
| 426 f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) |
| 427 if not os.path.abspath(f).startswith(self.storage_path): |
| 428 raise cherrypy.HTTPError(400, "Invalid session id in cookie.") |
| 429 return f |
| 430 |
| 431 def _exists(self): |
| 432 path = self._get_file_path() |
| 433 return os.path.exists(path) |
| 434 |
| 435 def _load(self, path=None): |
| 436 if path is None: |
| 437 path = self._get_file_path() |
| 438 try: |
| 439 f = open(path, "rb") |
| 440 try: |
| 441 return pickle.load(f) |
| 442 finally: |
| 443 f.close() |
| 444 except (IOError, EOFError): |
| 445 return None |
| 446 |
| 447 def _save(self, expiration_time): |
| 448 f = open(self._get_file_path(), "wb") |
| 449 try: |
| 450 pickle.dump((self._data, expiration_time), f, self.pickle_protocol) |
| 451 finally: |
| 452 f.close() |
| 453 |
| 454 def _delete(self): |
| 455 try: |
| 456 os.unlink(self._get_file_path()) |
| 457 except OSError: |
| 458 pass |
| 459 |
| 460 def acquire_lock(self, path=None): |
| 461 """Acquire an exclusive lock on the currently-loaded session data.""" |
| 462 if path is None: |
| 463 path = self._get_file_path() |
| 464 path += self.LOCK_SUFFIX |
| 465 while True: |
| 466 try: |
| 467 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) |
| 468 except OSError: |
| 469 time.sleep(0.1) |
| 470 else: |
| 471 os.close(lockfd) |
| 472 break |
| 473 self.locked = True |
| 474 |
| 475 def release_lock(self, path=None): |
| 476 """Release the lock on the currently-loaded session data.""" |
| 477 if path is None: |
| 478 path = self._get_file_path() |
| 479 os.unlink(path + self.LOCK_SUFFIX) |
| 480 self.locked = False |
| 481 |
| 482 def clean_up(self): |
| 483 """Clean up expired sessions.""" |
| 484 now = self.now() |
| 485 # Iterate over all session files in self.storage_path |
| 486 for fname in os.listdir(self.storage_path): |
| 487 if (fname.startswith(self.SESSION_PREFIX) |
| 488 and not fname.endswith(self.LOCK_SUFFIX)): |
| 489 # We have a session file: lock and load it and check |
| 490 # if it's expired. If it fails, nevermind. |
| 491 path = os.path.join(self.storage_path, fname) |
| 492 self.acquire_lock(path) |
| 493 try: |
| 494 contents = self._load(path) |
| 495 # _load returns None on IOError |
| 496 if contents is not None: |
| 497 data, expiration_time = contents |
| 498 if expiration_time < now: |
| 499 # Session expired: deleting it |
| 500 os.unlink(path) |
| 501 finally: |
| 502 self.release_lock(path) |
| 503 |
| 504 def __len__(self): |
| 505 """Return the number of active sessions.""" |
| 506 return len([fname for fname in os.listdir(self.storage_path) |
| 507 if (fname.startswith(self.SESSION_PREFIX) |
| 508 and not fname.endswith(self.LOCK_SUFFIX))]) |
| 509 |
| 510 |
| 511 class PostgresqlSession(Session): |
| 512 """ Implementation of the PostgreSQL backend for sessions. It assumes |
| 513 a table like this:: |
| 514 |
| 515 create table session ( |
| 516 id varchar(40), |
| 517 data text, |
| 518 expiration_time timestamp |
| 519 ) |
| 520 |
| 521 You must provide your own get_db function. |
| 522 """ |
| 523 |
| 524 pickle_protocol = pickle.HIGHEST_PROTOCOL |
| 525 |
| 526 def __init__(self, id=None, **kwargs): |
| 527 Session.__init__(self, id, **kwargs) |
| 528 self.cursor = self.db.cursor() |
| 529 |
| 530 def setup(cls, **kwargs): |
| 531 """Set up the storage system for Postgres-based sessions. |
| 532 |
| 533 This should only be called once per process; this will be done |
| 534 automatically when using sessions.init (as the built-in Tool does). |
| 535 """ |
| 536 for k, v in kwargs.items(): |
| 537 setattr(cls, k, v) |
| 538 |
| 539 self.db = self.get_db() |
| 540 setup = classmethod(setup) |
| 541 |
| 542 def __del__(self): |
| 543 if self.cursor: |
| 544 self.cursor.close() |
| 545 self.db.commit() |
| 546 |
| 547 def _exists(self): |
| 548 # Select session data from table |
| 549 self.cursor.execute('select data, expiration_time from session ' |
| 550 'where id=%s', (self.id,)) |
| 551 rows = self.cursor.fetchall() |
| 552 return bool(rows) |
| 553 |
| 554 def _load(self): |
| 555 # Select session data from table |
| 556 self.cursor.execute('select data, expiration_time from session ' |
| 557 'where id=%s', (self.id,)) |
| 558 rows = self.cursor.fetchall() |
| 559 if not rows: |
| 560 return None |
| 561 |
| 562 pickled_data, expiration_time = rows[0] |
| 563 data = pickle.loads(pickled_data) |
| 564 return data, expiration_time |
| 565 |
| 566 def _save(self, expiration_time): |
| 567 pickled_data = pickle.dumps(self._data, self.pickle_protocol) |
| 568 self.cursor.execute('update session set data = %s, ' |
| 569 'expiration_time = %s where id = %s', |
| 570 (pickled_data, expiration_time, self.id)) |
| 571 |
| 572 def _delete(self): |
| 573 self.cursor.execute('delete from session where id=%s', (self.id,)) |
| 574 |
| 575 def acquire_lock(self): |
| 576 """Acquire an exclusive lock on the currently-loaded session data.""" |
| 577 # We use the "for update" clause to lock the row |
| 578 self.locked = True |
| 579 self.cursor.execute('select id from session where id=%s for update', |
| 580 (self.id,)) |
| 581 |
| 582 def release_lock(self): |
| 583 """Release the lock on the currently-loaded session data.""" |
| 584 # We just close the cursor and that will remove the lock |
| 585 # introduced by the "for update" clause |
| 586 self.cursor.close() |
| 587 self.locked = False |
| 588 |
| 589 def clean_up(self): |
| 590 """Clean up expired sessions.""" |
| 591 self.cursor.execute('delete from session where expiration_time < %s', |
| 592 (self.now(),)) |
| 593 |
| 594 |
| 595 class MemcachedSession(Session): |
| 596 |
| 597 # The most popular memcached client for Python isn't thread-safe. |
| 598 # Wrap all .get and .set operations in a single lock. |
| 599 mc_lock = threading.RLock() |
| 600 |
| 601 # This is a seperate set of locks per session id. |
| 602 locks = {} |
| 603 |
| 604 servers = ['127.0.0.1:11211'] |
| 605 |
| 606 def setup(cls, **kwargs): |
| 607 """Set up the storage system for memcached-based sessions. |
| 608 |
| 609 This should only be called once per process; this will be done |
| 610 automatically when using sessions.init (as the built-in Tool does). |
| 611 """ |
| 612 for k, v in kwargs.items(): |
| 613 setattr(cls, k, v) |
| 614 |
| 615 import memcache |
| 616 cls.cache = memcache.Client(cls.servers) |
| 617 setup = classmethod(setup) |
| 618 |
| 619 def _get_id(self): |
| 620 return self._id |
| 621 def _set_id(self, value): |
| 622 # This encode() call is where we differ from the superclass. |
| 623 # Memcache keys MUST be byte strings, not unicode. |
| 624 if isinstance(value, unicodestr): |
| 625 value = value.encode('utf-8') |
| 626 |
| 627 self._id = value |
| 628 for o in self.id_observers: |
| 629 o(value) |
| 630 id = property(_get_id, _set_id, doc="The current session ID.") |
| 631 |
| 632 def _exists(self): |
| 633 self.mc_lock.acquire() |
| 634 try: |
| 635 return bool(self.cache.get(self.id)) |
| 636 finally: |
| 637 self.mc_lock.release() |
| 638 |
| 639 def _load(self): |
| 640 self.mc_lock.acquire() |
| 641 try: |
| 642 return self.cache.get(self.id) |
| 643 finally: |
| 644 self.mc_lock.release() |
| 645 |
| 646 def _save(self, expiration_time): |
| 647 # Send the expiration time as "Unix time" (seconds since 1/1/1970) |
| 648 td = int(time.mktime(expiration_time.timetuple())) |
| 649 self.mc_lock.acquire() |
| 650 try: |
| 651 if not self.cache.set(self.id, (self._data, expiration_time), td): |
| 652 raise AssertionError("Session data for id %r not set." % self.id
) |
| 653 finally: |
| 654 self.mc_lock.release() |
| 655 |
| 656 def _delete(self): |
| 657 self.cache.delete(self.id) |
| 658 |
| 659 def acquire_lock(self): |
| 660 """Acquire an exclusive lock on the currently-loaded session data.""" |
| 661 self.locked = True |
| 662 self.locks.setdefault(self.id, threading.RLock()).acquire() |
| 663 |
| 664 def release_lock(self): |
| 665 """Release the lock on the currently-loaded session data.""" |
| 666 self.locks[self.id].release() |
| 667 self.locked = False |
| 668 |
| 669 def __len__(self): |
| 670 """Return the number of active sessions.""" |
| 671 raise NotImplementedError |
| 672 |
| 673 |
| 674 # Hook functions (for CherryPy tools) |
| 675 |
| 676 def save(): |
| 677 """Save any changed session data.""" |
| 678 |
| 679 if not hasattr(cherrypy.serving, "session"): |
| 680 return |
| 681 request = cherrypy.serving.request |
| 682 response = cherrypy.serving.response |
| 683 |
| 684 # Guard against running twice |
| 685 if hasattr(request, "_sessionsaved"): |
| 686 return |
| 687 request._sessionsaved = True |
| 688 |
| 689 if response.stream: |
| 690 # If the body is being streamed, we have to save the data |
| 691 # *after* the response has been written out |
| 692 request.hooks.attach('on_end_request', cherrypy.session.save) |
| 693 else: |
| 694 # If the body is not being streamed, we save the data now |
| 695 # (so we can release the lock). |
| 696 if isinstance(response.body, types.GeneratorType): |
| 697 response.collapse_body() |
| 698 cherrypy.session.save() |
| 699 save.failsafe = True |
| 700 |
| 701 def close(): |
| 702 """Close the session object for this request.""" |
| 703 sess = getattr(cherrypy.serving, "session", None) |
| 704 if getattr(sess, "locked", False): |
| 705 # If the session is still locked we release the lock |
| 706 sess.release_lock() |
| 707 close.failsafe = True |
| 708 close.priority = 90 |
| 709 |
| 710 |
| 711 def init(storage_type='ram', path=None, path_header=None, name='session_id', |
| 712 timeout=60, domain=None, secure=False, clean_freq=5, |
| 713 persistent=True, httponly=False, debug=False, **kwargs): |
| 714 """Initialize session object (using cookies). |
| 715 |
| 716 storage_type |
| 717 One of 'ram', 'file', 'postgresql', 'memcached'. This will be |
| 718 used to look up the corresponding class in cherrypy.lib.sessions |
| 719 globals. For example, 'file' will use the FileSession class. |
| 720 |
| 721 path |
| 722 The 'path' value to stick in the response cookie metadata. |
| 723 |
| 724 path_header |
| 725 If 'path' is None (the default), then the response |
| 726 cookie 'path' will be pulled from request.headers[path_header]. |
| 727 |
| 728 name |
| 729 The name of the cookie. |
| 730 |
| 731 timeout |
| 732 The expiration timeout (in minutes) for the stored session data. |
| 733 If 'persistent' is True (the default), this is also the timeout |
| 734 for the cookie. |
| 735 |
| 736 domain |
| 737 The cookie domain. |
| 738 |
| 739 secure |
| 740 If False (the default) the cookie 'secure' value will not |
| 741 be set. If True, the cookie 'secure' value will be set (to 1). |
| 742 |
| 743 clean_freq (minutes) |
| 744 The poll rate for expired session cleanup. |
| 745 |
| 746 persistent |
| 747 If True (the default), the 'timeout' argument will be used |
| 748 to expire the cookie. If False, the cookie will not have an expiry, |
| 749 and the cookie will be a "session cookie" which expires when the |
| 750 browser is closed. |
| 751 |
| 752 httponly |
| 753 If False (the default) the cookie 'httponly' value will not be set. |
| 754 If True, the cookie 'httponly' value will be set (to 1). |
| 755 |
| 756 Any additional kwargs will be bound to the new Session instance, |
| 757 and may be specific to the storage type. See the subclass of Session |
| 758 you're using for more information. |
| 759 """ |
| 760 |
| 761 request = cherrypy.serving.request |
| 762 |
| 763 # Guard against running twice |
| 764 if hasattr(request, "_session_init_flag"): |
| 765 return |
| 766 request._session_init_flag = True |
| 767 |
| 768 # Check if request came with a session ID |
| 769 id = None |
| 770 if name in request.cookie: |
| 771 id = request.cookie[name].value |
| 772 if debug: |
| 773 cherrypy.log('ID obtained from request.cookie: %r' % id, |
| 774 'TOOLS.SESSIONS') |
| 775 |
| 776 # Find the storage class and call setup (first time only). |
| 777 storage_class = storage_type.title() + 'Session' |
| 778 storage_class = globals()[storage_class] |
| 779 if not hasattr(cherrypy, "session"): |
| 780 if hasattr(storage_class, "setup"): |
| 781 storage_class.setup(**kwargs) |
| 782 |
| 783 # Create and attach a new Session instance to cherrypy.serving. |
| 784 # It will possess a reference to (and lock, and lazily load) |
| 785 # the requested session data. |
| 786 kwargs['timeout'] = timeout |
| 787 kwargs['clean_freq'] = clean_freq |
| 788 cherrypy.serving.session = sess = storage_class(id, **kwargs) |
| 789 sess.debug = debug |
| 790 def update_cookie(id): |
| 791 """Update the cookie every time the session id changes.""" |
| 792 cherrypy.serving.response.cookie[name] = id |
| 793 sess.id_observers.append(update_cookie) |
| 794 |
| 795 # Create cherrypy.session which will proxy to cherrypy.serving.session |
| 796 if not hasattr(cherrypy, "session"): |
| 797 cherrypy.session = cherrypy._ThreadLocalProxy('session') |
| 798 |
| 799 if persistent: |
| 800 cookie_timeout = timeout |
| 801 else: |
| 802 # See http://support.microsoft.com/kb/223799/EN-US/ |
| 803 # and http://support.mozilla.com/en-US/kb/Cookies |
| 804 cookie_timeout = None |
| 805 set_response_cookie(path=path, path_header=path_header, name=name, |
| 806 timeout=cookie_timeout, domain=domain, secure=secure, |
| 807 httponly=httponly) |
| 808 |
| 809 |
| 810 def set_response_cookie(path=None, path_header=None, name='session_id', |
| 811 timeout=60, domain=None, secure=False, httponly=False): |
| 812 """Set a response cookie for the client. |
| 813 |
| 814 path |
| 815 the 'path' value to stick in the response cookie metadata. |
| 816 |
| 817 path_header |
| 818 if 'path' is None (the default), then the response |
| 819 cookie 'path' will be pulled from request.headers[path_header]. |
| 820 |
| 821 name |
| 822 the name of the cookie. |
| 823 |
| 824 timeout |
| 825 the expiration timeout for the cookie. If 0 or other boolean |
| 826 False, no 'expires' param will be set, and the cookie will be a |
| 827 "session cookie" which expires when the browser is closed. |
| 828 |
| 829 domain |
| 830 the cookie domain. |
| 831 |
| 832 secure |
| 833 if False (the default) the cookie 'secure' value will not |
| 834 be set. If True, the cookie 'secure' value will be set (to 1). |
| 835 |
| 836 httponly |
| 837 If False (the default) the cookie 'httponly' value will not be set. |
| 838 If True, the cookie 'httponly' value will be set (to 1). |
| 839 |
| 840 """ |
| 841 # Set response cookie |
| 842 cookie = cherrypy.serving.response.cookie |
| 843 cookie[name] = cherrypy.serving.session.id |
| 844 cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_he
ader) |
| 845 or '/') |
| 846 |
| 847 # We'd like to use the "max-age" param as indicated in |
| 848 # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't |
| 849 # save it to disk and the session is lost if people close |
| 850 # the browser. So we have to use the old "expires" ... sigh ... |
| 851 ## cookie[name]['max-age'] = timeout * 60 |
| 852 if timeout: |
| 853 e = time.time() + (timeout * 60) |
| 854 cookie[name]['expires'] = httputil.HTTPDate(e) |
| 855 if domain is not None: |
| 856 cookie[name]['domain'] = domain |
| 857 if secure: |
| 858 cookie[name]['secure'] = 1 |
| 859 if httponly: |
| 860 if not cookie[name].isReservedKey('httponly'): |
| 861 raise ValueError("The httponly cookie token is not supported.") |
| 862 cookie[name]['httponly'] = 1 |
| 863 |
| 864 def expire(): |
| 865 """Expire the current session cookie.""" |
| 866 name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_i
d') |
| 867 one_year = 60 * 60 * 24 * 365 |
| 868 e = time.time() - one_year |
| 869 cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) |
| 870 |
| 871 |
OLD | NEW |