You are viewing a plain text version of this content. The canonical link for it is here.
Posted to mod_python-commits@quetz.apache.org by gr...@apache.org on 2003/06/30 20:04:35 UTC
cvs commit: httpd-python/test test.py
grisha 2003/06/30 11:04:35
Modified: lib/python/mod_python Cookie.py
test test.py
Log:
This patch adjusts the Cookie implementation using __metaclass__ which
reduces the amount of code and makes inheritance work correctly (before,
properties didn't get inherited).
Unfortunately it looks like I had DOS end-of-lines before, and now they
are in UNIX, but as a sideeffect this patch replaces the whole file.
Revision Changes Path
1.4 +456 -472 httpd-python/lib/python/mod_python/Cookie.py
Index: Cookie.py
===================================================================
RCS file: /home/cvs/httpd-python/lib/python/mod_python/Cookie.py,v
retrieving revision 1.3
retrieving revision 1.4
diff -u -r1.3 -r1.4
--- Cookie.py 27 Jun 2003 16:59:05 -0000 1.3
+++ Cookie.py 30 Jun 2003 18:04:35 -0000 1.4
@@ -1,472 +1,456 @@
- # ====================================================================
- # The Apache Software License, Version 1.1
- #
- # Copyright (c) 2000-2003 The Apache Software Foundation. All rights
- # reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions
- # are met:
- #
- # 1. Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- #
- # 2. Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in
- # the documentation and/or other materials provided with the
- # distribution.
- #
- # 3. The end-user documentation included with the redistribution,
- # if any, must include the following acknowledgment:
- # "This product includes software developed by the
- # Apache Software Foundation (http://www.apache.org/)."
- # Alternately, this acknowledgment may appear in the software itself,
- # if and wherever such third-party acknowledgments normally appear.
- #
- # 4. The names "Apache" and "Apache Software Foundation" must
- # not be used to endorse or promote products derived from this
- # software without prior written permission. For written
- # permission, please contact apache@apache.org.
- #
- # 5. Products derived from this software may not be called "Apache",
- # "mod_python", or "modpython", nor may these terms appear in their
- # name, without prior written permission of the Apache Software
- # Foundation.
- #
- # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
- # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- # DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
- # ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
- # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
- # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
- # SUCH DAMAGE.
- # ====================================================================
- #
- # This software consists of voluntary contributions made by many
- # individuals on behalf of the Apache Software Foundation. For more
- # information on the Apache Software Foundation, please see
- # <http://www.apache.org/>.
- #
- # Originally developed by Gregory Trubetskoy.
- #
- # $Id$
-
-"""
-
-This module contains classes to support HTTP State Management
-Mechanism, also known as Cookies. The classes provide simple
-ways for creating, parsing and digitally signing cookies, as
-well as the ability to store simple Python objects in Cookies
-(using marshalling).
-
-The behaviour of the classes is designed to be most useful
-within mod_python applications.
-
-The current state of HTTP State Management standardization is
-rather unclear. It appears that the de-facto standard is the
-original Netscape specification, even though already two RFC's
-have been put out (RFC2109 (1997) and RFC2965 (2000)). The
-RFC's add a couple of useful features (e.g. using Max-Age instead
-of Expires, but my limited tests show that Max-Age is ignored
-by the two browsers tested (IE and Safari). As a result of this,
-perhaps trying to be RFC-compliant (by automatically providing
-Max-Age and Version) could be a waste of cookie space...
-
-
-Sample usage:
-
-A "Cookie" is a cookie, not a list of cookies as in std lib Cookie.py
-
-* making a cookie:
-
->>> c = Cookie("spam", "eggs")
->>> print c
-spam=eggs; version=1
->>> c.max_age = 3
->>> str(c)
-'spam=eggs; version=1; expires=Sat, 14-Jun-2003 02:42:36 GMT; max_age=3'
->>>
-
-* bogus attributes not allowed:
-
->>> c.eggs = 24
-Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- AttributeError: 'Cookie' object has no attribute 'eggs'
-
-* parsing (note the result is a dict of cookies)
-
->>> Cookie.parse(str(c))
-{'spam': <Cookie: spam=eggs; version=1; expires=Sat, 14-Jun-2003 02:42:36 GMT; max_age=3>}
->>>
-
-* signed cookies (uses hmac):
-
->>> sc = SignedCookie("spam", "eggs", "secret")
->>> print sc
-spam=da1170b718dfbad95c392db649d24898eggs; version=1
->>>
-
-* parsing signed cookies:
-
->>> SignedCookie.parse("secret", str(sc))
-{'spam': <SignedCookie: spam=da1170b718dfbad95c392db649d24898eggs; version=1>}
->>>
-
->>> SignedCookie.parse("evil", str(sc))
- [snip]
- Cookie.CookieError: Incorrectly Signed Cookie: spam=da1170b718dfbad95c392db649d24898eggs
->>>
-
-* marshal cookies (subclass of SignedCookie, so MUST be signed),
- also - this is marshal, not pickle (that would be too scary):
-
->>> mc = MarshalCookie("spam", {"eggs":24}, "secret")
->>> print mc
-spam=a90f71893109ca246ab68860f552302ce3MEAAAAZWdnc2kYAAAAMA==; version=1
->>>
-
->>> newmc = MarshalCookie.parse("secret", str(mc))
->>> print newmc["spam"]
-spam=a90f71893109ca246ab68860f552302ce3MEAAAAZWdnc2kYAAAAMA==; version=1
->>> newmc["spam"].value
-{'eggs': 24}
->>>
-
-NB: This module is named Cookie with a capital C so as to let people
-have a variable called cookie without accidently overwriting the
-module.
-
-"""
-
-import time
-import re
-import hmac
-import marshal
-import base64
-
-class CookieError(Exception):
- pass
-
-class Cookie(object):
- """
- This class implements the basic Cookie functionality. Note that
- unlike the Python Standard Library Cookie class, this class represents
- a single cookie (not a list of Morsels).
- """
-
- _valid_attr = (
- "version", "path", "domain", "secure",
- "comment", "expires", "max_age",
- # RFC 2965
- "commentURL", "discard", "port")
-
- # _valid_attr + property values
- # (note __slots__ is a new Python feature, it
- # prevents any other attribute from being set)
- __slots__ = _valid_attr + ("name", "value", "_value",
- "_expires", "_max_age")
-
- def parse(Class, str):
- """
- Parse a Cookie or Set-Cookie header value, and return
- a dict of Cookies. Note: the string should NOT include the
- header name, only the value.
- """
-
- dict = _parseCookie(str, Class)
- return dict
-
- parse = classmethod(parse)
-
-
- def __init__(self, name, value, **kw):
-
- """
- This constructor takes at least a name and value as the
- arguments, as well as optionally any of allowed cookie attributes
- as defined in the existing cookie standards.
- """
-
- self.name, self.value = name, value
-
- for k in kw:
- setattr(self, k.lower(), kw[k])
-
-
- def __str__(self):
-
- """
- Provides the string representation of the Cookie suitable for
- sending to the browser. Note that the actual header name will
- not be part of the string.
-
- This method makes no attempt to automatically double-quote
- strings that contain special characters, even though the RFC's
- dictate this. This is because doing so seems to confuse most
- browsers out there.
- """
-
- result = ["%s=%s" % (self.name, self.value)]
- for name in self._valid_attr:
- if hasattr(self, name):
- if name in ("secure", "discard"):
- result.append(name)
- else:
- result.append("%s=%s" % (name, getattr(self, name)))
- return "; ".join(result)
-
- def __repr__(self):
- return '<%s: %s>' % (self.__class__.__name__,
- str(self))
-
- def set_expires(self, value):
-
- if type(value) == type(""):
- # if it's a string, it should be
- # valid format as per Netscape spec
- try:
- t = time.strptime(value, "%a, %d-%b-%Y %H:%M:%S GMT")
- except ValueError:
- raise ValueError, "Invalid expires time: %s" % value
- t = time.mktime(t)
- else:
- # otherwise assume it's a number
- # representing time as from time.time()
- t = value
- value = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
- time.gmtime(t))
-
- self._expires = "%s" % value
-
- def get_expires(self):
- return self._expires
-
- def set_max_age(self, value):
-
- self._max_age = int(value)
-
- # if expires not already set, make it
- # match max_age for those old browsers that
- # only understand the Netscape spec
- if not hasattr(self, "expires"):
- self._expires = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
- time.gmtime(time.time() +
- self._max_age))
-
- def get_max_age(self):
- return self._max_age
-
- expires = property(fget=get_expires, fset=set_expires)
- max_age = property(fget=get_max_age, fset=set_max_age)
-
-class SignedCookie(Cookie):
- """
- This is a variation of Cookie that provides automatic
- cryptographic signing of cookies and verification. It uses
- the HMAC support in the Python standard library. This ensures
- that the cookie has not been tamprered with on the client side.
-
- Note that this class does not encrypt cookie data, thus it
- is still plainly visible as part of the cookie.
- """
-
- def parse(Class, secret, str):
-
- dict = _parseCookie(str, Class)
-
- for k in dict:
- dict[k].unsign(secret)
-
- return dict
-
- parse = classmethod(parse)
-
- __slots__ = Cookie.__slots__ + ("_secret",)
-
- expires = property(fget=Cookie.get_expires, fset=Cookie.set_expires)
- max_age = property(fget=Cookie.get_max_age, fset=Cookie.set_max_age)
-
- def __init__(self, name, value, secret=None, **kw):
-
- self._secret = secret
-
- Cookie.__init__(self, name, value, **kw)
-
- def hexdigest(self, str):
- if not self._secret:
- raise CookieError, "Cannot sign without a secret"
- _hmac = hmac.new(self._secret, self.name)
- _hmac.update(str)
- return _hmac.hexdigest()
-
- def __str__(self):
-
- result = ["%s=%s%s" % (self.name, self.hexdigest(self.value),
- self.value)]
- for name in self._valid_attr:
- if hasattr(self, name):
- if name in ("secure", "discard"):
- result.append(name)
- else:
- result.append("%s=%s" % (name, getattr(self, name)))
- return "; ".join(result)
-
- def unsign(self, secret):
-
- sig, val = self.value[:32], self.value[32:]
-
- mac = hmac.new(secret, self.name)
- mac.update(val)
-
- if mac.hexdigest() == sig:
- self.value = val
- self._secret = secret
- else:
- raise CookieError, "Incorrectly Signed Cookie: %s=%s" % (self.name, self.value)
-
-
-class MarshalCookie(SignedCookie):
-
- """
- This is a variation of SignedCookie that can store more than
- just strings. It will automatically marshal the cookie value,
- therefore any marshallable object can be used as value.
-
- The standard library Cookie module provides the ability to pickle
- data, which is a major security problem. It is believed that unmarshalling
- (as opposed to unpickling) is safe, yet we still err on the side of caution
- which is why this class is a subclass of SignedCooke making sure what
- we are about to unmarshal passes the digital signature test.
-
- Here is a link to a sugesstion that marshalling is safer than unpickling
- http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&selm=7xn0hcugmy.fsf%40ruckus.brouhaha.com
- """
- __slots__ = SignedCookie.__slots__
-
- expires = property(fget=Cookie.get_expires, fset=Cookie.set_expires)
- max_age = property(fget=Cookie.get_max_age, fset=Cookie.set_max_age)
-
- def parse(Class, secret, str):
-
- dict = _parseCookie(str, Class)
-
- for k in dict:
- dict[k].unmarshal(secret)
-
- return dict
-
- parse = classmethod(parse)
-
- def __str__(self):
-
- m = base64.encodestring(marshal.dumps(self.value))[:-1]
-
- result = ["%s=%s%s" % (self.name, self.hexdigest(m), m)]
- for name in self._valid_attr:
- if hasattr(self, name):
- if name in ("secure", "discard"):
- result.append(name)
- else:
- result.append("%s=%s" % (name, getattr(self, name)))
- return "; ".join(result)
-
- def unmarshal(self, secret):
-
- self.unsign(secret)
- self.value = marshal.loads(base64.decodestring(self.value))
-
-
-
-# This is a simplified and in some places corrected
-# (at least I think it is) pattern from standard lib Cookie.py
-
-_cookiePattern = re.compile(
- r"(?x)" # Verbose pattern
- r"[,\ ]*" # space/comma (RFC2616 4.2) before attr-val is eaten
- r"(?P<key>" # Start of group 'key'
- r"[^;\ =]+" # anything but ';', ' ' or '='
- r")" # End of group 'key'
- r"\ *(=\ *)?" # apace, then may be "=", more space
- r"(?P<val>" # Start of group 'val'
- r'"(?:[^\\"]|\\.)*"' # a doublequoted string
- r"|" # or
- r"[^;]*" # any word or empty string
- r")" # End of group 'val'
- r"\s*;?" # probably ending in a semi-colon
- )
-
-def _parseCookie(str, Class):
-
- # XXX problem is we should allow duplicate
- # strings
- result = {}
-
- # max-age is a problem because of the '-'
- # XXX there should be a more elegant way
- valid = Cookie._valid_attr + ("max-age",)
-
- c = None
- matchIter = _cookiePattern.finditer(str)
-
- for match in matchIter:
-
- key, val = match.group("key"), match.group("val")
-
- if not c:
- # new cookie
- c = Class(key, val)
- result[key] = c
-
- l_key = key.lower()
-
- if (l_key in valid or key[0] == '$'):
-
- # "internal" attribute, add to cookie
-
- if l_key == "max-age":
- l_key = "max_age"
- setattr(c, l_key, val)
-
- else:
- # start a new cookie
- c = Class(key, val)
- result[key] = c
-
- return result
-
-
-def setCookie(req, cookie):
- """
- Sets a cookie in outgoing headers and adds a cache
- directive so that caches don't cache the cookie.
- """
-
- if not req.headers_out.has_key("Set-Cookie"):
- req.headers_out.add("Cache-Control", 'no-cache="set-cookie"')
-
- req.headers_out.add("Set-Cookie", str(cookie))
-
-def getCookie(req, Class=Cookie, data=None):
- """
- A shorthand for retrieveing and parsing cookies given
- a Cookie class. The class must be one of the classes from
- this module.
- """
-
- if not req.headers_in.has_key("cookie"):
- return None
-
- cookies = req.headers_in["cookie"]
- if type(cookies) == type([]):
- cookies = '; '.join(cookies)
-
- if data:
- return Class.parse(data, cookies)
- else:
- return Class.parse(cookies)
+ # ====================================================================
+ # The Apache Software License, Version 1.1
+ #
+ # Copyright (c) 2000-2003 The Apache Software Foundation. All rights
+ # reserved.
+ #
+ # Redistribution and use in source and binary forms, with or without
+ # modification, are permitted provided that the following conditions
+ # are met:
+ #
+ # 1. Redistributions of source code must retain the above copyright
+ # notice, this list of conditions and the following disclaimer.
+ #
+ # 2. Redistributions in binary form must reproduce the above copyright
+ # notice, this list of conditions and the following disclaimer in
+ # the documentation and/or other materials provided with the
+ # distribution.
+ #
+ # 3. The end-user documentation included with the redistribution,
+ # if any, must include the following acknowledgment:
+ # "This product includes software developed by the
+ # Apache Software Foundation (http://www.apache.org/)."
+ # Alternately, this acknowledgment may appear in the software itself,
+ # if and wherever such third-party acknowledgments normally appear.
+ #
+ # 4. The names "Apache" and "Apache Software Foundation" must
+ # not be used to endorse or promote products derived from this
+ # software without prior written permission. For written
+ # permission, please contact apache@apache.org.
+ #
+ # 5. Products derived from this software may not be called "Apache",
+ # "mod_python", or "modpython", nor may these terms appear in their
+ # name, without prior written permission of the Apache Software
+ # Foundation.
+ #
+ # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ # DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ # ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ # SUCH DAMAGE.
+ # ====================================================================
+ #
+ # This software consists of voluntary contributions made by many
+ # individuals on behalf of the Apache Software Foundation. For more
+ # information on the Apache Software Foundation, please see
+ # <http://www.apache.org/>.
+ #
+ # Originally developed by Gregory Trubetskoy.
+ #
+ # $Id$
+
+"""
+
+This module contains classes to support HTTP State Management
+Mechanism, also known as Cookies. The classes provide simple
+ways for creating, parsing and digitally signing cookies, as
+well as the ability to store simple Python objects in Cookies
+(using marshalling).
+
+The behaviour of the classes is designed to be most useful
+within mod_python applications.
+
+The current state of HTTP State Management standardization is
+rather unclear. It appears that the de-facto standard is the
+original Netscape specification, even though already two RFC's
+have been put out (RFC2109 (1997) and RFC2965 (2000)). The
+RFC's add a couple of useful features (e.g. using Max-Age instead
+of Expires, but my limited tests show that Max-Age is ignored
+by the two browsers tested (IE and Safari). As a result of this,
+perhaps trying to be RFC-compliant (by automatically providing
+Max-Age and Version) could be a waste of cookie space...
+
+
+Sample usage:
+
+A "Cookie" is a cookie, not a list of cookies as in std lib Cookie.py
+
+* making a cookie:
+
+>>> c = Cookie("spam", "eggs")
+>>> print c
+spam=eggs; version=1
+>>> c.max_age = 3
+>>> str(c)
+'spam=eggs; version=1; expires=Sat, 14-Jun-2003 02:42:36 GMT; max_age=3'
+>>>
+
+* bogus attributes not allowed:
+
+>>> c.eggs = 24
+Traceback (most recent call last):
+ File "<stdin>", line 1, in ?
+ AttributeError: 'Cookie' object has no attribute 'eggs'
+
+* parsing (note the result is a dict of cookies)
+
+>>> Cookie.parse(str(c))
+{'spam': <Cookie: spam=eggs; version=1; expires=Sat, 14-Jun-2003 02:42:36 GMT; max_age=3>}
+>>>
+
+* signed cookies (uses hmac):
+
+>>> sc = SignedCookie("spam", "eggs", "secret")
+>>> print sc
+spam=da1170b718dfbad95c392db649d24898eggs; version=1
+>>>
+
+* parsing signed cookies:
+
+>>> SignedCookie.parse("secret", str(sc))
+{'spam': <SignedCookie: spam=da1170b718dfbad95c392db649d24898eggs; version=1>}
+>>>
+
+>>> SignedCookie.parse("evil", str(sc))
+ [snip]
+ Cookie.CookieError: Incorrectly Signed Cookie: spam=da1170b718dfbad95c392db649d24898eggs
+>>>
+
+* marshal cookies (subclass of SignedCookie, so MUST be signed),
+ also - this is marshal, not pickle (that would be too scary):
+
+>>> mc = MarshalCookie("spam", {"eggs":24}, "secret")
+>>> print mc
+spam=a90f71893109ca246ab68860f552302ce3MEAAAAZWdnc2kYAAAAMA==; version=1
+>>>
+
+>>> newmc = MarshalCookie.parse("secret", str(mc))
+>>> print newmc["spam"]
+spam=a90f71893109ca246ab68860f552302ce3MEAAAAZWdnc2kYAAAAMA==; version=1
+>>> newmc["spam"].value
+{'eggs': 24}
+>>>
+
+"""
+
+import time
+import re
+import hmac
+import marshal
+import base64
+
+from mod_python import apache
+
+class CookieError(Exception):
+ pass
+
+class metaCookie(type):
+
+ def __new__(cls, clsname, bases, clsdict):
+
+ _valid_attr = (
+ "version", "path", "domain", "secure",
+ "comment", "expires", "max_age",
+ # RFC 2965
+ "commentURL", "discard", "port")
+
+ # _valid_attr + property values
+ # (note __slots__ is a new Python feature, it
+ # prevents any other attribute from being set)
+ __slots__ = _valid_attr + ("name", "value", "_value",
+ "_expires", "__data__")
+
+ clsdict["_valid_attr"] = _valid_attr
+ clsdict["__slots__"] = __slots__
+
+ def set_expires(self, value):
+
+ if type(value) == type(""):
+ # if it's a string, it should be
+ # valid format as per Netscape spec
+ try:
+ t = time.strptime(value, "%a, %d-%b-%Y %H:%M:%S GMT")
+ except ValueError:
+ raise ValueError, "Invalid expires time: %s" % value
+ t = time.mktime(t)
+ else:
+ # otherwise assume it's a number
+ # representing time as from time.time()
+ t = value
+ value = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
+ time.gmtime(t))
+
+ self._expires = "%s" % value
+
+ def get_expires(self):
+ return self._expires
+
+ clsdict["expires"] = property(fget=get_expires, fset=set_expires)
+
+ return type.__new__(cls, clsname, bases, clsdict)
+
+class Cookie(object):
+ """
+ This class implements the basic Cookie functionality. Note that
+ unlike the Python Standard Library Cookie class, this class represents
+ a single cookie (not a list of Morsels).
+ """
+
+ __metaclass__ = metaCookie
+
+ def parse(Class, str):
+ """
+ Parse a Cookie or Set-Cookie header value, and return
+ a dict of Cookies. Note: the string should NOT include the
+ header name, only the value.
+ """
+
+ dict = _parseCookie(str, Class)
+ return dict
+
+ parse = classmethod(parse)
+
+ def __init__(self, name, value, **kw):
+
+ """
+ This constructor takes at least a name and value as the
+ arguments, as well as optionally any of allowed cookie attributes
+ as defined in the existing cookie standards.
+ """
+ self.name, self.value = name, value
+
+ for k in kw:
+ setattr(self, k.lower(), kw[k])
+
+ # subclasses can use this for internal stuff
+ self.__data__ = {}
+
+
+ def __str__(self):
+
+ """
+ Provides the string representation of the Cookie suitable for
+ sending to the browser. Note that the actual header name will
+ not be part of the string.
+
+ This method makes no attempt to automatically double-quote
+ strings that contain special characters, even though the RFC's
+ dictate this. This is because doing so seems to confuse most
+ browsers out there.
+ """
+
+ result = ["%s=%s" % (self.name, self.value)]
+ for name in self._valid_attr:
+ if hasattr(self, name):
+ if name in ("secure", "discard"):
+ result.append(name)
+ else:
+ result.append("%s=%s" % (name, getattr(self, name)))
+ return "; ".join(result)
+
+ def __repr__(self):
+ return '<%s: %s>' % (self.__class__.__name__,
+ str(self))
+
+
+class SignedCookie(Cookie):
+ """
+ This is a variation of Cookie that provides automatic
+ cryptographic signing of cookies and verification. It uses
+ the HMAC support in the Python standard library. This ensures
+ that the cookie has not been tamprered with on the client side.
+
+ Note that this class does not encrypt cookie data, thus it
+ is still plainly visible as part of the cookie.
+ """
+
+ def parse(Class, secret, str):
+
+ dict = _parseCookie(str, Class)
+
+ for k in dict:
+ dict[k].unsign(secret)
+
+ return dict
+
+ parse = classmethod(parse)
+
+ def __init__(self, name, value, secret=None, **kw):
+ Cookie.__init__(self, name, value, **kw)
+
+ self.__data__["secret"] = secret
+
+ def hexdigest(self, str):
+ if not self.__data__["secret"]:
+ raise CookieError, "Cannot sign without a secret"
+ _hmac = hmac.new(self.__data__["secret"], self.name)
+ _hmac.update(str)
+ return _hmac.hexdigest()
+
+ def __str__(self):
+
+ result = ["%s=%s%s" % (self.name, self.hexdigest(self.value),
+ self.value)]
+ for name in self._valid_attr:
+ if hasattr(self, name):
+ if name in ("secure", "discard"):
+ result.append(name)
+ else:
+ result.append("%s=%s" % (name, getattr(self, name)))
+ return "; ".join(result)
+
+ def unsign(self, secret):
+
+ sig, val = self.value[:32], self.value[32:]
+
+ mac = hmac.new(secret, self.name)
+ mac.update(val)
+
+ if mac.hexdigest() == sig:
+ self.value = val
+ self.__data__["secret"] = secret
+ else:
+ raise CookieError, "Incorrectly Signed Cookie: %s=%s" % (self.name, self.value)
+
+
+class MarshalCookie(SignedCookie):
+
+ """
+ This is a variation of SignedCookie that can store more than
+ just strings. It will automatically marshal the cookie value,
+ therefore any marshallable object can be used as value.
+
+ The standard library Cookie module provides the ability to pickle
+ data, which is a major security problem. It is believed that unmarshalling
+ (as opposed to unpickling) is safe, yet we still err on the side of caution
+ which is why this class is a subclass of SignedCooke making sure what
+ we are about to unmarshal passes the digital signature test.
+
+ Here is a link to a sugesstion that marshalling is safer than unpickling
+ http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&selm=7xn0hcugmy.fsf%40ruckus.brouhaha.com
+ """
+
+ def parse(Class, secret, str):
+
+ dict = _parseCookie(str, Class)
+
+ for k in dict:
+ dict[k].unmarshal(secret)
+
+ return dict
+
+ parse = classmethod(parse)
+
+ def __str__(self):
+
+ m = base64.encodestring(marshal.dumps(self.value))[:-1]
+
+ result = ["%s=%s%s" % (self.name, self.hexdigest(m), m)]
+ for name in self._valid_attr:
+ if hasattr(self, name):
+ if name in ("secure", "discard"):
+ result.append(name)
+ else:
+ result.append("%s=%s" % (name, getattr(self, name)))
+ return "; ".join(result)
+
+ def unmarshal(self, secret):
+
+ self.unsign(secret)
+ self.value = marshal.loads(base64.decodestring(self.value))
+
+
+
+# This is a simplified and in some places corrected
+# (at least I think it is) pattern from standard lib Cookie.py
+
+_cookiePattern = re.compile(
+ r"(?x)" # Verbose pattern
+ r"[,\ ]*" # space/comma (RFC2616 4.2) before attr-val is eaten
+ r"(?P<key>" # Start of group 'key'
+ r"[^;\ =]+" # anything but ';', ' ' or '='
+ r")" # End of group 'key'
+ r"\ *(=\ *)?" # a space, then may be "=", more space
+ r"(?P<val>" # Start of group 'val'
+ r'"(?:[^\\"]|\\.)*"' # a doublequoted string
+ r"|" # or
+ r"[^;]*" # any word or empty string
+ r")" # End of group 'val'
+ r"\s*;?" # probably ending in a semi-colon
+ )
+
+def _parseCookie(str, Class):
+
+ # XXX problem is we should allow duplicate
+ # strings
+ result = {}
+
+ # max-age is a problem because of the '-'
+ # XXX there should be a more elegant way
+ valid = Cookie._valid_attr + ("max-age",)
+
+ c = None
+ matchIter = _cookiePattern.finditer(str)
+
+ for match in matchIter:
+
+ key, val = match.group("key"), match.group("val")
+
+ if not c:
+ # new cookie
+ c = Class(key, val)
+ result[key] = c
+
+ l_key = key.lower()
+
+ if (l_key in valid or key[0] == '$'):
+
+ # "internal" attribute, add to cookie
+
+ if l_key == "max-age":
+ l_key = "max_age"
+ setattr(c, l_key, val)
+
+ else:
+ # start a new cookie
+ c = Class(key, val)
+ result[key] = c
+
+ return result
+
+def setCookie(req, cookie):
+ """
+ Sets a cookie in outgoing headers and adds a cache
+ directive so that caches don't cache the cookie.
+ """
+
+ if not req.headers_out.has_key("Set-Cookie"):
+ req.headers_out.add("Cache-Control", 'no-cache="set-cookie"')
+
+ req.headers_out.add("Set-Cookie", str(cookie))
+
+def getCookie(req, Class=Cookie, data=None):
+ """
+ A shorthand for retrieveing and parsing cookies given
+ a Cookie class. The class must be one of the classes from
+ this module.
+ """
+
+ if not req.headers_in.has_key("cookie"):
+ return None
+
+ cookies = req.headers_in["cookie"]
+ if type(cookies) == type([]):
+ cookies = '; '.join(cookies)
+
+ if data:
+ return Class.parse(data, cookies)
+ else:
+ return Class.parse(cookies)
1.33 +2 -2 httpd-python/test/test.py
Index: test.py
===================================================================
RCS file: /home/cvs/httpd-python/test/test.py,v
retrieving revision 1.32
retrieving revision 1.33
diff -u -r1.32 -r1.33
--- test.py 24 Jun 2003 04:16:00 -0000 1.32
+++ test.py 30 Jun 2003 18:04:35 -0000 1.33
@@ -893,7 +893,7 @@
print "\n * Testing Cookie.MarshalCookie"
- mc = "eggs=648aba93b961ee717e70531417d75fddWwIAAABzAwAAAGZvb3MDAAAAYmFy"
+ mc = "eggs=d049b2b61adb6a1d895646719a3dc30bcwQAAABzcGFt"
conn = httplib.HTTPConnection("127.0.0.1:%s" % PORT)
conn.putrequest("GET", "/testz.py", skip_host=1)