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