[mod_python] Cookie patch

Craig Warren craig.warren at encorp.com
Thu Jan 13 14:44:36 EST 2005


Skipped content of type multipart/alternative-------------- next part --------------
 #
 # Copyright 2004 Apache Software Foundation 
 # 
 # Licensed under the Apache License, Version 2.0 (the "License"); you
 # may not use this file except in compliance with the License.  You
 # may obtain a copy of the License at
 #
 #      http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 # implied.  See the License for the specific language governing
 # permissions and limitations under the License.
 #
 # Originally developed by Gregory Trubetskoy.
 #
 # $Id: Cookie.py,v 1.11 2004/02/16 19:47:27 grisha Exp $

"""

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...

"""

import time
import re
import hmac
import marshal
import base64

#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 = _parse_cookie(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, s, secret):

        dict = _parse_cookie(s, Class)

        for k in dict:
            c = dict[k]
            try:
                c.unsign(secret)
            except CookieError:
                # downgrade to Cookie
                dict[k] = Cookie.parse(Cookie.__str__(c))[k]
        
        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, s, secret):

        dict = _parse_cookie(s, Class)

        for k in dict:
            c = dict[k]
            try:
                c.unmarshal(secret)
            except (CookieError, ValueError):
                # downgrade to Cookie
                dict[k] = Cookie.parse(Cookie.__str__(c))[k]

        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 _parse_cookie(str, Class):
    # XXX problem is we should allow duplicate
    # strings
    result = {}
    all_cookies_attribute = {}

    valid = Cookie._valid_attr

    c = None
    matchIter = _cookiePattern.finditer(str)

    for match in matchIter:

        key, val = match.group("key"), match.group("val")

        # we will check whether the cookie name is a valid attribute name
        # for the previous cookie.
        l_key = key.lower()
        # fix from Craig Warren
        if l_key[0]=='$':
            l_key=l_key[1:]
        if l_key == "max-age":
            l_key = "max_age"
        
        if l_key in valid:
            if not c:
                # 'global' attribute, will be added to all cookies
                all_cookies_attribute[l_key]=val
            else:
                # "internal" attribute, add to cookie
                setattr(c, l_key, val)
        else:
            # start a new cookie
            # we don't use l_key so that we keep the initial name
            # this way we are consistent with the creation of the first cookie
            # as done in the previous version of the function
            c = Class(key, val)

            # XXX this is a bit heavyweight since usually we'll have only 0 or 1
            # global attribute...
            for key, val in all_cookies_attribute.items():
                setattr(c,key,val)

            result[key] = c

    return result


def _parse_cookie_new(str, Class):
    # XXX problem is we should allow duplicate
    # strings
    result = {}
    all_cookies_attribute = {}

    valid = Cookie._valid_attr

    c = None
    matchIter = _cookiePattern.finditer(str)

    for match in matchIter:

        key, val = match.group("key"), match.group("val")

        # we will check whether the cookie name is a valid attribute name
        # for the previous cookie.
        l_key = key.lower()
        # fix from Craig Warren
        if l_key[0]=='$':
            l_key=l_key[1:]
        if l_key == "max-age":
            l_key = "max_age"

        c = Class(key,l_key)
        
        if l_key in valid:
            if key[0] == '$':
                all_cookies_attribute[l_key]=val
            elif c == None:
                c = Class(key,l_key)
            else:
                setattr(c, l_key, val)
        else:
            # start a new cookie
            # we don't use l_key so that we keep the initial name
            # this way we are consistent with the creation of the first cookie
            # as done in the previous version of the function
            c = Class(key, val)

            # XXX this is a bit heavyweight since usually we'll have only 0 or 1
            # global attribute...
            for key, val in all_cookies_attribute.items():
                setattr(c,key,val)

            result[key] = c

    return result

def _parse_cookie_my(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"
            if key[0] == '$':
                l_key = l_key[1:]
            setattr(c, l_key, val)

        else:
            # start a new cookie
            c = Class(l_key, val)
            result[l_key] = c

    return result

def add_cookie(req, cookie, value="", **kw):
    """
    Sets a cookie in outgoing headers and adds a cache
    directive so that caches don't cache the cookie.
    """

    # is this a cookie?
    if not isinstance(cookie, Cookie):

        # make a cookie
        cookie = Cookie(cookie, value, **kw)
        
    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 get_cookies(req, Class=Cookie, **kw):
    """
    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 {}

    cookies = req.headers_in["cookie"]
    if type(cookies) == type([]):
        cookies = '; '.join(cookies)

    return Class.parse(cookies, **kw)

-------------- next part --------------
from Cookie import _parse_cookie,_parse_cookie_my,Cookie

def print_cookies(c):
    print 'Cookies'
    for k in c.keys():
        print c[k]
    print 

c1 = 'test=1;bob=1;$Version=0;$Path=/'
c2 = '$Version=0;$Path=/;test=1'
c3 = 'test=1;$Version=0;bob=1;$Path=/'

tests = [c1,c2,c3]
for t in tests:
    c = _parse_cookie(t,Cookie)
    print 'TEST'
    print ' --------------------------------------------------'
    print 'New Funtion'
    print 'Cookie:%s' % t
    print_cookies(c)

    c = _parse_cookie_my(t,Cookie)
    print 'My function Old with my fix'
    print 'Cookie:%s' % t
    print_cookies(c)
    print ' --------------------------------------------------'
    print





More information about the Mod_python mailing list