• 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
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_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub
188
189def _unquote_replace(m):
190    if m[1]:
191        return chr(int(m[1], 8))
192    else:
193        return m[2]
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    return _unquote_sub(_unquote_replace, str)
214
215# The _getdate() routine is used to set the expiration time in the cookie's HTTP
216# header.  By default, _getdate() returns the current time in the appropriate
217# "expires" format for a Set-Cookie header.  The one optional argument is an
218# offset from now, in seconds.  For example, an offset of -3600 means "one hour
219# ago".  The offset may be a floating-point number.
220#
221
222_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
223
224_monthname = [None,
225              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
226              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
227
228def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname):
229    from time import gmtime, time
230    now = time()
231    year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future)
232    return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \
233           (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
234
235
236class Morsel(dict):
237    """A class to hold ONE (key, value) pair.
238
239    In a cookie, each such pair may have several attributes, so this class is
240    used to keep the attributes associated with the appropriate key,value pair.
241    This class also includes a coded_value attribute, which is used to hold
242    the network representation of the value.
243    """
244    # RFC 2109 lists these attributes as reserved:
245    #   path       comment         domain
246    #   max-age    secure      version
247    #
248    # For historical reasons, these attributes are also reserved:
249    #   expires
250    #
251    # This is an extension from Microsoft:
252    #   httponly
253    #
254    # This dictionary provides a mapping from the lowercase
255    # variant on the left to the appropriate traditional
256    # formatting on the right.
257    _reserved = {
258        "expires"  : "expires",
259        "path"     : "Path",
260        "comment"  : "Comment",
261        "domain"   : "Domain",
262        "max-age"  : "Max-Age",
263        "secure"   : "Secure",
264        "httponly" : "HttpOnly",
265        "version"  : "Version",
266        "samesite" : "SameSite",
267    }
268
269    _flags = {'secure', 'httponly'}
270
271    def __init__(self):
272        # Set defaults
273        self._key = self._value = self._coded_value = None
274
275        # Set default attributes
276        for key in self._reserved:
277            dict.__setitem__(self, key, "")
278
279    @property
280    def key(self):
281        return self._key
282
283    @property
284    def value(self):
285        return self._value
286
287    @property
288    def coded_value(self):
289        return self._coded_value
290
291    def __setitem__(self, K, V):
292        K = K.lower()
293        if not K in self._reserved:
294            raise CookieError("Invalid attribute %r" % (K,))
295        dict.__setitem__(self, K, V)
296
297    def setdefault(self, key, val=None):
298        key = key.lower()
299        if key not in self._reserved:
300            raise CookieError("Invalid attribute %r" % (key,))
301        return dict.setdefault(self, key, val)
302
303    def __eq__(self, morsel):
304        if not isinstance(morsel, Morsel):
305            return NotImplemented
306        return (dict.__eq__(self, morsel) and
307                self._value == morsel._value and
308                self._key == morsel._key and
309                self._coded_value == morsel._coded_value)
310
311    __ne__ = object.__ne__
312
313    def copy(self):
314        morsel = Morsel()
315        dict.update(morsel, self)
316        morsel.__dict__.update(self.__dict__)
317        return morsel
318
319    def update(self, values):
320        data = {}
321        for key, val in dict(values).items():
322            key = key.lower()
323            if key not in self._reserved:
324                raise CookieError("Invalid attribute %r" % (key,))
325            data[key] = val
326        dict.update(self, data)
327
328    def isReservedKey(self, K):
329        return K.lower() in self._reserved
330
331    def set(self, key, val, coded_val):
332        if key.lower() in self._reserved:
333            raise CookieError('Attempt to set a reserved key %r' % (key,))
334        if not _is_legal_key(key):
335            raise CookieError('Illegal key %r' % (key,))
336
337        # It's a good key, so save it.
338        self._key = key
339        self._value = val
340        self._coded_value = coded_val
341
342    def __getstate__(self):
343        return {
344            'key': self._key,
345            'value': self._value,
346            'coded_value': self._coded_value,
347        }
348
349    def __setstate__(self, state):
350        self._key = state['key']
351        self._value = state['value']
352        self._coded_value = state['coded_value']
353
354    def output(self, attrs=None, header="Set-Cookie:"):
355        return "%s %s" % (header, self.OutputString(attrs))
356
357    __str__ = output
358
359    def __repr__(self):
360        return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
361
362    def js_output(self, attrs=None):
363        # Print javascript
364        return """
365        <script type="text/javascript">
366        <!-- begin hiding
367        document.cookie = \"%s\";
368        // end hiding -->
369        </script>
370        """ % (self.OutputString(attrs).replace('"', r'\"'))
371
372    def OutputString(self, attrs=None):
373        # Build up our result
374        #
375        result = []
376        append = result.append
377
378        # First, the key=value pair
379        append("%s=%s" % (self.key, self.coded_value))
380
381        # Now add any defined attributes
382        if attrs is None:
383            attrs = self._reserved
384        items = sorted(self.items())
385        for key, value in items:
386            if value == "":
387                continue
388            if key not in attrs:
389                continue
390            if key == "expires" and isinstance(value, int):
391                append("%s=%s" % (self._reserved[key], _getdate(value)))
392            elif key == "max-age" and isinstance(value, int):
393                append("%s=%d" % (self._reserved[key], value))
394            elif key == "comment" and isinstance(value, str):
395                append("%s=%s" % (self._reserved[key], _quote(value)))
396            elif key in self._flags:
397                if value:
398                    append(str(self._reserved[key]))
399            else:
400                append("%s=%s" % (self._reserved[key], value))
401
402        # Return the result
403        return _semispacejoin(result)
404
405    __class_getitem__ = classmethod(types.GenericAlias)
406
407
408#
409# Pattern for finding cookie
410#
411# This used to be strict parsing based on the RFC2109 and RFC2068
412# specifications.  I have since discovered that MSIE 3.0x doesn't
413# follow the character rules outlined in those specs.  As a
414# result, the parsing rules here are less strict.
415#
416
417_LegalKeyChars  = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\="
418_LegalValueChars = _LegalKeyChars + r'\[\]'
419_CookiePattern = re.compile(r"""
420    \s*                            # Optional whitespace at start of cookie
421    (?P<key>                       # Start of group 'key'
422    [""" + _LegalKeyChars + r"""]+?   # Any word of at least one letter
423    )                              # End of group 'key'
424    (                              # Optional group: there may not be a value.
425    \s*=\s*                          # Equal Sign
426    (?P<val>                         # Start of group 'val'
427    "(?:[^\\"]|\\.)*"                  # Any doublequoted string
428    |                                  # or
429    \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT  # Special case for "expires" attr
430    |                                  # or
431    [""" + _LegalValueChars + r"""]*      # Any word or empty string
432    )                                # End of group 'val'
433    )?                             # End of optional value group
434    \s*                            # Any number of spaces.
435    (\s+|;|$)                      # Ending either at space, semicolon, or EOS.
436    """, re.ASCII | re.VERBOSE)    # re.ASCII may be removed if safe.
437
438
439# At long last, here is the cookie class.  Using this class is almost just like
440# using a dictionary.  See this module's docstring for example usage.
441#
442class BaseCookie(dict):
443    """A container class for a set of Morsels."""
444
445    def value_decode(self, val):
446        """real_value, coded_value = value_decode(STRING)
447        Called prior to setting a cookie's value from the network
448        representation.  The VALUE is the value read from HTTP
449        header.
450        Override this function to modify the behavior of cookies.
451        """
452        return val, val
453
454    def value_encode(self, val):
455        """real_value, coded_value = value_encode(VALUE)
456        Called prior to setting a cookie's value from the dictionary
457        representation.  The VALUE is the value being assigned.
458        Override this function to modify the behavior of cookies.
459        """
460        strval = str(val)
461        return strval, strval
462
463    def __init__(self, input=None):
464        if input:
465            self.load(input)
466
467    def __set(self, key, real_value, coded_value):
468        """Private method for setting a cookie's value"""
469        M = self.get(key, Morsel())
470        M.set(key, real_value, coded_value)
471        dict.__setitem__(self, key, M)
472
473    def __setitem__(self, key, value):
474        """Dictionary style assignment."""
475        if isinstance(value, Morsel):
476            # allow assignment of constructed Morsels (e.g. for pickling)
477            dict.__setitem__(self, key, value)
478        else:
479            rval, cval = self.value_encode(value)
480            self.__set(key, rval, cval)
481
482    def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
483        """Return a string suitable for HTTP."""
484        result = []
485        items = sorted(self.items())
486        for key, value in items:
487            result.append(value.output(attrs, header))
488        return sep.join(result)
489
490    __str__ = output
491
492    def __repr__(self):
493        l = []
494        items = sorted(self.items())
495        for key, value in items:
496            l.append('%s=%s' % (key, repr(value.value)))
497        return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l))
498
499    def js_output(self, attrs=None):
500        """Return a string suitable for JavaScript."""
501        result = []
502        items = sorted(self.items())
503        for key, value in items:
504            result.append(value.js_output(attrs))
505        return _nulljoin(result)
506
507    def load(self, rawdata):
508        """Load cookies from a string (presumably HTTP_COOKIE) or
509        from a dictionary.  Loading cookies from a dictionary 'd'
510        is equivalent to calling:
511            map(Cookie.__setitem__, d.keys(), d.values())
512        """
513        if isinstance(rawdata, str):
514            self.__parse_string(rawdata)
515        else:
516            # self.update() wouldn't call our custom __setitem__
517            for key, value in rawdata.items():
518                self[key] = value
519        return
520
521    def __parse_string(self, str, patt=_CookiePattern):
522        i = 0                 # Our starting point
523        n = len(str)          # Length of string
524        parsed_items = []     # Parsed (type, key, value) triples
525        morsel_seen = False   # A key=value pair was previously encountered
526
527        TYPE_ATTRIBUTE = 1
528        TYPE_KEYVALUE = 2
529
530        # We first parse the whole cookie string and reject it if it's
531        # syntactically invalid (this helps avoid some classes of injection
532        # attacks).
533        while 0 <= i < n:
534            # Start looking for a cookie
535            match = patt.match(str, i)
536            if not match:
537                # No more cookies
538                break
539
540            key, value = match.group("key"), match.group("val")
541            i = match.end(0)
542
543            if key[0] == "$":
544                if not morsel_seen:
545                    # We ignore attributes which pertain to the cookie
546                    # mechanism as a whole, such as "$Version".
547                    # See RFC 2965. (Does anyone care?)
548                    continue
549                parsed_items.append((TYPE_ATTRIBUTE, key[1:], value))
550            elif key.lower() in Morsel._reserved:
551                if not morsel_seen:
552                    # Invalid cookie string
553                    return
554                if value is None:
555                    if key.lower() in Morsel._flags:
556                        parsed_items.append((TYPE_ATTRIBUTE, key, True))
557                    else:
558                        # Invalid cookie string
559                        return
560                else:
561                    parsed_items.append((TYPE_ATTRIBUTE, key, _unquote(value)))
562            elif value is not None:
563                parsed_items.append((TYPE_KEYVALUE, key, self.value_decode(value)))
564                morsel_seen = True
565            else:
566                # Invalid cookie string
567                return
568
569        # The cookie string is valid, apply it.
570        M = None         # current morsel
571        for tp, key, value in parsed_items:
572            if tp == TYPE_ATTRIBUTE:
573                assert M is not None
574                M[key] = value
575            else:
576                assert tp == TYPE_KEYVALUE
577                rval, cval = value
578                self.__set(key, rval, cval)
579                M = self[key]
580
581
582class SimpleCookie(BaseCookie):
583    """
584    SimpleCookie supports strings as cookie values.  When setting
585    the value using the dictionary assignment notation, SimpleCookie
586    calls the builtin str() to convert the value to a string.  Values
587    received from HTTP are kept as strings.
588    """
589    def value_decode(self, val):
590        return _unquote(val), val
591
592    def value_encode(self, val):
593        strval = str(val)
594        return strval, _quote(strval)
595