home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2004 December / PCpro_2004_12.ISO / files / webserver / tsw / TSW_3.4.0.exe / Apache2 / python / Cookie.py < prev    next >
Encoding:
Python Source  |  2004-02-16  |  11.4 KB  |  371 lines

  1.  #
  2.  # Copyright 2004 Apache Software Foundation 
  3.  # 
  4.  # Licensed under the Apache License, Version 2.0 (the "License"); you
  5.  # may not use this file except in compliance with the License.  You
  6.  # may obtain a copy of the License at
  7.  #
  8.  #      http://www.apache.org/licenses/LICENSE-2.0
  9.  #
  10.  # Unless required by applicable law or agreed to in writing, software
  11.  # distributed under the License is distributed on an "AS IS" BASIS,
  12.  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  13.  # implied.  See the License for the specific language governing
  14.  # permissions and limitations under the License.
  15.  #
  16.  # Originally developed by Gregory Trubetskoy.
  17.  #
  18.  # $Id: Cookie.py,v 1.11 2004/02/16 19:47:27 grisha Exp $
  19.  
  20. """
  21.  
  22. This module contains classes to support HTTP State Management
  23. Mechanism, also known as Cookies. The classes provide simple
  24. ways for creating, parsing and digitally signing cookies, as
  25. well as the ability to store simple Python objects in Cookies
  26. (using marshalling).
  27.  
  28. The behaviour of the classes is designed to be most useful
  29. within mod_python applications.
  30.  
  31. The current state of HTTP State Management standardization is
  32. rather unclear. It appears that the de-facto standard is the
  33. original Netscape specification, even though already two RFC's
  34. have been put out (RFC2109 (1997) and RFC2965 (2000)). The
  35. RFC's add a couple of useful features (e.g. using Max-Age instead
  36. of Expires, but my limited tests show that Max-Age is ignored
  37. by the two browsers tested (IE and Safari). As a result of this,
  38. perhaps trying to be RFC-compliant (by automatically providing
  39. Max-Age and Version) could be a waste of cookie space...
  40.  
  41. """
  42.  
  43. import time
  44. import re
  45. import hmac
  46. import marshal
  47. import base64
  48.  
  49. import apache
  50.  
  51. class CookieError(Exception):
  52.     pass
  53.  
  54. class metaCookie(type):
  55.  
  56.     def __new__(cls, clsname, bases, clsdict):
  57.  
  58.         _valid_attr = (
  59.             "version", "path", "domain", "secure",
  60.             "comment", "expires", "max_age",
  61.             # RFC 2965
  62.             "commentURL", "discard", "port")
  63.  
  64.         # _valid_attr + property values
  65.         # (note __slots__ is a new Python feature, it
  66.         # prevents any other attribute from being set)
  67.         __slots__ = _valid_attr + ("name", "value", "_value",
  68.                                    "_expires", "__data__")
  69.  
  70.         clsdict["_valid_attr"] = _valid_attr
  71.         clsdict["__slots__"] = __slots__
  72.  
  73.         def set_expires(self, value):
  74.  
  75.             if type(value) == type(""):
  76.                 # if it's a string, it should be
  77.                 # valid format as per Netscape spec
  78.                 try:
  79.                     t = time.strptime(value, "%a, %d-%b-%Y %H:%M:%S GMT")
  80.                 except ValueError:
  81.                     raise ValueError, "Invalid expires time: %s" % value
  82.                 t = time.mktime(t)
  83.             else:
  84.                 # otherwise assume it's a number
  85.                 # representing time as from time.time()
  86.                 t = value
  87.                 value = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT",
  88.                                       time.gmtime(t))
  89.  
  90.             self._expires = "%s" % value
  91.  
  92.         def get_expires(self):
  93.             return self._expires
  94.  
  95.         clsdict["expires"] = property(fget=get_expires, fset=set_expires)
  96.  
  97.         return type.__new__(cls, clsname, bases, clsdict)
  98.  
  99. class Cookie(object):
  100.     """
  101.     This class implements the basic Cookie functionality. Note that
  102.     unlike the Python Standard Library Cookie class, this class represents
  103.     a single cookie (not a list of Morsels).
  104.     """
  105.  
  106.     __metaclass__ = metaCookie
  107.  
  108.     def parse(Class, str):
  109.         """
  110.         Parse a Cookie or Set-Cookie header value, and return
  111.         a dict of Cookies. Note: the string should NOT include the
  112.         header name, only the value.
  113.         """
  114.  
  115.         dict = _parse_cookie(str, Class)
  116.         return dict
  117.  
  118.     parse = classmethod(parse)
  119.  
  120.     def __init__(self, name, value, **kw):
  121.  
  122.         """
  123.         This constructor takes at least a name and value as the
  124.         arguments, as well as optionally any of allowed cookie attributes
  125.         as defined in the existing cookie standards. 
  126.         """
  127.         self.name, self.value = name, value
  128.  
  129.         for k in kw:
  130.             setattr(self, k.lower(), kw[k])
  131.  
  132.         # subclasses can use this for internal stuff
  133.         self.__data__ = {}
  134.  
  135.  
  136.     def __str__(self):
  137.  
  138.         """
  139.         Provides the string representation of the Cookie suitable for
  140.         sending to the browser. Note that the actual header name will
  141.         not be part of the string.
  142.  
  143.         This method makes no attempt to automatically double-quote
  144.         strings that contain special characters, even though the RFC's
  145.         dictate this. This is because doing so seems to confuse most
  146.         browsers out there.
  147.         """
  148.         
  149.         result = ["%s=%s" % (self.name, self.value)]
  150.         for name in self._valid_attr:
  151.             if hasattr(self, name):
  152.                 if name in ("secure", "discard"):
  153.                     result.append(name)
  154.                 else:
  155.                     result.append("%s=%s" % (name, getattr(self, name)))
  156.         return "; ".join(result)
  157.     
  158.     def __repr__(self):
  159.         return '<%s: %s>' % (self.__class__.__name__,
  160.                                 str(self))
  161.     
  162.  
  163. class SignedCookie(Cookie):
  164.     """
  165.     This is a variation of Cookie that provides automatic
  166.     cryptographic signing of cookies and verification. It uses
  167.     the HMAC support in the Python standard library. This ensures
  168.     that the cookie has not been tamprered with on the client side.
  169.  
  170.     Note that this class does not encrypt cookie data, thus it
  171.     is still plainly visible as part of the cookie.
  172.     """
  173.  
  174.     def parse(Class, s, secret):
  175.  
  176.         dict = _parse_cookie(s, Class)
  177.  
  178.         for k in dict:
  179.             c = dict[k]
  180.             try:
  181.                 c.unsign(secret)
  182.             except CookieError:
  183.                 # downgrade to Cookie
  184.                 dict[k] = Cookie.parse(Cookie.__str__(c))[k]
  185.         
  186.         return dict
  187.  
  188.     parse = classmethod(parse)
  189.  
  190.     def __init__(self, name, value, secret=None, **kw):
  191.         Cookie.__init__(self, name, value, **kw)
  192.  
  193.         self.__data__["secret"] = secret
  194.  
  195.     def hexdigest(self, str):
  196.         if not self.__data__["secret"]:
  197.             raise CookieError, "Cannot sign without a secret"
  198.         _hmac = hmac.new(self.__data__["secret"], self.name)
  199.         _hmac.update(str)
  200.         return _hmac.hexdigest()
  201.  
  202.     def __str__(self):
  203.         
  204.         result = ["%s=%s%s" % (self.name, self.hexdigest(self.value),
  205.                                self.value)]
  206.         for name in self._valid_attr:
  207.             if hasattr(self, name):
  208.                 if name in ("secure", "discard"):
  209.                     result.append(name)
  210.                 else:
  211.                     result.append("%s=%s" % (name, getattr(self, name)))
  212.         return "; ".join(result)
  213.  
  214.     def unsign(self, secret):
  215.  
  216.         sig, val = self.value[:32], self.value[32:]
  217.  
  218.         mac = hmac.new(secret, self.name)
  219.         mac.update(val)
  220.  
  221.         if mac.hexdigest() == sig:
  222.             self.value = val
  223.             self.__data__["secret"] = secret
  224.         else:
  225.             raise CookieError, "Incorrectly Signed Cookie: %s=%s" % (self.name, self.value)
  226.  
  227.  
  228. class MarshalCookie(SignedCookie):
  229.  
  230.     """
  231.     This is a variation of SignedCookie that can store more than
  232.     just strings. It will automatically marshal the cookie value,
  233.     therefore any marshallable object can be used as value.
  234.  
  235.     The standard library Cookie module provides the ability to pickle
  236.     data, which is a major security problem. It is believed that unmarshalling
  237.     (as opposed to unpickling) is safe, yet we still err on the side of caution
  238.     which is why this class is a subclass of SignedCooke making sure what
  239.     we are about to unmarshal passes the digital signature test.
  240.  
  241.     Here is a link to a sugesstion that marshalling is safer than unpickling
  242.     http://groups.google.com/groups?hl=en&lr=&ie=UTF-8&selm=7xn0hcugmy.fsf%40ruckus.brouhaha.com
  243.     """
  244.  
  245.     def parse(Class, s, secret):
  246.  
  247.         dict = _parse_cookie(s, Class)
  248.  
  249.         for k in dict:
  250.             c = dict[k]
  251.             try:
  252.                 c.unmarshal(secret)
  253.             except (CookieError, ValueError):
  254.                 # downgrade to Cookie
  255.                 dict[k] = Cookie.parse(Cookie.__str__(c))[k]
  256.  
  257.         return dict
  258.  
  259.     parse = classmethod(parse)
  260.  
  261.     def __str__(self):
  262.         
  263.         m = base64.encodestring(marshal.dumps(self.value))[:-1]
  264.  
  265.         result = ["%s=%s%s" % (self.name, self.hexdigest(m), m)]
  266.         for name in self._valid_attr:
  267.             if hasattr(self, name):
  268.                 if name in ("secure", "discard"):
  269.                     result.append(name)
  270.                 else:
  271.                     result.append("%s=%s" % (name, getattr(self, name)))
  272.         return "; ".join(result)
  273.  
  274.     def unmarshal(self, secret):
  275.  
  276.         self.unsign(secret)
  277.         self.value = marshal.loads(base64.decodestring(self.value))
  278.  
  279.  
  280.  
  281. # This is a simplified and in some places corrected
  282. # (at least I think it is) pattern from standard lib Cookie.py
  283.  
  284. _cookiePattern = re.compile(
  285.     r"(?x)"                       # Verbose pattern
  286.     r"[,\ ]*"                        # space/comma (RFC2616 4.2) before attr-val is eaten
  287.     r"(?P<key>"                   # Start of group 'key'
  288.     r"[^;\ =]+"                     # anything but ';', ' ' or '='
  289.     r")"                          # End of group 'key'
  290.     r"\ *(=\ *)?"                 # a space, then may be "=", more space
  291.     r"(?P<val>"                   # Start of group 'val'
  292.     r'"(?:[^\\"]|\\.)*"'            # a doublequoted string
  293.     r"|"                            # or
  294.     r"[^;]*"                        # any word or empty string
  295.     r")"                          # End of group 'val'
  296.     r"\s*;?"                      # probably ending in a semi-colon
  297.     )
  298.  
  299. def _parse_cookie(str, Class):
  300.  
  301.     # XXX problem is we should allow duplicate
  302.     # strings
  303.     result = {}
  304.  
  305.     # max-age is a problem because of the '-'
  306.     # XXX there should be a more elegant way
  307.     valid = Cookie._valid_attr + ("max-age",)
  308.  
  309.     c = None
  310.     matchIter = _cookiePattern.finditer(str)
  311.  
  312.     for match in matchIter:
  313.  
  314.         key, val = match.group("key"), match.group("val")
  315.  
  316.         if not c:
  317.             # new cookie
  318.             c = Class(key, val)
  319.             result[key] = c
  320.  
  321.         l_key = key.lower()
  322.         
  323.         if (l_key in valid or key[0] == '$'):
  324.             
  325.             # "internal" attribute, add to cookie
  326.  
  327.             if l_key == "max-age":
  328.                 l_key = "max_age"
  329.             setattr(c, l_key, val)
  330.  
  331.         else:
  332.             # start a new cookie
  333.             c = Class(l_key, val)
  334.             result[l_key] = c
  335.  
  336.     return result
  337.  
  338. def add_cookie(req, cookie, value="", **kw):
  339.     """
  340.     Sets a cookie in outgoing headers and adds a cache
  341.     directive so that caches don't cache the cookie.
  342.     """
  343.  
  344.     # is this a cookie?
  345.     if not isinstance(cookie, Cookie):
  346.  
  347.         # make a cookie
  348.         cookie = Cookie(cookie, value, **kw)
  349.         
  350.     if not req.headers_out.has_key("Set-Cookie"):
  351.         req.headers_out.add("Cache-Control", 'no-cache="set-cookie"')
  352.  
  353.     req.headers_out.add("Set-Cookie", str(cookie))
  354.  
  355. def get_cookies(req, Class=Cookie, **kw):
  356.     """
  357.     A shorthand for retrieveing and parsing cookies given
  358.     a Cookie class. The class must be one of the classes from
  359.     this module.
  360.     """
  361.     
  362.     if not req.headers_in.has_key("cookie"):
  363.         return {}
  364.  
  365.     cookies = req.headers_in["cookie"]
  366.     if type(cookies) == type([]):
  367.         cookies = '; '.join(cookies)
  368.  
  369.     return Class.parse(cookies, **kw)
  370.  
  371.