Home | History | Annotate | Download | only in http
      1 ####
      2 # Copyright 2000 by Timothy O'Malley <timo (at] alum.mit.edu>
      3 #
      4 #                All Rights Reserved
      5 #
      6 # Permission to use, copy, modify, and distribute this software
      7 # and its documentation for any purpose and without fee is hereby
      8 # granted, provided that the above copyright notice appear in all
      9 # copies and that both that copyright notice and this permission
     10 # notice appear in supporting documentation, and that the name of
     11 # Timothy O'Malley  not be used in advertising or publicity
     12 # pertaining to distribution of the software without specific, written
     13 # prior permission.
     14 #
     15 # Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
     16 # SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
     17 # AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR
     18 # ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     19 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
     20 # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
     21 # ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
     22 # PERFORMANCE OF THIS SOFTWARE.
     23 #
     24 ####
     25 #
     26 # Id: Cookie.py,v 2.29 2000/08/23 05:28:49 timo Exp
     27 #   by Timothy O'Malley <timo (at] alum.mit.edu>
     28 #
     29 #  Cookie.py is a Python module for the handling of HTTP
     30 #  cookies as a Python dictionary.  See RFC 2109 for more
     31 #  information on cookies.
     32 #
     33 #  The original idea to treat Cookies as a dictionary came from
     34 #  Dave Mitchell (davem (at] magnet.com) in 1995, when he released the
     35 #  first version of nscookie.py.
     36 #
     37 ####
     38 
     39 r"""
     40 Here's a sample session to show how to use this module.
     41 At the moment, this is the only documentation.
     42 
     43 The Basics
     44 ----------
     45 
     46 Importing is easy...
     47 
     48    >>> from http import cookies
     49 
     50 Most of the time you start by creating a cookie.
     51 
     52    >>> C = cookies.SimpleCookie()
     53 
     54 Once you've created your Cookie, you can add values just as if it were
     55 a dictionary.
     56 
     57    >>> C = cookies.SimpleCookie()
     58    >>> C["fig"] = "newton"
     59    >>> C["sugar"] = "wafer"
     60    >>> C.output()
     61    'Set-Cookie: fig=newton\r\nSet-Cookie: sugar=wafer'
     62 
     63 Notice that the printable representation of a Cookie is the
     64 appropriate format for a Set-Cookie: header.  This is the
     65 default behavior.  You can change the header and printed
     66 attributes by using the .output() function
     67 
     68    >>> C = cookies.SimpleCookie()
     69    >>> C["rocky"] = "road"
     70    >>> C["rocky"]["path"] = "/cookie"
     71    >>> print(C.output(header="Cookie:"))
     72    Cookie: rocky=road; Path=/cookie
     73    >>> print(C.output(attrs=[], header="Cookie:"))
     74    Cookie: rocky=road
     75 
     76 The load() method of a Cookie extracts cookies from a string.  In a
     77 CGI script, you would use this method to extract the cookies from the
     78 HTTP_COOKIE environment variable.
     79 
     80    >>> C = cookies.SimpleCookie()
     81    >>> C.load("chips=ahoy; vienna=finger")
     82    >>> C.output()
     83    'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger'
     84 
     85 The load() method is darn-tootin smart about identifying cookies
     86 within a string.  Escaped quotation marks, nested semicolons, and other
     87 such trickeries do not confuse it.
     88 
     89    >>> C = cookies.SimpleCookie()
     90    >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
     91    >>> print(C)
     92    Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
     93 
     94 Each element of the Cookie also supports all of the RFC 2109
     95 Cookie attributes.  Here's an example which sets the Path
     96 attribute.
     97 
     98    >>> C = cookies.SimpleCookie()
     99    >>> C["oreo"] = "doublestuff"
    100    >>> C["oreo"]["path"] = "/"
    101    >>> print(C)
    102    Set-Cookie: oreo=doublestuff; Path=/
    103 
    104 Each dictionary element has a 'value' attribute, which gives you
    105 back the value associated with the key.
    106 
    107    >>> C = cookies.SimpleCookie()
    108    >>> C["twix"] = "none for you"
    109    >>> C["twix"].value
    110    'none for you'
    111 
    112 The SimpleCookie expects that all values should be standard strings.
    113 Just to be sure, SimpleCookie invokes the str() builtin to convert
    114 the value to a string, when the values are set dictionary-style.
    115 
    116    >>> C = cookies.SimpleCookie()
    117    >>> C["number"] = 7
    118    >>> C["string"] = "seven"
    119    >>> C["number"].value
    120    '7'
    121    >>> C["string"].value
    122    'seven'
    123    >>> C.output()
    124    'Set-Cookie: number=7\r\nSet-Cookie: string=seven'
    125 
    126 Finis.
    127 """
    128 
    129 #
    130 # Import our required modules
    131 #
    132 import re
    133 import string
    134 
    135 __all__ = ["CookieError", "BaseCookie", "SimpleCookie"]
    136 
    137 _nulljoin = ''.join
    138 _semispacejoin = '; '.join
    139 _spacejoin = ' '.join
    140 
    141 def _warn_deprecated_setter(setter):
    142     import warnings
    143     msg = ('The .%s setter is deprecated. The attribute will be read-only in '
    144            'future releases. Please use the set() method instead.' % setter)
    145     warnings.warn(msg, DeprecationWarning, stacklevel=3)
    146 
    147 #
    148 # Define an exception visible to External modules
    149 #
    150 class CookieError(Exception):
    151     pass
    152 
    153 
    154 # These quoting routines conform to the RFC2109 specification, which in
    155 # turn references the character definitions from RFC2068.  They provide
    156 # a two-way quoting algorithm.  Any non-text character is translated
    157 # into a 4 character sequence: a forward-slash followed by the
    158 # three-digit octal equivalent of the character.  Any '\' or '"' is
    159 # quoted with a preceding '\' slash.
    160 # Because of the way browsers really handle cookies (as opposed to what
    161 # the RFC says) we also encode "," and ";".
    162 #
    163 # These are taken from RFC2068 and RFC2109.
    164 #       _LegalChars       is the list of chars which don't require "'s
    165 #       _Translator       hash-table for fast quoting
    166 #
    167 _LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
    168 _UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
    169 
    170 _Translator = {n: '\\%03o' % n
    171                for n in set(range(256)) - set(map(ord, _UnescapedChars))}
    172 _Translator.update({
    173     ord('"'): '\\"',
    174     ord('\\'): '\\\\',
    175 })
    176 
    177 _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
    178 
    179 def _quote(str):
    180     r"""Quote a string for use in a cookie header.
    181 
    182     If the string does not need to be double-quoted, then just return the
    183     string.  Otherwise, surround the string in doublequotes and quote
    184     (with a \) special characters.
    185     """
    186     if str is None or _is_legal_key(str):
    187         return str
    188     else:
    189         return '"' + str.translate(_Translator) + '"'
    190 
    191 
    192 _OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
    193 _QuotePatt = re.compile(r"[\\].")
    194 
    195 def _unquote(str):
    196     # If there aren't any doublequotes,
    197     # then there can't be any special characters.  See RFC 2109.
    198     if str is None or len(str) < 2:
    199         return str
    200     if str[0] != '"' or str[-1] != '"':
    201         return str
    202 
    203     # We have to assume that we must decode this string.
    204     # Down to work.
    205 
    206     # Remove the "s
    207     str = str[1:-1]
    208 
    209     # Check for special sequences.  Examples:
    210     #    \012 --> \n
    211     #    \"   --> "
    212     #
    213     i = 0
    214     n = len(str)
    215     res = []
    216     while 0 <= i < n:
    217         o_match = _OctalPatt.search(str, i)
    218         q_match = _QuotePatt.search(str, i)
    219         if not o_match and not q_match:              # Neither matched
    220             res.append(str[i:])
    221             break
    222         # else:
    223         j = k = -1
    224         if o_match:
    225             j = o_match.start(0)
    226         if q_match:
    227             k = q_match.start(0)
    228         if q_match and (not o_match or k < j):     # QuotePatt matched
    229             res.append(str[i:k])
    230             res.append(str[k+1])
    231             i = k + 2
    232         else:                                      # OctalPatt matched
    233             res.append(str[i:j])
    234             res.append(chr(int(str[j+1:j+4], 8)))
    235             i = j + 4
    236     return _nulljoin(res)
    237 
    238 # The _getdate() routine is used to set the expiration time in the cookie's HTTP
    239 # header.  By default, _getdate() returns the current time in the appropriate
    240 # "expires" format for a Set-Cookie header.  The one optional argument is an
    241 # offset from now, in seconds.  For example, an offset of -3600 means "one hour
    242 # ago".  The offset may be a floating point number.
    243 #
    244 
    245 _weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    246 
    247 _monthname = [None,
    248               'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
    249               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
    250 
    251 def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname):
    252     from time import gmtime, time
    253     now = time()
    254     year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future)
    255     return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \
    256            (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
    257 
    258 
    259 class Morsel(dict):
    260     """A class to hold ONE (key, value) pair.
    261 
    262     In a cookie, each such pair may have several attributes, so this class is
    263     used to keep the attributes associated with the appropriate key,value pair.
    264     This class also includes a coded_value attribute, which is used to hold
    265     the network representation of the value.  This is most useful when Python
    266     objects are pickled for network transit.
    267     """
    268     # RFC 2109 lists these attributes as reserved:
    269     #   path       comment         domain
    270     #   max-age    secure      version
    271     #
    272     # For historical reasons, these attributes are also reserved:
    273     #   expires
    274     #
    275     # This is an extension from Microsoft:
    276     #   httponly
    277     #
    278     # This dictionary provides a mapping from the lowercase
    279     # variant on the left to the appropriate traditional
    280     # formatting on the right.
    281     _reserved = {
    282         "expires"  : "expires",
    283         "path"     : "Path",
    284         "comment"  : "Comment",
    285         "domain"   : "Domain",
    286         "max-age"  : "Max-Age",
    287         "secure"   : "Secure",
    288         "httponly" : "HttpOnly",
    289         "version"  : "Version",
    290     }
    291 
    292     _flags = {'secure', 'httponly'}
    293 
    294     def __init__(self):
    295         # Set defaults
    296         self._key = self._value = self._coded_value = None
    297 
    298         # Set default attributes
    299         for key in self._reserved:
    300             dict.__setitem__(self, key, "")
    301 
    302     @property
    303     def key(self):
    304         return self._key
    305 
    306     @key.setter
    307     def key(self, key):
    308         _warn_deprecated_setter('key')
    309         self._key = key
    310 
    311     @property
    312     def value(self):
    313         return self._value
    314 
    315     @value.setter
    316     def value(self, value):
    317         _warn_deprecated_setter('value')
    318         self._value = value
    319 
    320     @property
    321     def coded_value(self):
    322         return self._coded_value
    323 
    324     @coded_value.setter
    325     def coded_value(self, coded_value):
    326         _warn_deprecated_setter('coded_value')
    327         self._coded_value = coded_value
    328 
    329     def __setitem__(self, K, V):
    330         K = K.lower()
    331         if not K in self._reserved:
    332             raise CookieError("Invalid attribute %r" % (K,))
    333         dict.__setitem__(self, K, V)
    334 
    335     def setdefault(self, key, val=None):
    336         key = key.lower()
    337         if key not in self._reserved:
    338             raise CookieError("Invalid attribute %r" % (key,))
    339         return dict.setdefault(self, key, val)
    340 
    341     def __eq__(self, morsel):
    342         if not isinstance(morsel, Morsel):
    343             return NotImplemented
    344         return (dict.__eq__(self, morsel) and
    345                 self._value == morsel._value and
    346                 self._key == morsel._key and
    347                 self._coded_value == morsel._coded_value)
    348 
    349     __ne__ = object.__ne__
    350 
    351     def copy(self):
    352         morsel = Morsel()
    353         dict.update(morsel, self)
    354         morsel.__dict__.update(self.__dict__)
    355         return morsel
    356 
    357     def update(self, values):
    358         data = {}
    359         for key, val in dict(values).items():
    360             key = key.lower()
    361             if key not in self._reserved:
    362                 raise CookieError("Invalid attribute %r" % (key,))
    363             data[key] = val
    364         dict.update(self, data)
    365 
    366     def isReservedKey(self, K):
    367         return K.lower() in self._reserved
    368 
    369     def set(self, key, val, coded_val, LegalChars=_LegalChars):
    370         if LegalChars != _LegalChars:
    371             import warnings
    372             warnings.warn(
    373                 'LegalChars parameter is deprecated, ignored and will '
    374                 'be removed in future versions.', DeprecationWarning,
    375                 stacklevel=2)
    376 
    377         if key.lower() in self._reserved:
    378             raise CookieError('Attempt to set a reserved key %r' % (key,))
    379         if not _is_legal_key(key):
    380             raise CookieError('Illegal key %r' % (key,))
    381 
    382         # It's a good key, so save it.
    383         self._key = key
    384         self._value = val
    385         self._coded_value = coded_val
    386 
    387     def __getstate__(self):
    388         return {
    389             'key': self._key,
    390             'value': self._value,
    391             'coded_value': self._coded_value,
    392         }
    393 
    394     def __setstate__(self, state):
    395         self._key = state['key']
    396         self._value = state['value']
    397         self._coded_value = state['coded_value']
    398 
    399     def output(self, attrs=None, header="Set-Cookie:"):
    400         return "%s %s" % (header, self.OutputString(attrs))
    401 
    402     __str__ = output
    403 
    404     def __repr__(self):
    405         return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
    406 
    407     def js_output(self, attrs=None):
    408         # Print javascript
    409         return """
    410         <script type="text/javascript">
    411         <!-- begin hiding
    412         document.cookie = \"%s\";
    413         // end hiding -->
    414         </script>
    415         """ % (self.OutputString(attrs).replace('"', r'\"'))
    416 
    417     def OutputString(self, attrs=None):
    418         # Build up our result
    419         #
    420         result = []
    421         append = result.append
    422 
    423         # First, the key=value pair
    424         append("%s=%s" % (self.key, self.coded_value))
    425 
    426         # Now add any defined attributes
    427         if attrs is None:
    428             attrs = self._reserved
    429         items = sorted(self.items())
    430         for key, value in items:
    431             if value == "":
    432                 continue
    433             if key not in attrs:
    434                 continue
    435             if key == "expires" and isinstance(value, int):
    436                 append("%s=%s" % (self._reserved[key], _getdate(value)))
    437             elif key == "max-age" and isinstance(value, int):
    438                 append("%s=%d" % (self._reserved[key], value))
    439             elif key in self._flags:
    440                 if value:
    441                     append(str(self._reserved[key]))
    442             else:
    443                 append("%s=%s" % (self._reserved[key], value))
    444 
    445         # Return the result
    446         return _semispacejoin(result)
    447 
    448 
    449 #
    450 # Pattern for finding cookie
    451 #
    452 # This used to be strict parsing based on the RFC2109 and RFC2068
    453 # specifications.  I have since discovered that MSIE 3.0x doesn't
    454 # follow the character rules outlined in those specs.  As a
    455 # result, the parsing rules here are less strict.
    456 #
    457 
    458 _LegalKeyChars  = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\="
    459 _LegalValueChars = _LegalKeyChars + r'\[\]'
    460 _CookiePattern = re.compile(r"""
    461     \s*                            # Optional whitespace at start of cookie
    462     (?P<key>                       # Start of group 'key'
    463     [""" + _LegalKeyChars + r"""]+?   # Any word of at least one letter
    464     )                              # End of group 'key'
    465     (                              # Optional group: there may not be a value.
    466     \s*=\s*                          # Equal Sign
    467     (?P<val>                         # Start of group 'val'
    468     "(?:[^\\"]|\\.)*"                  # Any doublequoted string
    469     |                                  # or
    470     \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT  # Special case for "expires" attr
    471     |                                  # or
    472     [""" + _LegalValueChars + r"""]*      # Any word or empty string
    473     )                                # End of group 'val'
    474     )?                             # End of optional value group
    475     \s*                            # Any number of spaces.
    476     (\s+|;|$)                      # Ending either at space, semicolon, or EOS.
    477     """, re.ASCII | re.VERBOSE)    # re.ASCII may be removed if safe.
    478 
    479 
    480 # At long last, here is the cookie class.  Using this class is almost just like
    481 # using a dictionary.  See this module's docstring for example usage.
    482 #
    483 class BaseCookie(dict):
    484     """A container class for a set of Morsels."""
    485 
    486     def value_decode(self, val):
    487         """real_value, coded_value = value_decode(STRING)
    488         Called prior to setting a cookie's value from the network
    489         representation.  The VALUE is the value read from HTTP
    490         header.
    491         Override this function to modify the behavior of cookies.
    492         """
    493         return val, val
    494 
    495     def value_encode(self, val):
    496         """real_value, coded_value = value_encode(VALUE)
    497         Called prior to setting a cookie's value from the dictionary
    498         representation.  The VALUE is the value being assigned.
    499         Override this function to modify the behavior of cookies.
    500         """
    501         strval = str(val)
    502         return strval, strval
    503 
    504     def __init__(self, input=None):
    505         if input:
    506             self.load(input)
    507 
    508     def __set(self, key, real_value, coded_value):
    509         """Private method for setting a cookie's value"""
    510         M = self.get(key, Morsel())
    511         M.set(key, real_value, coded_value)
    512         dict.__setitem__(self, key, M)
    513 
    514     def __setitem__(self, key, value):
    515         """Dictionary style assignment."""
    516         if isinstance(value, Morsel):
    517             # allow assignment of constructed Morsels (e.g. for pickling)
    518             dict.__setitem__(self, key, value)
    519         else:
    520             rval, cval = self.value_encode(value)
    521             self.__set(key, rval, cval)
    522 
    523     def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
    524         """Return a string suitable for HTTP."""
    525         result = []
    526         items = sorted(self.items())
    527         for key, value in items:
    528             result.append(value.output(attrs, header))
    529         return sep.join(result)
    530 
    531     __str__ = output
    532 
    533     def __repr__(self):
    534         l = []
    535         items = sorted(self.items())
    536         for key, value in items:
    537             l.append('%s=%s' % (key, repr(value.value)))
    538         return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l))
    539 
    540     def js_output(self, attrs=None):
    541         """Return a string suitable for JavaScript."""
    542         result = []
    543         items = sorted(self.items())
    544         for key, value in items:
    545             result.append(value.js_output(attrs))
    546         return _nulljoin(result)
    547 
    548     def load(self, rawdata):
    549         """Load cookies from a string (presumably HTTP_COOKIE) or
    550         from a dictionary.  Loading cookies from a dictionary 'd'
    551         is equivalent to calling:
    552             map(Cookie.__setitem__, d.keys(), d.values())
    553         """
    554         if isinstance(rawdata, str):
    555             self.__parse_string(rawdata)
    556         else:
    557             # self.update() wouldn't call our custom __setitem__
    558             for key, value in rawdata.items():
    559                 self[key] = value
    560         return
    561 
    562     def __parse_string(self, str, patt=_CookiePattern):
    563         i = 0                 # Our starting point
    564         n = len(str)          # Length of string
    565         parsed_items = []     # Parsed (type, key, value) triples
    566         morsel_seen = False   # A key=value pair was previously encountered
    567 
    568         TYPE_ATTRIBUTE = 1
    569         TYPE_KEYVALUE = 2
    570 
    571         # We first parse the whole cookie string and reject it if it's
    572         # syntactically invalid (this helps avoid some classes of injection
    573         # attacks).
    574         while 0 <= i < n:
    575             # Start looking for a cookie
    576             match = patt.match(str, i)
    577             if not match:
    578                 # No more cookies
    579                 break
    580 
    581             key, value = match.group("key"), match.group("val")
    582             i = match.end(0)
    583 
    584             if key[0] == "$":
    585                 if not morsel_seen:
    586                     # We ignore attributes which pertain to the cookie
    587                     # mechanism as a whole, such as "$Version".
    588                     # See RFC 2965. (Does anyone care?)
    589                     continue
    590                 parsed_items.append((TYPE_ATTRIBUTE, key[1:], value))
    591             elif key.lower() in Morsel._reserved:
    592                 if not morsel_seen:
    593                     # Invalid cookie string
    594                     return
    595                 if value is None:
    596                     if key.lower() in Morsel._flags:
    597                         parsed_items.append((TYPE_ATTRIBUTE, key, True))
    598                     else:
    599                         # Invalid cookie string
    600                         return
    601                 else:
    602                     parsed_items.append((TYPE_ATTRIBUTE, key, _unquote(value)))
    603             elif value is not None:
    604                 parsed_items.append((TYPE_KEYVALUE, key, self.value_decode(value)))
    605                 morsel_seen = True
    606             else:
    607                 # Invalid cookie string
    608                 return
    609 
    610         # The cookie string is valid, apply it.
    611         M = None         # current morsel
    612         for tp, key, value in parsed_items:
    613             if tp == TYPE_ATTRIBUTE:
    614                 assert M is not None
    615                 M[key] = value
    616             else:
    617                 assert tp == TYPE_KEYVALUE
    618                 rval, cval = value
    619                 self.__set(key, rval, cval)
    620                 M = self[key]
    621 
    622 
    623 class SimpleCookie(BaseCookie):
    624     """
    625     SimpleCookie supports strings as cookie values.  When setting
    626     the value using the dictionary assignment notation, SimpleCookie
    627     calls the builtin str() to convert the value to a string.  Values
    628     received from HTTP are kept as strings.
    629     """
    630     def value_decode(self, val):
    631         return _unquote(val), val
    632 
    633     def value_encode(self, val):
    634         strval = str(val)
    635         return strval, _quote(strval)
    636