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