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