#!/usr/bin/env python """ #### # Copyright (C) 1998 GTE Internetworking # Author: Timothy O'Malley # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Library General Public # License as published by the Free Software Foundation, version 2. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Library General Public License for more details. # # You can retrieve a copy of the GNU Library General Public License # from http://www.gnu.org/. For a copy via US Mail, write to the # Free Software Foundation, Inc. # 59 Temple Place - Suite 330, # Boston, MA 02111-1307 # USA # # GTE Internetworking can be reached electronically via the URL # http://www.bbn.com or via US Mail at # # GTE Internetworking # 5525 N. MacArthur Boulevard # Suite 320 # Irving, Texas 75038 # USA #### # Cookie.py is an update for the old nscookie.py module. # Under the old module, it was not possible to set attributes, # such as "secure" or "Max-Age" on key,value granularity. This # shortcoming has been addressed in Cookie.py but has come at # the cost of a slightly changed interface. Cookie.py also # requires Python-1.5, for the re and cPickle modules. # # The original idea to treat Cookies as a dictionary came from # Dave Mitchel (davem@magnet.com) in 1995, when he released the # first version of nscookie.py. Here's a sample session to show how to use this module. At the moment, this is the only documentation. Importing is easy.. >>> import Cookie Most of the time you start by creating a cookie. The __init__ routine can take several arguments, but that isn't covered here. >>> C = Cookie.Cookie() Now, you can add values to the Cookie just as is if it were a dictionary. >>> C["joe"] = "a cookie" >>> C Set-Cookie: joe="a cookie"; Notice that the printable representation of a Cookie is the appropriate format for a Set-Cookie: header. This is the default behavior. You can change the header by using the the .output() function >>> C.output("Cookie:") 'Cookie: joe="a cookie";' The .load() method of a Cookie extracts cookies from a string. In a CGI script, you would use this method to extract the cookies from the HTTP_COOKIE environment variable. >>> C.load("mary=hadalittlelamb;") >>> C Set-Cookie: mary=hadalittlelamb; Set-Cookie: joe="a cookie"; Each element of the Cookie also supports all of the RFC 2109 Cookie attributes. Here's an example which sets the Path attribute. >>> C["joe"]["path"] = "/home/joe" >>> C Set-Cookie: mary=hadalittlelamb; Set-Cookie: joe="a cookie"; Path=/home/joe; Before I forget, the .load() method is pretty smart about identifying a cookie. Escaped quotation marks and nested semicolons do not confuse it. >>> C.load('lobotomy="joe=wolf; lobotomy=\\"nested quote\\"; mark=\\012;";') >>> C Set-Cookie: mary=hadalittlelamb; Set-Cookie: joe="a cookie"; Path=/home/joe; Set-Cookie: lobotomy="joe=wolf; lobotomy=\"nested quote\"; mark=\012;"; Each dictionary element has a 'value' attribute, which gives you back the value associated with the key. >>> C["joe"].value 'a cookie' >>> C["lobotomy"].value 'joe=wolf; lobotomy="nested quote"; mark=\012;' If you set a cookie to a non-string object, that object is automatically pickled (using cPickle or pickle) in the Set-Cookie: header. >>> C["int"] = 7 >>> C Set-Cookie: lobotomy="joe=wolf; lobotomy=\"nested quote\"; mark=\012;"; Set-Cookie: joe="a cookie"; Path=/home/joe; Set-Cookie: mary=hadalittlelamb; Set-Cookie: int="I7\012."; If the .load() method finds a pickled object in the string, then it automatically unpickles it. The 'value' attribute gives you back the true value, not the encoded representation. >>> C.load('anotherint="I45\\012.";') >>> C["anotherint"].value 45 >>> C["int"].value 7 >>> C Set-Cookie: lobotomy="joe=wolf; lobotomy=\"nested quote\"; mark=\012;"; Set-Cookie: joe="a cookie"; Path=/home/joe; Set-Cookie: mary=hadalittlelamb; Set-Cookie: anotherint="I45\012."; Set-Cookie: int="I7\012."; Finally, the encoding/decoding behavior is controllable by two attributes of the Cookie: net_setfunc() Takes in an encoded string and returns a value user_setfunc() Takes in a value and returns the encoded string By default, these functions are defined in the Cookie module, but you should feel free to override them. >>> C.net_setfunc >>> C.user_setfunc Finis. """ # # Import our required modules # import string, sys from UserDict import UserDict try: from cPickle import dumps, loads except ImportError: from pickle import dumps, loads try: import re except ImportError: raise ImportError, "Cookie.py requires 're' from Python 1.5 or later" # # Define an exception visible to External modules # class CookieError(Exception): pass # These quoting routines conform to the RFC2109 specification, which in # turn references the character definitions from RFC2068. They provide # a two-way quoting algorithm. Any non-text character is translated # into a 4 character sequence: a forward-slash followed by the # three-digit octal equivalent of the character. Any '\' or '"' is # quoted with a preceeding '\' slash. # # These are taken from RFC2068 and RFC2109. # _LegalChars is the list of chars which don't require "'s # _SpecialChars require the cookie to be double-quoted # _Translator hash-table for fast quoting # _LegalChars = string.letters + string.digits + "!#$%&'*+-.^_`|~" _SpecialChars = string.translate(string._idmap, string._idmap, _LegalChars) _Translator = { '\000' : '\\000', '\001' : '\\001', '\002' : '\\002', '\003' : '\\003', '\004' : '\\004', '\005' : '\\005', '\006' : '\\006', '\007' : '\\007', '\010' : '\\010', '\011' : '\\011', '\012' : '\\012', '\013' : '\\013', '\014' : '\\014', '\015' : '\\015', '\016' : '\\016', '\017' : '\\017', '\020' : '\\020', '\021' : '\\021', '\022' : '\\022', '\023' : '\\023', '\024' : '\\024', '\025' : '\\025', '\026' : '\\026', '\027' : '\\027', '\030' : '\\030', '\031' : '\\031', '\032' : '\\032', '\033' : '\\033', '\034' : '\\034', '\035' : '\\035', '\036' : '\\036', '\037' : '\\037', ' ' : ' ', '!' : '!', '"' : '\\"', '#' : '#', '$' : '$', '%' : '%', '&' : '&', "'" : "'", '(' : '(', ')' : ')', '*' : '*', '+' : '+', ',' : ',', '-' : '-', '.' : '.', '/' : '/', '0' : '0', '1' : '1', '2' : '2', '3' : '3', '4' : '4', '5' : '5', '6' : '6', '7' : '7', '8' : '8', '9' : '9', ':' : ':', ';' : ';', '<' : '<', ':' : ':', '>' : '>', '?' : '?', '=':'=', '@' : '@', 'A' : 'A', 'B' : 'B', 'C' : 'C', 'D' : 'D', 'E' : 'E', 'F' : 'F', 'G' : 'G', 'H' : 'H', 'I' : 'I', 'J' : 'J', 'K' : 'K', 'L' : 'L', 'M' : 'M', 'N' : 'N', 'O' : 'O', 'P' : 'P', 'Q' : 'Q', 'R' : 'R', 'S' : 'S', 'T' : 'T', 'U' : 'U', 'V' : 'V', 'W' : 'W', 'X' : 'X', 'Y' : 'Y', 'Z' : 'Z', '[' : '[', '\\' : '\\\\', ']' : ']', '^' : '^', '_' : '_', '`' : '`', 'a' : 'a', 'b' : 'b', 'c' : 'c', 'd' : 'd', 'e' : 'e', 'f' : 'f', 'g' : 'g', 'h' : 'h', 'i' : 'i', 'j' : 'j', 'k' : 'k', 'l' : 'l', 'm' : 'm', 'n' : 'n', 'o' : 'o', 'p' : 'p', 'q' : 'q', 'r' : 'r', 's' : 's', 't' : 't', 'u' : 'u', 'v' : 'v', 'w' : 'w', 'x' : 'x', 'y' : 'y', 'z' : 'z', '{' : '{', '|' : '|', '}' : '}', '~' : '~', '\177' : '\\177', '\200' : '\\200', '\201' : '\\201', '\202' : '\\202', '\203' : '\\203', '\204' : '\\204', '\205' : '\\205', '\206' : '\\206', '\207' : '\\207', '\210' : '\\210', '\211' : '\\211', '\212' : '\\212', '\213' : '\\213', '\214' : '\\214', '\215' : '\\215', '\216' : '\\216', '\217' : '\\217', '\220' : '\\220', '\221' : '\\221', '\222' : '\\222', '\223' : '\\223', '\224' : '\\224', '\225' : '\\225', '\226' : '\\226', '\227' : '\\227', '\230' : '\\230', '\231' : '\\231', '\232' : '\\232', '\233' : '\\233', '\234' : '\\234', '\235' : '\\235', '\236' : '\\236', '\237' : '\\237', '\240' : '\\240', '\241' : '\\241', '\242' : '\\242', '\243' : '\\243', '\244' : '\\244', '\245' : '\\245', '\246' : '\\246', '\247' : '\\247', '\250' : '\\250', '\251' : '\\251', '\252' : '\\252', '\253' : '\\253', '\254' : '\\254', '\255' : '\\255', '\256' : '\\256', '\257' : '\\257', '\260' : '\\260', '\261' : '\\261', '\262' : '\\262', '\263' : '\\263', '\264' : '\\264', '\265' : '\\265', '\266' : '\\266', '\267' : '\\267', '\270' : '\\270', '\271' : '\\271', '\272' : '\\272', '\273' : '\\273', '\274' : '\\274', '\275' : '\\275', '\276' : '\\276', '\277' : '\\277', '\300' : '\\300', '\301' : '\\301', '\302' : '\\302', '\303' : '\\303', '\304' : '\\304', '\305' : '\\305', '\306' : '\\306', '\307' : '\\307', '\310' : '\\310', '\311' : '\\311', '\312' : '\\312', '\313' : '\\313', '\314' : '\\314', '\315' : '\\315', '\316' : '\\316', '\317' : '\\317', '\320' : '\\320', '\321' : '\\321', '\322' : '\\322', '\323' : '\\323', '\324' : '\\324', '\325' : '\\325', '\326' : '\\326', '\327' : '\\327', '\330' : '\\330', '\331' : '\\331', '\332' : '\\332', '\333' : '\\333', '\334' : '\\334', '\335' : '\\335', '\336' : '\\336', '\337' : '\\337', '\340' : '\\340', '\341' : '\\341', '\342' : '\\342', '\343' : '\\343', '\344' : '\\344', '\345' : '\\345', '\346' : '\\346', '\347' : '\\347', '\350' : '\\350', '\351' : '\\351', '\352' : '\\352', '\353' : '\\353', '\354' : '\\354', '\355' : '\\355', '\356' : '\\356', '\357' : '\\357', '\360' : '\\360', '\361' : '\\361', '\362' : '\\362', '\363' : '\\363', '\364' : '\\364', '\365' : '\\365', '\366' : '\\366', '\367' : '\\367', '\370' : '\\370', '\371' : '\\371', '\372' : '\\372', '\373' : '\\373', '\374' : '\\374', '\375' : '\\375', '\376' : '\\376', '\377' : '\\377' } def _translate(c, table=_Translator): return table[c] def _quote(str, join=string.join): # First check for common (and simple) case. # for C in _SpecialChars: if C in str: break else: return str # Ok, down to work. # It's a shame we can't use _Translator.__getitem__ # but Python code doesn't have access to that function. # return '"' + join( map(_translate, str), "") + '"' # end _quote _OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") _QuotePatt = re.compile(r"[\\].") def _unquote(str): # If there aren't any doublequotes, # then there can't be any special characters. See RFC 2109. if len(str) < 2: return str if str[0] != '"' or str[-1] != '"': return str # We have to assume that we must decode this string. # Down to work. # Remove the "s str = str[1:-1] # Check for special sequences. Examples: # \012 --> \n # \" --> " # i = 0 n = len(str) res = [] while 0 <= i < n: Omatch = _OctalPatt.search(str, i) Qmatch = _QuotePatt.search(str, i) if not Omatch and not Qmatch: # Neither matched res.append(str[i:]) break # else: j = k = -1 if Omatch: j = Omatch.start(0) if Qmatch: k = Qmatch.start(0) if Qmatch and ( not Omatch or k < j ): # QuotePatt matched res.append(str[i:k]) res.append(str[k+1]) i = k+2 else: # OctalPatt matched res.append(str[i:j]) res.append( chr( string.atoi(str[j+1:j+4], 8) ) ) i = j+4 return string.join(res, "") # end _unquote # The _babelize() and _debabelize() functions allow arbitrary objects # to be used as cookie values. Large cookies may add significant # overhead, because the client retransmits them on each visit. # # Note: HTTP imposes a 2k limit on the size of cookie. I don't check # for this limit, so be careful!!! # def _babelize(val, dumps=dumps): if type(val) == type(""): return _quote(val) else: return _quote( dumps(val) ) def _debabelize(val, loads=loads): str = _unquote(val) try: return loads(str) except: return str # The _getdate() routine is used to set the expiration time in # the cookie's HTTP header. By default, _getdate() returns the # current time in the appropriate "expires" format for a # Set-Cookie header. The one optional argument is an offset from # now, in seconds. For example, an offset of -3600 means "one hour ago". # The offset may be a floating point number. # _weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] _monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): from time import gmtime, time now = time() year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future) return "%s, %02d-%3s-%4d %02d:%02d:%02d GMT" % \ (weekdayname[wd], day, monthname[month], year, hh, mm, ss) # # A class to hold ONE key,value pair. # In a cookie, each such pair may have several attributes. # so this class is used to keep the attributes associated # with the appropriate key,value pair. # This class also includes a coded_value attribute, which # is used to hold the network representation of the # value. This is most useful when Python objects are # pickled for network transit. # class Morsel(UserDict): # RFC 2109 lists these attributes as reserved: # path comment domain # max-age secure version # # For historical reasons, these attributes are also reserved: # expires # # This dictionary provides a mapping from the lowercase # variant on the left to the appropriate Set-Cookie # format on the right. __reserved = { "expires" : "expires", "path" : "Path", "comment" : "Comment", "domain" : "Domain", "max-age" : "Max-Age", "secure" : "secure", "version" : "Version", } __reserved_keys = __reserved.keys() def __init__(self): # Set defaults self.key = self.value = self.coded_value = None UserDict.__init__(self) # Set default attributes for K in self.__reserved_keys: UserDict.__setitem__(self, K, "") # end __init__ def __setitem__(self, K, V): K = string.lower(K) if not K in self.__reserved_keys: raise CookieError("Invalid Attribute %s" % K) UserDict.__setitem__(self, K, V) # end __setitem__ def set(self, key, val, coded_val): if string.lower(key) in self.__reserved_keys: raise CookieError("Attempt to set a reserved key: %s" % key) self.key = key self.value = val self.coded_value = coded_val # end set def output(self, header = "Set-Cookie:"): return "%s %s" % ( header, self.OutputString() ) __repr__ = output def js_output(self): # Print javascript return """ """ % ( self.OutputString(), ) # end js_output() def OutputString(self): # Build up our result # result = [] RA = result.append # First, the key=value pair RA("%s=%s;" % (self.key, self.coded_value)) # Now add any defined attributes for K,V in self.items(): if V == '': continue if K == "expires" and type(V) == type(1): RA("%s=%s;" % (self.__reserved[K], _getdate(V))) elif K == "max-age" and type(V) == type(1): RA("%s=%d;" % (self.__reserved[K], V)) elif K == "secure": RA("%s;" % self.__reserved[K]) else: RA("%s=%s;" % (self.__reserved[K], V)) # Return the result return string.join(result, " ") # end OutputString # end Morsel class # # Pattern for finding cookie # # This used to be strict parsing based on the RFC2109 and RFC2068 # specifications. I have since discovered that MSIE 3.0x doesn't # follow the character rules outlined in those specs. As a # result, the parsing rules here are less strict. # _LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{]+" _CookiePattern = re.compile( r"(?x)" # This is a Verbose pattern r"(?P" # Start of group 'key' ""+ _LegalCharsPatt +"" # Any word r")" # End of group 'key' r"\s*=\s*" # Equal Sign r"(?P" # Start of group 'val' r'"(?:[^\\"]|\\.)*"' # Any doublequoted string r"|" # or ""+ _LegalCharsPatt +"" # Any word r")" # End of group 'val' r"\s*;?" # Probably ending in a semi-colon ) # At long last, here is the cookie class. # Using this class is almost just like using a dictionary. # See this module's docstring for example usage. # class Cookie(UserDict): # A container class for a set of Morsels # def __init__(self, input=None, net_setfunc=_debabelize, user_setfunc=_babelize): self.net_setfunc = net_setfunc # when set from network self.user_setfunc = user_setfunc # when set by user UserDict.__init__(self) if input: self.load(input) # end __init__ def __setitem__(self, key, value): """Dictionary style assignment.""" M = self.get(key, Morsel()) M.set(key, value, apply(self.user_setfunc, (value,))) UserDict.__setitem__(self, key, M) # end __setitem__ def output(self, header="Set-Cookie:"): """Return a string suitable for HTTP.""" result = [] for K,V in self.items(): result.append( V.output(header) ) return string.join(result,"\n") # end output __repr__ = output def js_output(self): """Return a string suitable for JavaScript.""" result = [] for K,V in self.items(): result.append( V.js_output() ) return string.join(result, "") # end js_output def load(self, rawdata): """Load cookies from a string (presumably HTTP_COOKIE) or from a dictionary. Loading cookies from a dictionary 'd' is equivalent to calling: map(Cookie.__setitem__, d.keys(), d.values()) Unfortunately, this does NOT allow merging of two Cookie dictionaries! """ if type(rawdata) == type(""): self.__ParseString(rawdata) else: for K,V in rawdata.items(): self[K] = V return # end get() def __ParseString(self, str, patt=_CookiePattern): i = 0 # Our starting point n = len(str) # Length of string M = None # Current morsel while 0 <= i < n: # Start looking for a cookie match = patt.search(str, i) if not match: break # No more cookies K,V = match.group("key"), match.group("val") i = match.end(0) # Parse the key, value in case it's metainfo if K[0] == "$" and M: M[string.lower(K[1:])] = V # We ignore attributes which pertain to the cookie # mechanism as a whole. See RFC 2109. # (Does anyone care?) else: M = Morsel() M.set(K, apply(self.net_setfunc, (V,)), V) UserDict.__setitem__(self, K, M) return # end __ParseString # end Cookie class #Local Variables: #tab-width: 4 #End: