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