~ubuntu-branches/ubuntu/warty/libapache2-mod-python/warty

« back to all changes in this revision

Viewing changes to lib/python/mod_python/Cookie.py

  • Committer: Bazaar Package Importer
  • Author(s): Thom May
  • Date: 2004-09-06 20:27:57 UTC
  • Revision ID: james.westby@ubuntu.com-20040906202757-yzpyu1bcabgpjtiu
Tags: upstream-3.1.3
ImportĀ upstreamĀ versionĀ 3.1.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 #
 
2
 # Copyright 2004 Apache Software Foundation 
 
3
 # 
 
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
 
7
 #
 
8
 #      http://www.apache.org/licenses/LICENSE-2.0
 
9
 #
 
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.
 
15
 #
 
16
 # Originally developed by Gregory Trubetskoy.
 
17
 #
 
18
 # $Id: Cookie.py,v 1.11 2004/02/16 19:47:27 grisha Exp $
 
19
 
 
20
"""
 
21
 
 
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
 
26
(using marshalling).
 
27
 
 
28
The behaviour of the classes is designed to be most useful
 
29
within mod_python applications.
 
30
 
 
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...
 
40
 
 
41
"""
 
42
 
 
43
import time
 
44
import re
 
45
import hmac
 
46
import marshal
 
47
import base64
 
48
 
 
49
import apache
 
50
 
 
51
class CookieError(Exception):
 
52
    pass
 
53
 
 
54
class metaCookie(type):
 
55
 
 
56
    def __new__(cls, clsname, bases, clsdict):
 
57
 
 
58
        _valid_attr = (
 
59
            "version", "path", "domain", "secure",
 
60
            "comment", "expires", "max_age",
 
61
            # RFC 2965
 
62
            "commentURL", "discard", "port")
 
63
 
 
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__")
 
69
 
 
70
        clsdict["_valid_attr"] = _valid_attr
 
71
        clsdict["__slots__"] = __slots__
 
72
 
 
73
        def set_expires(self, value):
 
74
 
 
75
            if type(value) == type(""):
 
76
                # if it's a string, it should be
 
77
                # valid format as per Netscape spec
 
78
                try:
 
79
                    t = time.strptime(value, "%a, %d-%b-%Y %H:%M:%S GMT")
 
80
                except ValueError:
 
81
                    raise ValueError, "Invalid expires time: %s" % value
 
82
                t = time.mktime(t)
 
83
            else:
 
84
                # otherwise assume it's a number
 
85
                # representing time as from time.time()
 
86
                t = value
 
87
                value = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
 
88
                                      time.gmtime(t))
 
89
 
 
90
            self._expires = "%s" % value
 
91
 
 
92
        def get_expires(self):
 
93
            return self._expires
 
94
 
 
95
        clsdict["expires"] = property(fget=get_expires, fset=set_expires)
 
96
 
 
97
        return type.__new__(cls, clsname, bases, clsdict)
 
98
 
 
99
class Cookie(object):
 
100
    """
 
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).
 
104
    """
 
105
 
 
106
    __metaclass__ = metaCookie
 
107
 
 
108
    def parse(Class, str):
 
109
        """
 
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.
 
113
        """
 
114
 
 
115
        dict = _parse_cookie(str, Class)
 
116
        return dict
 
117
 
 
118
    parse = classmethod(parse)
 
119
 
 
120
    def __init__(self, name, value, **kw):
 
121
 
 
122
        """
 
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. 
 
126
        """
 
127
        self.name, self.value = name, value
 
128
 
 
129
        for k in kw:
 
130
            setattr(self, k.lower(), kw[k])
 
131
 
 
132
        # subclasses can use this for internal stuff
 
133
        self.__data__ = {}
 
134
 
 
135
 
 
136
    def __str__(self):
 
137
 
 
138
        """
 
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.
 
142
 
 
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
 
146
        browsers out there.
 
147
        """
 
148
        
 
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"):
 
153
                    result.append(name)
 
154
                else:
 
155
                    result.append("%s=%s" % (name, getattr(self, name)))
 
156
        return "; ".join(result)
 
157
    
 
158
    def __repr__(self):
 
159
        return '<%s: %s>' % (self.__class__.__name__,
 
160
                                str(self))
 
161
    
 
162
 
 
163
class SignedCookie(Cookie):
 
164
    """
 
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.
 
169
 
 
170
    Note that this class does not encrypt cookie data, thus it
 
171
    is still plainly visible as part of the cookie.
 
172
    """
 
173
 
 
174
    def parse(Class, s, secret):
 
175
 
 
176
        dict = _parse_cookie(s, Class)
 
177
 
 
178
        for k in dict:
 
179
            c = dict[k]
 
180
            try:
 
181
                c.unsign(secret)
 
182
            except CookieError:
 
183
                # downgrade to Cookie
 
184
                dict[k] = Cookie.parse(Cookie.__str__(c))[k]
 
185
        
 
186
        return dict
 
187
 
 
188
    parse = classmethod(parse)
 
189
 
 
190
    def __init__(self, name, value, secret=None, **kw):
 
191
        Cookie.__init__(self, name, value, **kw)
 
192
 
 
193
        self.__data__["secret"] = secret
 
