Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/cherrypy/lib/sessions.py
diff options
context:
space:
mode:
Diffstat (limited to 'cherrypy/lib/sessions.py')
-rwxr-xr-xcherrypy/lib/sessions.py832
1 files changed, 832 insertions, 0 deletions
diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py
new file mode 100755
index 0000000..42c2800
--- /dev/null
+++ b/cherrypy/lib/sessions.py
@@ -0,0 +1,832 @@
+"""Session implementation for CherryPy.
+
+You need to edit your config file to use sessions. Here's an example::
+
+ [/]
+ tools.sessions.on = True
+ tools.sessions.storage_type = "file"
+ tools.sessions.storage_path = "/home/site/sessions"
+ tools.sessions.timeout = 60
+
+This sets the session to be stored in files in the directory /home/site/sessions,
+and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions
+will be saved in RAM. ``tools.sessions.on`` is the only required line for
+working sessions, the rest are optional.
+
+By default, the session ID is passed in a cookie, so the client's browser must
+have cookies enabled for your site.
+
+To set data for the current session, use
+``cherrypy.session['fieldname'] = 'fieldvalue'``;
+to get data use ``cherrypy.session.get('fieldname')``.
+
+================
+Locking sessions
+================
+
+By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
+the session is locked early and unlocked late. If you want to control when the
+session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``.
+Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
+Regardless of which mode you use, the session is guaranteed to be unlocked when
+the request is complete.
+
+=================
+Expiring Sessions
+=================
+
+You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
+Simply call that function at the point you want the session to expire, and it
+will cause the session cookie to expire client-side.
+
+===========================
+Session Fixation Protection
+===========================
+
+If CherryPy receives, via a request cookie, a session id that it does not
+recognize, it will reject that id and create a new one to return in the
+response cookie. This `helps prevent session fixation attacks
+<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
+However, CherryPy "recognizes" a session id by looking up the saved session
+data for that id. Therefore, if you never save any session data,
+**you will get a new session id for every request**.
+
+================
+Sharing Sessions
+================
+
+If you run multiple instances of CherryPy (for example via mod_python behind
+Apache prefork), you most likely cannot use the RAM session backend, since each
+instance of CherryPy will have its own memory space. Use a different backend
+instead, and verify that all instances are pointing at the same file or db
+location. Alternately, you might try a load balancer which makes sessions
+"sticky". Google is your friend, there.
+
+================
+Expiration Dates
+================
+
+The response cookie will possess an expiration date to inform the client at
+which point to stop sending the cookie back in requests. If the server time
+and client time differ, expect sessions to be unreliable. **Make sure the
+system time of your server is accurate**.
+
+CherryPy defaults to a 60-minute session timeout, which also applies to the
+cookie which is sent to the client. Unfortunately, some versions of Safari
+("4 public beta" on Windows XP at least) appear to have a bug in their parsing
+of the GMT expiration date--they appear to interpret the date as one hour in
+the past. Sixty minutes minus one hour is pretty close to zero, so you may
+experience this bug as a new session id for every request, unless the requests
+are less than one second apart. To fix, try increasing the session.timeout.
+
+On the other extreme, some users report Firefox sending cookies after their
+expiration date, although this was on a system with an inaccurate system time.
+Maybe FF doesn't trust system time.
+"""
+
+import datetime
+import os
+import random
+import time
+import threading
+import types
+from warnings import warn
+
+import cherrypy
+from cherrypy._cpcompat import copyitems, pickle, random20
+from cherrypy.lib import httputil
+
+
+missing = object()
+
+class Session(object):
+ """A CherryPy dict-like Session object (one per request)."""
+
+ _id = None
+
+ id_observers = None
+ "A list of callbacks to which to pass new id's."
+
+ def _get_id(self):
+ return self._id
+ def _set_id(self, value):
+ self._id = value
+ for o in self.id_observers:
+ o(value)
+ id = property(_get_id, _set_id, doc="The current session ID.")
+
+ timeout = 60
+ "Number of minutes after which to delete session data."
+
+ locked = False
+ """
+ If True, this session instance has exclusive read/write access
+ to session data."""
+
+ loaded = False
+ """
+ If True, data has been retrieved from storage. This should happen
+ automatically on the first attempt to access session data."""
+
+ clean_thread = None
+ "Class-level Monitor which calls self.clean_up."
+
+ clean_freq = 5
+ "The poll rate for expired session cleanup in minutes."
+
+ originalid = None
+ "The session id passed by the client. May be missing or unsafe."
+
+ missing = False
+ "True if the session requested by the client did not exist."
+
+ regenerated = False
+ """
+ True if the application called session.regenerate(). This is not set by
+ internal calls to regenerate the session id."""
+
+ debug=False
+
+ def __init__(self, id=None, **kwargs):
+ self.id_observers = []
+ self._data = {}
+
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ self.originalid = id
+ self.missing = False
+ if id is None:
+ if self.debug:
+ cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
+ self._regenerate()
+ else:
+ self.id = id
+ if not self._exists():
+ if self.debug:
+ cherrypy.log('Expired or malicious session %r; '
+ 'making a new one' % id, 'TOOLS.SESSIONS')
+ # Expired or malicious session. Make a new one.
+ # See http://www.cherrypy.org/ticket/709.
+ self.id = None
+ self.missing = True
+ self._regenerate()
+
+ def regenerate(self):
+ """Replace the current session (with a new id)."""
+ self.regenerated = True
+ self._regenerate()
+
+ def _regenerate(self):
+ if self.id is not None:
+ self.delete()
+
+ old_session_was_locked = self.locked
+ if old_session_was_locked:
+ self.release_lock()
+
+ self.id = None
+ while self.id is None:
+ self.id = self.generate_id()
+ # Assert that the generated id is not already stored.
+ if self._exists():
+ self.id = None
+
+ if old_session_was_locked:
+ self.acquire_lock()
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ pass
+
+ def generate_id(self):
+ """Return a new session id."""
+ return random20()
+
+ def save(self):
+ """Save session data."""
+ try:
+ # If session data has never been loaded then it's never been
+ # accessed: no need to save it
+ if self.loaded:
+ t = datetime.timedelta(seconds = self.timeout * 60)
+ expiration_time = datetime.datetime.now() + t
+ if self.debug:
+ cherrypy.log('Saving with expiry %s' % expiration_time,
+ 'TOOLS.SESSIONS')
+ self._save(expiration_time)
+
+ finally:
+ if self.locked:
+ # Always release the lock if the user didn't release it
+ self.release_lock()
+
+ def load(self):
+ """Copy stored session data into this session instance."""
+ data = self._load()
+ # data is either None or a tuple (session_data, expiration_time)
+ if data is None or data[1] < datetime.datetime.now():
+ if self.debug:
+ cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS')
+ self._data = {}
+ else:
+ self._data = data[0]
+ self.loaded = True
+
+ # Stick the clean_thread in the class, not the instance.
+ # The instances are created and destroyed per-request.
+ cls = self.__class__
+ if self.clean_freq and not cls.clean_thread:
+ # clean_up is in instancemethod and not a classmethod,
+ # so that tool config can be accessed inside the method.
+ t = cherrypy.process.plugins.Monitor(
+ cherrypy.engine, self.clean_up, self.clean_freq * 60,
+ name='Session cleanup')
+ t.subscribe()
+ cls.clean_thread = t
+ t.start()
+
+ def delete(self):
+ """Delete stored session data."""
+ self._delete()
+
+ def __getitem__(self, key):
+ if not self.loaded: self.load()
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ if not self.loaded: self.load()
+ self._data[key] = value
+
+ def __delitem__(self, key):
+ if not self.loaded: self.load()
+ del self._data[key]
+
+ def pop(self, key, default=missing):
+ """Remove the specified key and return the corresponding value.
+ If key is not found, default is returned if given,
+ otherwise KeyError is raised.
+ """
+ if not self.loaded: self.load()
+ if default is missing:
+ return self._data.pop(key)
+ else:
+ return self._data.pop(key, default)
+
+ def __contains__(self, key):
+ if not self.loaded: self.load()
+ return key in self._data
+
+ def has_key(self, key):
+ """D.has_key(k) -> True if D has a key k, else False."""
+ if not self.loaded: self.load()
+ return key in self._data
+
+ def get(self, key, default=None):
+ """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
+ if not self.loaded: self.load()
+ return self._data.get(key, default)
+
+ def update(self, d):
+ """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
+ if not self.loaded: self.load()
+ self._data.update(d)
+
+ def setdefault(self, key, default=None):
+ """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
+ if not self.loaded: self.load()
+ return self._data.setdefault(key, default)
+
+ def clear(self):
+ """D.clear() -> None. Remove all items from D."""
+ if not self.loaded: self.load()
+ self._data.clear()
+
+ def keys(self):
+ """D.keys() -> list of D's keys."""
+ if not self.loaded: self.load()
+ return self._data.keys()
+
+ def items(self):
+ """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
+ if not self.loaded: self.load()
+ return self._data.items()
+
+ def values(self):
+ """D.values() -> list of D's values."""
+ if not self.loaded: self.load()
+ return self._data.values()
+
+
+class RamSession(Session):
+
+ # Class-level objects. Don't rebind these!
+ cache = {}
+ locks = {}
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ now = datetime.datetime.now()
+ for id, (data, expiration_time) in copyitems(self.cache):
+ if expiration_time <= now:
+ try:
+ del self.cache[id]
+ except KeyError:
+ pass
+ try:
+ del self.locks[id]
+ except KeyError:
+ pass
+
+ def _exists(self):
+ return self.id in self.cache
+
+ def _load(self):
+ return self.cache.get(self.id)
+
+ def _save(self, expiration_time):
+ self.cache[self.id] = (self._data, expiration_time)
+
+ def _delete(self):
+ self.cache.pop(self.id, None)
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ self.locked = True
+ self.locks.setdefault(self.id, threading.RLock()).acquire()
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ self.locks[self.id].release()
+ self.locked = False
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ return len(self.cache)
+
+
+class FileSession(Session):
+ """Implementation of the File backend for sessions
+
+ storage_path
+ The folder where session data will be saved. Each session
+ will be saved as pickle.dump(data, expiration_time) in its own file;
+ the filename will be self.SESSION_PREFIX + self.id.
+
+ """
+
+ SESSION_PREFIX = 'session-'
+ LOCK_SUFFIX = '.lock'
+ pickle_protocol = pickle.HIGHEST_PROTOCOL
+
+ def __init__(self, id=None, **kwargs):
+ # The 'storage_path' arg is required for file-based sessions.
+ kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+ Session.__init__(self, id=id, **kwargs)
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for file-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ # The 'storage_path' arg is required for file-based sessions.
+ kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ # Warn if any lock files exist at startup.
+ lockfiles = [fname for fname in os.listdir(cls.storage_path)
+ if (fname.startswith(cls.SESSION_PREFIX)
+ and fname.endswith(cls.LOCK_SUFFIX))]
+ if lockfiles:
+ plural = ('', 's')[len(lockfiles) > 1]
+ warn("%s session lockfile%s found at startup. If you are "
+ "only running one process, then you may need to "
+ "manually delete the lockfiles found at %r."
+ % (len(lockfiles), plural, cls.storage_path))
+ setup = classmethod(setup)
+
+ def _get_file_path(self):
+ f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
+ if not os.path.abspath(f).startswith(self.storage_path):
+ raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
+ return f
+
+ def _exists(self):
+ path = self._get_file_path()
+ return os.path.exists(path)
+
+ def _load(self, path=None):
+ if path is None:
+ path = self._get_file_path()
+ try:
+ f = open(path, "rb")
+ try:
+ return pickle.load(f)
+ finally:
+ f.close()
+ except (IOError, EOFError):
+ return None
+
+ def _save(self, expiration_time):
+ f = open(self._get_file_path(), "wb")
+ try:
+ pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
+ finally:
+ f.close()
+
+ def _delete(self):
+ try:
+ os.unlink(self._get_file_path())
+ except OSError:
+ pass
+
+ def acquire_lock(self, path=None):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ if path is None:
+ path = self._get_file_path()
+ path += self.LOCK_SUFFIX
+ while True:
+ try:
+ lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
+ except OSError:
+ time.sleep(0.1)
+ else:
+ os.close(lockfd)
+ break
+ self.locked = True
+
+ def release_lock(self, path=None):
+ """Release the lock on the currently-loaded session data."""
+ if path is None:
+ path = self._get_file_path()
+ os.unlink(path + self.LOCK_SUFFIX)
+ self.locked = False
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ now = datetime.datetime.now()
+ # Iterate over all session files in self.storage_path
+ for fname in os.listdir(self.storage_path):
+ if (fname.startswith(self.SESSION_PREFIX)
+ and not fname.endswith(self.LOCK_SUFFIX)):
+ # We have a session file: lock and load it and check
+ # if it's expired. If it fails, nevermind.
+ path = os.path.join(self.storage_path, fname)
+ self.acquire_lock(path)
+ try:
+ contents = self._load(path)
+ # _load returns None on IOError
+ if contents is not None:
+ data, expiration_time = contents
+ if expiration_time < now:
+ # Session expired: deleting it
+ os.unlink(path)
+ finally:
+ self.release_lock(path)
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ return len([fname for fname in os.listdir(self.storage_path)
+ if (fname.startswith(self.SESSION_PREFIX)
+ and not fname.endswith(self.LOCK_SUFFIX))])
+
+
+class PostgresqlSession(Session):
+ """ Implementation of the PostgreSQL backend for sessions. It assumes
+ a table like this::
+
+ create table session (
+ id varchar(40),
+ data text,
+ expiration_time timestamp
+ )
+
+ You must provide your own get_db function.
+ """
+
+ pickle_protocol = pickle.HIGHEST_PROTOCOL
+
+ def __init__(self, id=None, **kwargs):
+ Session.__init__(self, id, **kwargs)
+ self.cursor = self.db.cursor()
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for Postgres-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ self.db = self.get_db()
+ setup = classmethod(setup)
+
+ def __del__(self):
+ if self.cursor:
+ self.cursor.close()
+ self.db.commit()
+
+ def _exists(self):
+ # Select session data from table
+ self.cursor.execute('select data, expiration_time from session '
+ 'where id=%s', (self.id,))
+ rows = self.cursor.fetchall()
+ return bool(rows)
+
+ def _load(self):
+ # Select session data from table
+ self.cursor.execute('select data, expiration_time from session '
+ 'where id=%s', (self.id,))
+ rows = self.cursor.fetchall()
+ if not rows:
+ return None
+
+ pickled_data, expiration_time = rows[0]
+ data = pickle.loads(pickled_data)
+ return data, expiration_time
+
+ def _save(self, expiration_time):
+ pickled_data = pickle.dumps(self._data, self.pickle_protocol)
+ self.cursor.execute('update session set data = %s, '
+ 'expiration_time = %s where id = %s',
+ (pickled_data, expiration_time, self.id))
+
+ def _delete(self):
+ self.cursor.execute('delete from session where id=%s', (self.id,))
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ # We use the "for update" clause to lock the row
+ self.locked = True
+ self.cursor.execute('select id from session where id=%s for update',
+ (self.id,))
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ # We just close the cursor and that will remove the lock
+ # introduced by the "for update" clause
+ self.cursor.close()
+ self.locked = False
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ self.cursor.execute('delete from session where expiration_time < %s',
+ (datetime.datetime.now(),))
+
+
+class MemcachedSession(Session):
+
+ # The most popular memcached client for Python isn't thread-safe.
+ # Wrap all .get and .set operations in a single lock.
+ mc_lock = threading.RLock()
+
+ # This is a seperate set of locks per session id.
+ locks = {}
+
+ servers = ['127.0.0.1:11211']
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for memcached-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ import memcache
+ cls.cache = memcache.Client(cls.servers)
+ setup = classmethod(setup)
+
+ def _exists(self):
+ self.mc_lock.acquire()
+ try:
+ return bool(self.cache.get(self.id))
+ finally:
+ self.mc_lock.release()
+
+ def _load(self):
+ self.mc_lock.acquire()
+ try:
+ return self.cache.get(self.id)
+ finally:
+ self.mc_lock.release()
+
+ def _save(self, expiration_time):
+ # Send the expiration time as "Unix time" (seconds since 1/1/1970)
+ td = int(time.mktime(expiration_time.timetuple()))
+ self.mc_lock.acquire()
+ try:
+ if not self.cache.set(self.id, (self._data, expiration_time), td):
+ raise AssertionError("Session data for id %r not set." % self.id)
+ finally:
+ self.mc_lock.release()
+
+ def _delete(self):
+ self.cache.delete(self.id)
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ self.locked = True
+ self.locks.setdefault(self.id, threading.RLock()).acquire()
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ self.locks[self.id].release()
+ self.locked = False
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ raise NotImplementedError
+
+
+# Hook functions (for CherryPy tools)
+
+def save():
+ """Save any changed session data."""
+
+ if not hasattr(cherrypy.serving, "session"):
+ return
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ # Guard against running twice
+ if hasattr(request, "_sessionsaved"):
+ return
+ request._sessionsaved = True
+
+ if response.stream:
+ # If the body is being streamed, we have to save the data
+ # *after* the response has been written out
+ request.hooks.attach('on_end_request', cherrypy.session.save)
+ else:
+ # If the body is not being streamed, we save the data now
+ # (so we can release the lock).
+ if isinstance(response.body, types.GeneratorType):
+ response.collapse_body()
+ cherrypy.session.save()
+save.failsafe = True
+
+def close():
+ """Close the session object for this request."""
+ sess = getattr(cherrypy.serving, "session", None)
+ if getattr(sess, "locked", False):
+ # If the session is still locked we release the lock
+ sess.release_lock()
+close.failsafe = True
+close.priority = 90
+
+
+def init(storage_type='ram', path=None, path_header=None, name='session_id',
+ timeout=60, domain=None, secure=False, clean_freq=5,
+ persistent=True, debug=False, **kwargs):
+ """Initialize session object (using cookies).
+
+ storage_type
+ One of 'ram', 'file', 'postgresql'. This will be used
+ to look up the corresponding class in cherrypy.lib.sessions
+ globals. For example, 'file' will use the FileSession class.
+
+ path
+ The 'path' value to stick in the response cookie metadata.
+
+ path_header
+ If 'path' is None (the default), then the response
+ cookie 'path' will be pulled from request.headers[path_header].
+
+ name
+ The name of the cookie.
+
+ timeout
+ The expiration timeout (in minutes) for the stored session data.
+ If 'persistent' is True (the default), this is also the timeout
+ for the cookie.
+
+ domain
+ The cookie domain.
+
+ secure
+ If False (the default) the cookie 'secure' value will not
+ be set. If True, the cookie 'secure' value will be set (to 1).
+
+ clean_freq (minutes)
+ The poll rate for expired session cleanup.
+
+ persistent
+ If True (the default), the 'timeout' argument will be used
+ to expire the cookie. If False, the cookie will not have an expiry,
+ and the cookie will be a "session cookie" which expires when the
+ browser is closed.
+
+ Any additional kwargs will be bound to the new Session instance,
+ and may be specific to the storage type. See the subclass of Session
+ you're using for more information.
+ """
+
+ request = cherrypy.serving.request
+
+ # Guard against running twice
+ if hasattr(request, "_session_init_flag"):
+ return
+ request._session_init_flag = True
+
+ # Check if request came with a session ID
+ id = None
+ if name in request.cookie:
+ id = request.cookie[name].value
+ if debug:
+ cherrypy.log('ID obtained from request.cookie: %r' % id,
+ 'TOOLS.SESSIONS')
+
+ # Find the storage class and call setup (first time only).
+ storage_class = storage_type.title() + 'Session'
+ storage_class = globals()[storage_class]
+ if not hasattr(cherrypy, "session"):
+ if hasattr(storage_class, "setup"):
+ storage_class.setup(**kwargs)
+
+ # Create and attach a new Session instance to cherrypy.serving.
+ # It will possess a reference to (and lock, and lazily load)
+ # the requested session data.
+ kwargs['timeout'] = timeout
+ kwargs['clean_freq'] = clean_freq
+ cherrypy.serving.session = sess = storage_class(id, **kwargs)
+ sess.debug = debug
+ def update_cookie(id):
+ """Update the cookie every time the session id changes."""
+ cherrypy.serving.response.cookie[name] = id
+ sess.id_observers.append(update_cookie)
+
+ # Create cherrypy.session which will proxy to cherrypy.serving.session
+ if not hasattr(cherrypy, "session"):
+ cherrypy.session = cherrypy._ThreadLocalProxy('session')
+
+ if persistent:
+ cookie_timeout = timeout
+ else:
+ # See http://support.microsoft.com/kb/223799/EN-US/
+ # and http://support.mozilla.com/en-US/kb/Cookies
+ cookie_timeout = None
+ set_response_cookie(path=path, path_header=path_header, name=name,
+ timeout=cookie_timeout, domain=domain, secure=secure)
+
+
+def set_response_cookie(path=None, path_header=None, name='session_id',
+ timeout=60, domain=None, secure=False):
+ """Set a response cookie for the client.
+
+ path
+ the 'path' value to stick in the response cookie metadata.
+
+ path_header
+ if 'path' is None (the default), then the response
+ cookie 'path' will be pulled from request.headers[path_header].
+
+ name
+ the name of the cookie.
+
+ timeout
+ the expiration timeout for the cookie. If 0 or other boolean
+ False, no 'expires' param will be set, and the cookie will be a
+ "session cookie" which expires when the browser is closed.
+
+ domain
+ the cookie domain.
+
+ secure
+ if False (the default) the cookie 'secure' value will not
+ be set. If True, the cookie 'secure' value will be set (to 1).
+
+ """
+ # Set response cookie
+ cookie = cherrypy.serving.response.cookie
+ cookie[name] = cherrypy.serving.session.id
+ cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
+ or '/')
+
+ # We'd like to use the "max-age" param as indicated in
+ # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
+ # save it to disk and the session is lost if people close
+ # the browser. So we have to use the old "expires" ... sigh ...
+## cookie[name]['max-age'] = timeout * 60
+ if timeout:
+ e = time.time() + (timeout * 60)
+ cookie[name]['expires'] = httputil.HTTPDate(e)
+ if domain is not None:
+ cookie[name]['domain'] = domain
+ if secure:
+ cookie[name]['secure'] = 1
+
+
+def expire():
+ """Expire the current session cookie."""
+ name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id')
+ one_year = 60 * 60 * 24 * 365
+ e = time.time() - one_year
+ cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
+
+