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