summaryrefslogtreecommitdiff
path: root/misc/Cookie.py
diff options
context:
space:
mode:
Diffstat (limited to 'misc/Cookie.py')
-rw-r--r--misc/Cookie.py553
1 files changed, 553 insertions, 0 deletions
diff --git a/misc/Cookie.py b/misc/Cookie.py
new file mode 100644
index 000000000..d6e74cb6c
--- /dev/null
+++ b/misc/Cookie.py
@@ -0,0 +1,553 @@
+#!/usr/bin/env python
+
+"""
+# Id: Cookie.py,v 2.4 1998/02/13 16:42:30 timo Exp
+# by Timothy O'Malley <timo@bbn.com> Date: 1998/02/13 16:42:30
+#
+# 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
+ <function _debabelize at c1558>
+ >>> C.user_setfunc
+ <function _babelize at c1530>
+
+Finis.
+"""
+
+__version__ = "$Revision: 424 $"
+
+#
+# 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 """
+ <SCRIPT LANGUAGE="JavaScript">
+ <!-- begin hiding
+ document.cookie = \"%s\"
+ // end hiding -->
+ </script>
+ """ % ( 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 not 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<key>" # Start of group 'key'
+ ""+ _LegalCharsPatt +"" # Any word
+ r")" # End of group 'key'
+ r"\s*=\s*" # Equal Sign
+ r"(?P<val>" # 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