194
 
 
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)
 
199
        _hmac.update(str)
 
200
        return _hmac.hexdigest()
 
201
 
 
202
    def __str__(self):
 
203
        
 
204
        result = ["%s=%s%s" % (self.name, self.hexdigest(self.value),
 
205
                               self.value)]
 
206
        for name in self._valid_attr:
 
207
            if hasattr(self, name):
 
208
                if name in ("secure", "discard"):
 
209
                    result.append(name)
 
210
                else:
 
211
                    result.append("%s=%s" % (name, getattr(self, name)))
 
212
        return "; ".join(result)
 
213
 
 
214
    def unsign(self, secret):
 
215
 
 
216
        sig, val = self.value[:32], self.value[32:]
 
217
 
 
218
        mac = hmac.new(secret, self.name)
 
219
        mac.update(val)
 
220
 
 
221
        if mac.hexdigest() == sig:
 
222
            self.value = val
 
223
            self.__data__["secret"] = secret
 
224
        else:
 
225
            raise CookieError, "Incorrectly Signed Cookie: %s=%s" % (self.name, self.value)
 
226
 
 
227
 
 
228
class MarshalCookie(SignedCookie):
 
229
 
 
230
    """
 
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.
 
234
 
 
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.
 
240
 
 
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
 
243
    """
 
244
 
 
245
    def parse(Class, s, secret):
 
246
 
 
247
        dict = _parse_cookie(s, Class)
 
248
 
 
249
        for k in dict:
 
250
            c = dict[k]
 
251
            try:
 
252
                c.unmarshal(secret)
 
253
            except (CookieError, ValueError):
 
254
                # downgrade to Cookie
 
255
                dict[k] = Cookie.parse(Cookie.__str__(c))[k]
 
256
 
 
257
        return dict
 
258
 
 
259
    parse = classmethod(parse)
 
260
 
 
261
    def __str__(self):
 
262
        
 
263
        m = base64.encodestring(marshal.dumps(self.value))[:-1]
 
264
 
 
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"):
 
269
                    result.append(name)
 
270
                else:
 
271
                    result.append("%s=%s" % (name, getattr(self, name)))
 
272
        return "; ".join(result)
 
273
 
 
274
    def unmarshal(self, secret):
 
275
 
 
276
        self.unsign(secret)
 
277
        self.value = marshal.loads(base64.decodestring(self.value))
 
278
 
 
279
 
 
280
 
 
281
# This is a simplified and in some places corrected
 
282
# (at least I think it is) pattern from standard lib Cookie.py
 
283
 
 
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
 
293
    r"|"                            # or
 
294
    r"[^;]*"                        # any word or empty string
 
295
    r")"                          # End of group 'val'
 
296
    r"\s*;?"                      # probably ending in a semi-colon
 
297
    )
 
298
 
 
299
def _parse_cookie(str, Class):
 
300
 
 
301
    # XXX problem is we should allow duplicate
 
302
    # strings
 
303
    result = {}
 
304
 
 
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",)
 
308
 
 
309
    c = None
 
310
    matchIter = _cookiePattern.finditer(str)
 
311
 
 
312
    for match in matchIter:
 
313
 
 
314
        key, val = match.group("key"), match.group("val")
 
315
 
 
316
        if not c:
 
317
            # new cookie
 
318
            c = Class(key, val)
 
319
            result[key] = c
 
320
 
 
321
        l_key = key.lower()
 
322
        
 
323
        if (l_key in valid or key[0] == '$'):
 
324
            
 
325
            # "internal" attribute, add to cookie
 
326
 
 
327
            if l_key == "max-age":
 
328
                l_key = "max_age"
 
329
            setattr(c, l_key, val)
 
330
 
 
331
        else:
 
332
            # start a new cookie
 
333
            c = Class(l_key, val)
 
334
            result[l_key] = c
 
335
 
 
336
    return result
 
337
 
 
338
def add_cookie(req, cookie, value="", **kw):
 
339
    """
 
340
    Sets a cookie in outgoing headers and adds a cache
 
341
    directive so that caches don't cache the cookie.
 
342
    """
 
343
 
 
344
    # is this a cookie?
 
345
    if not isinstance(cookie, Cookie):
 
346
 
 
347
        # make a cookie
 
348
        cookie = Cookie(cookie, value, **kw)
 
349
        
 
350
    if not req.headers_out.has_key("Set-Cookie"):
 
351
        req.headers_out.add("Cache-Control", 'no-cache="set-cookie"')
 
352
 
 
353
    req.headers_out.add("Set-Cookie", str(cookie))
 
354
 
 
355
def get_cookies(req, Class=Cookie, **kw):
 
356
    """
 
357
    A shorthand for retrieveing and parsing cookies given
 
358
    a Cookie class. The class must be one of the classes from
 
359
    this module.
 
360
    """
 
361
    
 
362
    if not req.headers_in.has_key("cookie"):
 
363
        return {}
 
364
 
 
365
    cookies = req.headers_in["cookie"]
 
366
    if type(cookies) == type([]):
 
367
        cookies = '; '.join(cookies)
 
368
 
 
369
    return Class.parse(cookies, **kw)
 
370