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