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
141def _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#
150class 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
179def _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
195def _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
251def _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
259class 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#
483class 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
623class 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