2
# Copyright 2004 Apache Software Foundation
4
# Licensed under the Apache License, Version 2.0 (the "License"); you
5
# may not use this file except in compliance with the License. You
6
# may obtain a copy of the License at
8
# http://www.apache.org/licenses/LICENSE-2.0
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13
# implied. See the License for the specific language governing
14
# permissions and limitations under the License.
16
# Originally developed by Gregory Trubetskoy.
18
# $Id: Cookie.py,v 1.11 2004/02/16 19:47:27 grisha Exp $
22
This module contains classes to support HTTP State Management
23
Mechanism, also known as Cookies. The classes provide simple
24
ways for creating, parsing and digitally signing cookies, as
25
well as the ability to store simple Python objects in Cookies
28
The behaviour of the classes is designed to be most useful
29
within mod_python applications.
31
The current state of HTTP State Management standardization is
32
rather unclear. It appears that the de-facto standard is the
33
original Netscape specification, even though already two RFC's
34
have been put out (RFC2109 (1997) and RFC2965 (2000)). The
35
RFC's add a couple of useful features (e.g. using Max-Age instead
36
of Expires, but my limited tests show that Max-Age is ignored
37
by the two browsers tested (IE and Safari). As a result of this,
38
perhaps trying to be RFC-compliant (by automatically providing
39
Max-Age and Version) could be a waste of cookie space...
51
class CookieError(Exception):
54
class metaCookie(type):
56
def __new__(cls, clsname, bases, clsdict):
59
"version", "path", "domain", "secure",
60
"comment", "expires", "max_age",
62
"commentURL", "discard", "port")
64
# _valid_attr + property values
65
# (note __slots__ is a new Python feature, it
66
# prevents any other attribute from being set)
67
__slots__ = _valid_attr + ("name", "value", "_value",
68
"_expires", "__data__")
70
clsdict["_valid_attr"] = _valid_attr
71
clsdict["__slots__"] = __slots__
73
def set_expires(self, value):
75
if type(value) == type(""):
76
# if it's a string, it should be
77
# valid format as per Netscape spec
79
t = time.strptime(value, "%a, %d-%b-%Y %H:%M:%S GMT")
81
raise ValueError, "Invalid expires time: %s" % value
84
# otherwise assume it's a number
85
# representing time as from time.time()
87
value = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
90
self._expires = "%s" % value
92
def get_expires(self):
95
clsdict["expires"] = property(fget=get_expires, fset=set_expires)
97
return type.__new__(cls, clsname, bases, clsdict)
101
This class implements the basic Cookie functionality. Note that
102
unlike the Python Standard Library Cookie class, this class represents
103
a single cookie (not a list of Morsels).
106
__metaclass__ = metaCookie
108
def parse(Class, str):
110
Parse a Cookie or Set-Cookie header value, and return
111
a dict of Cookies. Note: the string should NOT include the
112
header name, only the value.
115
dict = _parse_cookie(str, Class)
118
parse = classmethod(parse)
120
def __init__(self, name, value, **kw):
123
This constructor takes at least a name and value as the
124
arguments, as well as optionally any of allowed cookie attributes
125
as defined in the existing cookie standards.
127
self.name, self.value = name, value
130
setattr(self, k.lower(), kw[k])
132
# subclasses can use this for internal stuff
139
Provides the string representation of the Cookie suitable for
140
sending to the browser. Note that the actual header name will
141
not be part of the string.
143
This method makes no attempt to automatically double-quote
144
strings that contain special characters, even though the RFC's
145
dictate this. This is because doing so seems to confuse most
149
result = ["%s=%s" % (self.name, self.value)]
150
for name in self._valid_attr:
151
if hasattr(self, name):
152
if name in ("secure", "discard"):
155
result.append("%s=%s" % (name, getattr(self, name)))
156
return "; ".join(result)
159
return '<%s: %s>' % (self.__class__.__name__,
163
class SignedCookie(Cookie):
165
This is a variation of Cookie that provides automatic
166
cryptographic signing of cookies and verification. It uses
167
the HMAC support in the Python standard library. This ensures
168
that the cookie has not been tamprered with on the client side.
170
Note that this class does not encrypt cookie data, thus it
171
is still plainly visible as part of the cookie.
174
def parse(Class, s, secret):
176
dict = _parse_cookie(s, Class)
183
# downgrade to Cookie
184
dict[k] = Cookie.parse(Cookie.__str__(c))[k]
188
parse = classmethod(parse)
190
def __init__(self, name, value, secret=None, **kw):
191
Cookie.__init__(self, name, value, **kw)
193
self.__data__["secret"] = secret
195
def hexdigest(self, str):
196
if not self.__data__["secret"]:
197
raise CookieError, "Cannot sign without a secret"
198
_hmac = hmac.new(self.__data__["secret"], self.name)
200
return _hmac.hexdigest()
204
result = ["%s=%s%s" % (self.name, self.hexdigest(self.value),
206
for name in self._valid_attr:
207
if hasattr(self, name):
208
if name in ("secure", "discard"):
211
result.append("%s=%s" % (name, getattr(self, name)))
212
return "; ".join(result)
214
def unsign(self, secret):
216
sig, val = self.value[:32], self.value[32:]
218
mac = hmac.new(secret, self.name)
221
if mac.hexdigest() == sig:
223
self.__data__["secret"] = secret
225
raise CookieError, "Incorrectly Signed Cookie: %s=%s" % (self.name, self.value)
228
class MarshalCookie(SignedCookie):
231
This is a variation of SignedCookie that can store more than
232
just strings. It will automatically marshal the cookie value,
233
therefore any marshallable object can be used as value.
235
The standard library Cookie module provides the ability to pickle
236
data, which is a major security problem. It is believed that unmarshalling
237
(as opposed to unpickling) is safe, yet we still err on the side of caution
238
which is why this class is a subclass of SignedCooke making sure what
239
we are about to unmarshal passes the digital signature test.
241
Here is a link to a sugesstion that marshalling is safer than unpickling
242
http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&selm=7xn0hcugmy.fsf%40ruckus.brouhaha.com
245
def parse(Class, s, secret):
247
dict = _parse_cookie(s, Class)
253
except (CookieError, ValueError):
254
# downgrade to Cookie
255
dict[k] = Cookie.parse(Cookie.__str__(c))[k]
259
parse = classmethod(parse)
263
m = base64.encodestring(marshal.dumps(self.value))[:-1]
265
result = ["%s=%s%s" % (self.name, self.hexdigest(m), m)]
266
for name in self._valid_attr:
267
if hasattr(self, name):
268
if name in ("secure", "discard"):
271
result.append("%s=%s" % (name, getattr(self, name)))
272
return "; ".join(result)
274
def unmarshal(self, secret):
277
self.value = marshal.loads(base64.decodestring(self.value))
281
# This is a simplified and in some places corrected
282
# (at least I think it is) pattern from standard lib Cookie.py
284
_cookiePattern = re.compile(
285
r"(?x)" # Verbose pattern
286
r"[,\ ]*" # space/comma (RFC2616 4.2) before attr-val is eaten
287
r"(?P<key>" # Start of group 'key'
288
r"[^;\ =]+" # anything but ';', ' ' or '='
289
r")" # End of group 'key'
290
r"\ *(=\ *)?" # a space, then may be "=", more space
291
r"(?P<val>" # Start of group 'val'
292
r'"(?:[^\\"]|\\.)*"' # a doublequoted string
294
r"[^;]*" # any word or empty string
295
r")" # End of group 'val'
296
r"\s*;?" # probably ending in a semi-colon
299
def _parse_cookie(str, Class):
301
# XXX problem is we should allow duplicate
305
# max-age is a problem because of the '-'
306
# XXX there should be a more elegant way
307
valid = Cookie._valid_attr + ("max-age",)
310
matchIter = _cookiePattern.finditer(str)
312
for match in matchIter:
314
key, val = match.group("key"), match.group("val")
323
if (l_key in valid or key[0] == '$'):
325
# "internal" attribute, add to cookie
327
if l_key == "max-age":
329
setattr(c, l_key, val)
333
c = Class(l_key, val)
338
def add_cookie(req, cookie, value="", **kw):
340
Sets a cookie in outgoing headers and adds a cache
341
directive so that caches don't cache the cookie.
345
if not isinstance(cookie, Cookie):
348
cookie = Cookie(cookie, value, **kw)
350
if not req.headers_out.has_key("Set-Cookie"):
351
req.headers_out.add("Cache-Control", 'no-cache="set-cookie"')
353
req.headers_out.add("Set-Cookie", str(cookie))
355
def get_cookies(req, Class=Cookie, **kw):
357
A shorthand for retrieveing and parsing cookies given
358
a Cookie class. The class must be one of the classes from
362
if not req.headers_in.has_key("cookie"):
365
cookies = req.headers_in["cookie"]
366
if type(cookies) == type([]):
367
cookies = '; '.join(cookies)
369
return Class.parse(cookies, **kw)