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
|