1
1
"""Session implementation for CherryPy.
3
We use cherrypy.request to store some convenient variables as
4
well as data about the session for the current request. Instead of
5
polluting cherrypy.request we use a Session object bound to
6
cherrypy.session to store these variables.
3
You need to edit your config file to use sessions. Here's an example::
6
tools.sessions.on = True
7
tools.sessions.storage_type = "file"
8
tools.sessions.storage_path = "/home/site/sessions"
9
tools.sessions.timeout = 60
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.
16
By default, the session ID is passed in a cookie, so the client's browser must
17
have cookies enabled for your site.
19
To set data for the current session, use
20
``cherrypy.session['fieldname'] = 'fieldvalue'``;
21
to get data use ``cherrypy.session.get('fieldname')``.
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_lock()``.
31
Regardless of which mode you use, the session is guaranteed to be unlocked when
32
the request is complete.
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.
42
===========================
43
Session Fixation Protection
44
===========================
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**.
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.
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**.
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.
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.
12
import cPickle as pickle
19
from hashlib import sha1 as sha
21
from sha import new as sha
26
93
from warnings import warn
29
from cherrypy.lib import http
96
from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr
97
from cherrypy.lib import httputil
32
100
missing = object()
34
102
class Session(object):
35
103
"""A CherryPy dict-like Session object (one per request)."""
37
__metaclass__ = cherrypy._AttributeDocstrings
40
107
id_observers = None
41
id_observers__doc = "A list of callbacks to which to pass new id's."
108
"A list of callbacks to which to pass new id's."
43
id__doc = "The current session ID."
44
110
def _get_id(self):
46
112
def _set_id(self, value):
48
114
for o in self.id_observers:
50
id = property(_get_id, _set_id, doc=id__doc)
116
id = property(_get_id, _set_id, doc="The current session ID.")
53
timeout__doc = "Number of minutes after which to delete session data."
119
"Number of minutes after which to delete session data."
57
123
If True, this session instance has exclusive read/write access
58
124
to session data."""
62
128
If True, data has been retrieved from storage. This should happen
63
129
automatically on the first attempt to access session data."""
65
131
clean_thread = None
66
clean_thread__doc = "Class-level Monitor which calls self.clean_up."
132
"Class-level Monitor which calls self.clean_up."
69
clean_freq__doc = "The poll rate for expired session cleanup in minutes."
135
"The poll rate for expired session cleanup in minutes."
138
"The session id passed by the client. May be missing or unsafe."
141
"True if the session requested by the client did not exist."
145
True if the application called session.regenerate(). This is not set by
146
internal calls to regenerate the session id."""
71
150
def __init__(self, id=None, **kwargs):
72
151
self.id_observers = []
75
for k, v in kwargs.iteritems():
154
for k, v in kwargs.items():
76
155
setattr(self, k, v)
161
cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
82
165
if not self._exists():
167
cherrypy.log('Expired or malicious session %r; '
168
'making a new one' % id, 'TOOLS.SESSIONS')
83
169
# Expired or malicious session. Make a new one.
84
170
# See http://www.cherrypy.org/ticket/709.
176
"""Generate the session specific concept of 'now'.
178
Other session providers can override this to use alternative,
179
possibly timezone aware, versions of 'now'.
181
return datetime.datetime.now()
88
183
def regenerate(self):
89
184
"""Replace the current session (with a new id)."""
185
self.regenerated = True
188
def _regenerate(self):
90
189
if self.id is not None:
108
207
"""Clean up expired sessions."""
113
except (AttributeError, NotImplementedError):
114
# os.urandom not available until Python 2.4. Fall back to random.random.
115
def generate_id(self):
116
"""Return a new session id."""
117
return sha('%s' % random.random()).hexdigest()
119
def generate_id(self):
120
"""Return a new session id."""
121
return os.urandom(20).encode('hex')
210
def generate_id(self):
211
"""Return a new session id."""
124
215
"""Save session data."""
126
217
# If session data has never been loaded then it's never been
127
# accessed: no need to delete it
218
# accessed: no need to save it
129
220
t = datetime.timedelta(seconds = self.timeout * 60)
130
expiration_time = datetime.datetime.now() + t
221
expiration_time = self.now() + t
223
cherrypy.log('Saving with expiry %s' % expiration_time,
131
225
self._save(expiration_time)
502
609
This should only be called once per process; this will be done
503
610
automatically when using sessions.init (as the built-in Tool does).
505
for k, v in kwargs.iteritems():
612
for k, v in kwargs.items():
506
613
setattr(cls, k, v)
509
616
cls.cache = memcache.Client(cls.servers)
510
617
setup = classmethod(setup)
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')
628
for o in self.id_observers:
630
id = property(_get_id, _set_id, doc="The current session ID.")
512
632
def _exists(self):
513
633
self.mc_lock.acquire()
559
679
if not hasattr(cherrypy.serving, "session"):
681
request = cherrypy.serving.request
682
response = cherrypy.serving.response
562
684
# Guard against running twice
563
if hasattr(cherrypy.request, "_sessionsaved"):
685
if hasattr(request, "_sessionsaved"):
565
cherrypy.request._sessionsaved = True
687
request._sessionsaved = True
567
if cherrypy.response.stream:
568
690
# If the body is being streamed, we have to save the data
569
691
# *after* the response has been written out
570
cherrypy.request.hooks.attach('on_end_request', cherrypy.session.save)
692
request.hooks.attach('on_end_request', cherrypy.session.save)
572
694
# If the body is not being streamed, we save the data now
573
695
# (so we can release the lock).
574
if isinstance(cherrypy.response.body, types.GeneratorType):
575
cherrypy.response.collapse_body()
696
if isinstance(response.body, types.GeneratorType):
697
response.collapse_body()
576
698
cherrypy.session.save()
577
699
save.failsafe = True
589
711
def init(storage_type='ram', path=None, path_header=None, name='session_id',
590
timeout=60, domain=None, secure=False, clean_freq=5, **kwargs):
712
timeout=60, domain=None, secure=False, clean_freq=5,
713
persistent=True, httponly=False, debug=False, **kwargs):
591
714
"""Initialize session object (using cookies).
593
storage_type: one of 'ram', 'file', 'postgresql'. This will be used
594
to look up the corresponding class in cherrypy.lib.sessions
717
One of 'ram', 'file', 'postgresql', 'memcached'. This will be
718
used to look up the corresponding class in cherrypy.lib.sessions
595
719
globals. For example, 'file' will use the FileSession class.
596
path: the 'path' value to stick in the response cookie metadata.
597
path_header: if 'path' is None (the default), then the response
722
The 'path' value to stick in the response cookie metadata.
725
If 'path' is None (the default), then the response
598
726
cookie 'path' will be pulled from request.headers[path_header].
599
name: the name of the cookie.
600
timeout: the expiration timeout (in minutes) for both the cookie and
602
domain: the cookie domain.
603
secure: if False (the default) the cookie 'secure' value will not
729
The name of the cookie.
732
The expiration timeout (in minutes) for the stored session data.
733
If 'persistent' is True (the default), this is also the timeout
740
If False (the default) the cookie 'secure' value will not
604
741
be set. If True, the cookie 'secure' value will be set (to 1).
605
clean_freq (minutes): the poll rate for expired session cleanup.
744
The poll rate for expired session cleanup.
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
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).
607
756
Any additional kwargs will be bound to the new Session instance,
608
757
and may be specific to the storage type. See the subclass of Session
609
758
you're using for more information.
612
request = cherrypy.request
761
request = cherrypy.serving.request
614
763
# Guard against running twice
615
764
if hasattr(request, "_session_init_flag"):
634
786
kwargs['timeout'] = timeout
635
787
kwargs['clean_freq'] = clean_freq
636
788
cherrypy.serving.session = sess = storage_class(id, **kwargs)
637
790
def update_cookie(id):
638
791
"""Update the cookie every time the session id changes."""
639
cherrypy.response.cookie[name] = id
792
cherrypy.serving.response.cookie[name] = id
640
793
sess.id_observers.append(update_cookie)
642
795
# Create cherrypy.session which will proxy to cherrypy.serving.session
643
796
if not hasattr(cherrypy, "session"):
644
797
cherrypy.session = cherrypy._ThreadLocalProxy('session')
800
cookie_timeout = timeout
802
# See http://support.microsoft.com/kb/223799/EN-US/
803
# and http://support.mozilla.com/en-US/kb/Cookies
804
cookie_timeout = None
646
805
set_response_cookie(path=path, path_header=path_header, name=name,
647
timeout=timeout, domain=domain, secure=secure)
806
timeout=cookie_timeout, domain=domain, secure=secure,
650
810
def set_response_cookie(path=None, path_header=None, name='session_id',
651
timeout=60, domain=None, secure=False):
811
timeout=60, domain=None, secure=False, httponly=False):
652
812
"""Set a response cookie for the client.
654
path: the 'path' value to stick in the response cookie metadata.
655
path_header: if 'path' is None (the default), then the response
815
the 'path' value to stick in the response cookie metadata.
818
if 'path' is None (the default), then the response
656
819
cookie 'path' will be pulled from request.headers[path_header].
657
name: the name of the cookie.
658
timeout: the expiration timeout for the cookie.
659
domain: the cookie domain.
660
secure: if False (the default) the cookie 'secure' value will not
822
the name of the cookie.
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.
833
if False (the default) the cookie 'secure' value will not
661
834
be set. If True, the cookie 'secure' value will be set (to 1).
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).
663
841
# Set response cookie
664
cookie = cherrypy.response.cookie
842
cookie = cherrypy.serving.response.cookie
665
843
cookie[name] = cherrypy.serving.session.id
666
cookie[name]['path'] = (path or cherrypy.request.headers.get(path_header)
844
cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
669
847
# We'd like to use the "max-age" param as indicated in
672
850
# the browser. So we have to use the old "expires" ... sigh ...
673
851
## cookie[name]['max-age'] = timeout * 60
675
cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
853
e = time.time() + (timeout * 60)
854
cookie[name]['expires'] = httputil.HTTPDate(e)
676
855
if domain is not None:
677
856
cookie[name]['domain'] = domain
679
858
cookie[name]['secure'] = 1
860
if not cookie[name].isReservedKey('httponly'):
861
raise ValueError("The httponly cookie token is not supported.")
862
cookie[name]['httponly'] = 1
683
865
"""Expire the current session cookie."""
684
name = cherrypy.request.config.get('tools.sessions.name', 'session_id')
866
name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id')
685
867
one_year = 60 * 60 * 24 * 365
686
exp = time.gmtime(time.time() - one_year)
687
t = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", exp)
688
cherrypy.response.cookie[name]['expires'] = t
868
e = time.time() - one_year
869
cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)