1 import collections 2 3 import base64 4 import binascii 5 import hashlib 6 import hmac 7 import json 8 from datetime import ( 9 date, 10 datetime, 11 timedelta, 12 ) 13 import re 14 import string 15 import time 16 import warnings 17 18 from webob.compat import ( 19 PY3, 20 text_type, 21 bytes_, 22 text_, 23 native_, 24 string_types, 25 ) 26 27 from webob.util import strings_differ 28 29 __all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer', 30 'JSONSerializer', 'Base64Serializer', 'make_cookie'] 31 32 _marker = object() 33 34 class RequestCookies(collections.MutableMapping): 35 36 _cache_key = 'webob._parsed_cookies' 37 38 def __init__(self, environ): 39 self._environ = environ 40 41 @property 42 def _cache(self): 43 env = self._environ 44 header = env.get('HTTP_COOKIE', '') 45 cache, cache_header = env.get(self._cache_key, ({}, None)) 46 if cache_header == header: 47 return cache 48 d = lambda b: b.decode('utf8') 49 cache = dict((d(k), d(v)) for k,v in parse_cookie(header)) 50 env[self._cache_key] = (cache, header) 51 return cache 52 53 def _mutate_header(self, name, value): 54 header = self._environ.get('HTTP_COOKIE') 55 had_header = header is not None 56 header = header or '' 57 if PY3: # pragma: no cover 58 header = header.encode('latin-1') 59 bytes_name = bytes_(name, 'ascii') 60 if value is None: 61 replacement = None 62 else: 63 bytes_val = _value_quote(bytes_(value, 'utf-8')) 64 replacement = bytes_name + b'=' + bytes_val 65 matches = _rx_cookie.finditer(header) 66 found = False 67 for match in matches: 68 start, end = match.span() 69 match_name = match.group(1) 70 if match_name == bytes_name: 71 found = True 72 if replacement is None: # remove value 73 header = header[:start].rstrip(b' ;') + header[end:] 74 else: # replace value 75 header = header[:start] + replacement + header[end:] 76 break 77 else: 78 if replacement is not None: 79 if header: 80 header += b'; ' + replacement 81 else: 82 header = replacement 83 84 if header: 85 self._environ['HTTP_COOKIE'] = native_(header, 'latin-1') 86 elif had_header: 87 self._environ['HTTP_COOKIE'] = '' 88 89 return found 90 91 def _valid_cookie_name(self, name): 92 if not isinstance(name, string_types): 93 raise TypeError(name, 'cookie name must be a string') 94 if not isinstance(name, text_type): 95 name = text_(name, 'utf-8') 96 try: 97 bytes_cookie_name = bytes_(name, 'ascii') 98 except UnicodeEncodeError: 99 raise TypeError('cookie name must be encodable to ascii') 100 if not _valid_cookie_name(bytes_cookie_name): 101 raise TypeError('cookie name must be valid according to RFC 6265') 102 return name 103 104 def __setitem__(self, name, value): 105 name = self._valid_cookie_name(name) 106 if not isinstance(value, string_types): 107 raise ValueError(value, 'cookie value must be a string') 108 if not isinstance(value, text_type): 109 try: 110 value = text_(value, 'utf-8') 111 except UnicodeDecodeError: 112 raise ValueError( 113 value, 'cookie value must be utf-8 binary or unicode') 114 self._mutate_header(name, value) 115 116 def __getitem__(self, name): 117 return self._cache[name] 118 119 def get(self, name, default=None): 120 return self._cache.get(name, default) 121 122 def __delitem__(self, name): 123 name = self._valid_cookie_name(name) 124 found = self._mutate_header(name, None) 125 if not found: 126 raise KeyError(name) 127 128 def keys(self): 129 return self._cache.keys() 130 131 def values(self): 132 return self._cache.values() 133 134 def items(self): 135 return self._cache.items() 136 137 if not PY3: 138 def iterkeys(self): 139 return self._cache.iterkeys() 140 141 def itervalues(self): 142 return self._cache.itervalues() 143 144 def iteritems(self): 145 return self._cache.iteritems() 146 147 def __contains__(self, name): 148 return name in self._cache 149 150 def __iter__(self): 151 return self._cache.__iter__() 152 153 def __len__(self): 154 return len(self._cache) 155 156 def clear(self): 157 self._environ['HTTP_COOKIE'] = '' 158 159 def __repr__(self): 160 return '<RequestCookies (dict-like) with values %r>' % (self._cache,) 161 162 163 class Cookie(dict): 164 def __init__(self, input=None): 165 if input: 166 self.load(input) 167 168 def load(self, data): 169 morsel = {} 170 for key, val in _parse_cookie(data): 171 if key.lower() in _c_keys: 172 morsel[key] = val 173 else: 174 morsel = self.add(key, val) 175 176 def add(self, key, val): 177 if not isinstance(key, bytes): 178 key = key.encode('ascii', 'replace') 179 if not _valid_cookie_name(key): 180 return {} 181 r = Morsel(key, val) 182 dict.__setitem__(self, key, r) 183 return r 184 __setitem__ = add 185 186 def serialize(self, full=True): 187 return '; '.join(m.serialize(full) for m in self.values()) 188 189 def values(self): 190 return [m for _, m in sorted(self.items())] 191 192 __str__ = serialize 193 194 def __repr__(self): 195 return '<%s: [%s]>' % (self.__class__.__name__, 196 ', '.join(map(repr, self.values()))) 197 198 199 def _parse_cookie(data): 200 if PY3: # pragma: no cover 201 data = data.encode('latin-1') 202 for key, val in _rx_cookie.findall(data): 203 yield key, _unquote(val) 204 205 def parse_cookie(data): 206 """ 207 Parse cookies ignoring anything except names and values 208 """ 209 return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k)) 210 211 212 def cookie_property(key, serialize=lambda v: v): 213 def fset(self, v): 214 self[key] = serialize(v) 215 return property(lambda self: self[key], fset) 216 217 def serialize_max_age(v): 218 if isinstance(v, timedelta): 219 v = str(v.seconds + v.days*24*60*60) 220 elif isinstance(v, int): 221 v = str(v) 222 return bytes_(v) 223 224 def serialize_cookie_date(v): 225 if v is None: 226 return None 227 elif isinstance(v, bytes): 228 return v 229 elif isinstance(v, text_type): 230 return v.encode('ascii') 231 elif isinstance(v, int): 232 v = timedelta(seconds=v) 233 if isinstance(v, timedelta): 234 v = datetime.utcnow() + v 235 if isinstance(v, (datetime, date)): 236 v = v.timetuple() 237 r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v) 238 return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii') 239 240 class Morsel(dict): 241 __slots__ = ('name', 'value') 242 def __init__(self, name, value): 243 self.name = bytes_(name, encoding='ascii') 244 self.value = bytes_(value, encoding='ascii') 245 assert _valid_cookie_name(self.name) 246 self.update(dict.fromkeys(_c_keys, None)) 247 248 path = cookie_property(b'path') 249 domain = cookie_property(b'domain') 250 comment = cookie_property(b'comment') 251 expires = cookie_property(b'expires', serialize_cookie_date) 252 max_age = cookie_property(b'max-age', serialize_max_age) 253 httponly = cookie_property(b'httponly', bool) 254 secure = cookie_property(b'secure', bool) 255 256 def __setitem__(self, k, v): 257 k = bytes_(k.lower(), 'ascii') 258 if k in _c_keys: 259 dict.__setitem__(self, k, v) 260 261 def serialize(self, full=True): 262 result = [] 263 add = result.append 264 add(self.name + b'=' + _value_quote(self.value)) 265 if full: 266 for k in _c_valkeys: 267 v = self[k] 268 if v: 269 info = _c_renames[k] 270 name = info['name'] 271 quoter = info['quoter'] 272 add(name + b'=' + quoter(v)) 273 expires = self[b'expires'] 274 if expires: 275 add(b'expires=' + expires) 276 if self.secure: 277 add(b'secure') 278 if self.httponly: 279 add(b'HttpOnly') 280 return native_(b'; '.join(result), 'ascii') 281 282 __str__ = serialize 283 284 def __repr__(self): 285 return '<%s: %s=%r>' % (self.__class__.__name__, 286 native_(self.name), 287 native_(self.value) 288 ) 289 290 # 291 # parsing 292 # 293 294 295 _re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string 296 _legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'" 297 _re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars) 298 _re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" 299 _re_cookie_str_key = r"(%s+?)" % _re_legal_char 300 _re_cookie_str_equal = r"\s*=\s*" 301 _re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char 302 _re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val, 303 _re_unquoted_val) 304 _re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val 305 306 _rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii')) 307 _rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii')) 308 309 _bchr = (lambda i: bytes([i])) if PY3 else chr 310 _ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i)) 311 for i in range(256) 312 ) 313 _ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values())) 314 315 _b_dollar_sign = ord('$') if PY3 else '$' 316 _b_quote_mark = ord('"') if PY3 else '"' 317 318 def _unquote(v): 319 #assert isinstance(v, bytes) 320 if v and v[0] == v[-1] == _b_quote_mark: 321 v = v[1:-1] 322 return _rx_unquote.sub(_ch_unquote, v) 323 324 def _ch_unquote(m): 325 return _ch_unquote_map[m.group(1)] 326 327 328 # 329 # serializing 330 # 331 332 # these chars can be in cookie value see 333 # http://tools.ietf.org/html/rfc6265#section-4.1.1 and 334 # https://github.com/Pylons/webob/pull/104#issuecomment-28044314 335 # 336 # ! (0x21), "#$%&'()*+" (0x25-0x2B), "-./0123456789:" (0x2D-0x3A), 337 # "<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" (0x3C-0x5B), 338 # "]^_`abcdefghijklmnopqrstuvwxyz{|}~" (0x5D-0x7E) 339 340 _allowed_special_chars = "!#$%&'()*+-./:<=>?@[]^_`{|}~" 341 _allowed_cookie_chars = (string.ascii_letters + string.digits + 342 _allowed_special_chars) 343 _allowed_cookie_bytes = bytes_(_allowed_cookie_chars) 344 345 # these are the characters accepted in cookie *names* 346 # From http://tools.ietf.org/html/rfc2616#section-2.2: 347 # token = 1*<any CHAR except CTLs or separators> 348 # separators = "(" | ")" | "<" | ">" | "@" 349 # | "," | ";" | ":" | "\" | <"> 350 # | "/" | "[" | "]" | "?" | "=" 351 # | "{" | "}" | SP | HT 352 # 353 # CTL = <any US-ASCII control character 354 # (octets 0 - 31) and DEL (127)> 355 # 356 _valid_token_chars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~" 357 _valid_token_bytes = bytes_(_valid_token_chars) 358 359 # this is a map used to escape the values 360 361 _escape_noop_chars = _allowed_cookie_chars + ' ' 362 _escape_map = dict((chr(i), '\\%03o' % i) for i in range(256)) 363 _escape_map.update(zip(_escape_noop_chars, _escape_noop_chars)) 364 if PY3: # pragma: no cover 365 # convert to {int -> bytes} 366 _escape_map = dict( 367 (ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items() 368 ) 369 _escape_char = _escape_map.__getitem__ 370 371 weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') 372 months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 373 'Oct', 'Nov', 'Dec') 374 375 376 # This is temporary, until we can remove this from _value_quote 377 _should_raise = None 378 379 def __warn_or_raise(text, warn_class, to_raise, raise_reason): 380 if _should_raise: 381 raise to_raise(raise_reason) 382 383 else: 384 warnings.warn(text, warn_class, stacklevel=2) 385 386 387 def _value_quote(v): 388 # This looks scary, but is simple. We remove all valid characters from the 389 # string, if we end up with leftovers (string is longer than 0, we have 390 # invalid characters in our value) 391 392 leftovers = v.translate(None, _allowed_cookie_bytes) 393 if leftovers: 394 __warn_or_raise( 395 "Cookie value contains invalid bytes: (%s). Future versions " 396 "will raise ValueError upon encountering invalid bytes." % 397 (leftovers,), 398 RuntimeWarning, ValueError, 'Invalid characters in cookie value' 399 ) 400 #raise ValueError('Invalid characters in cookie value') 401 return b'"' + b''.join(map(_escape_char, v)) + b'"' 402 403 return v 404 405 def _valid_cookie_name(key): 406 return isinstance(key, bytes) and not ( 407 key.translate(None, _valid_token_bytes) 408 # Not explicitly required by RFC6265, may consider removing later: 409 or key[0] == _b_dollar_sign 410 or key.lower() in _c_keys 411 ) 412 413 def _path_quote(v): 414 return b''.join(map(_escape_char, v)) 415 416 _domain_quote = _path_quote 417 _max_age_quote = _path_quote 418 419 _c_renames = { 420 b"path" : {'name':b"Path", 'quoter':_path_quote}, 421 b"comment" : {'name':b"Comment", 'quoter':_value_quote}, 422 b"domain" : {'name':b"Domain", 'quoter':_domain_quote}, 423 b"max-age" : {'name':b"Max-Age", 'quoter':_max_age_quote}, 424 } 425 _c_valkeys = sorted(_c_renames) 426 _c_keys = set(_c_renames) 427 _c_keys.update([b'expires', b'secure', b'httponly']) 428 429 430 def make_cookie(name, value, max_age=None, path='/', domain=None, 431 secure=False, httponly=False, comment=None): 432 """ Generate a cookie value. If ``value`` is None, generate a cookie value 433 with an expiration date in the past""" 434 435 # We are deleting the cookie, override max_age and expires 436 if value is None: 437 value = b'' 438 # Note that the max-age value of zero is technically contraspec; 439 # RFC6265 says that max-age cannot be zero. However, all browsers 440 # appear to support this to mean "delete immediately". 441 # http://www.timwilson.id.au/news-three-critical-problems-with-rfc6265.html 442 max_age = 0 443 expires = 'Wed, 31-Dec-97 23:59:59 GMT' 444 445 # Convert max_age to seconds 446 elif isinstance(max_age, timedelta): 447 max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds 448 expires = max_age 449 else: 450 expires = max_age 451 452 morsel = Morsel(name, value) 453 454 if domain is not None: 455 morsel.domain = bytes_(domain) 456 if path is not None: 457 morsel.path = bytes_(path) 458 if httponly: 459 morsel.httponly = True 460 if secure: 461 morsel.secure = True 462 if max_age is not None: 463 morsel.max_age = max_age 464 if expires is not None: 465 morsel.expires = expires 466 if comment is not None: 467 morsel.comment = bytes_(comment) 468 return morsel.serialize() 469 470 class JSONSerializer(object): 471 """ A serializer which uses `json.dumps`` and ``json.loads``""" 472 def dumps(self, appstruct): 473 return bytes_(json.dumps(appstruct), encoding='utf-8') 474 475 def loads(self, bstruct): 476 # NB: json.loads raises ValueError if no json object can be decoded 477 # so we don't have to do it explicitly here. 478 return json.loads(text_(bstruct, encoding='utf-8')) 479 480 class Base64Serializer(object): 481 """ A serializer which uses base64 to encode/decode data""" 482 483 def __init__(self, serializer=None): 484 if serializer is None: 485 serializer = JSONSerializer() 486 487 self.serializer = serializer 488 489 def dumps(self, appstruct): 490 """ 491 Given an ``appstruct``, serialize and sign the data. 492 493 Returns a bytestring. 494 """ 495 cstruct = self.serializer.dumps(appstruct) # will be bytes 496 return base64.urlsafe_b64encode(cstruct) 497 498 def loads(self, bstruct): 499 """ 500 Given a ``bstruct`` (a bytestring), verify the signature and then 501 deserialize and return the deserialized value. 502 503 A ``ValueError`` will be raised if the signature fails to validate. 504 """ 505 try: 506 cstruct = base64.urlsafe_b64decode(bytes_(bstruct)) 507 except (binascii.Error, TypeError) as e: 508 raise ValueError('Badly formed base64 data: %s' % e) 509 510 return self.serializer.loads(cstruct) 511 512 class SignedSerializer(object): 513 """ 514 A helper to cryptographically sign arbitrary content using HMAC. 515 516 The serializer accepts arbitrary functions for performing the actual 517 serialization and deserialization. 518 519 ``secret`` 520 A string which is used to sign the cookie. The secret should be at 521 least as long as the block size of the selected hash algorithm. For 522 ``sha512`` this would mean a 128 bit (64 character) secret. 523 524 ``salt`` 525 A namespace to avoid collisions between different uses of a shared 526 secret. 527 528 ``hashalg`` 529 The HMAC digest algorithm to use for signing. The algorithm must be 530 supported by the :mod:`hashlib` library. Default: ``'sha512'``. 531 532 ``serializer`` 533 An object with two methods: `loads`` and ``dumps``. The ``loads`` method 534 should accept bytes and return a Python object. The ``dumps`` method 535 should accept a Python object and return bytes. A ``ValueError`` should 536 be raised for malformed inputs. Default: ``None`, which will use a 537 derivation of :func:`json.dumps` and ``json.loads``. 538 539 """ 540 541 def __init__(self, 542 secret, 543 salt, 544 hashalg='sha512', 545 serializer=None, 546 ): 547 self.salt = salt 548 self.secret = secret 549 self.hashalg = hashalg 550 551 try: 552 # bwcompat with webob <= 1.3.1, leave latin-1 as the default 553 self.salted_secret = bytes_(salt or '') + bytes_(secret) 554 except UnicodeEncodeError: 555 self.salted_secret = ( 556 bytes_(salt or '', 'utf-8') + bytes_(secret, 'utf-8')) 557 558 self.digestmod = lambda string=b'': hashlib.new(self.hashalg, string) 559 self.digest_size = self.digestmod().digest_size 560 561 if serializer is None: 562 serializer = JSONSerializer() 563 564 self.serializer = serializer 565 566 def dumps(self, appstruct): 567 """ 568 Given an ``appstruct``, serialize and sign the data. 569 570 Returns a bytestring. 571 """ 572 cstruct = self.serializer.dumps(appstruct) # will be bytes 573 sig = hmac.new(self.salted_secret, cstruct, self.digestmod).digest() 574 return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') 575 576 def loads(self, bstruct): 577 """ 578 Given a ``bstruct`` (a bytestring), verify the signature and then 579 deserialize and return the deserialized value. 580 581 A ``ValueError`` will be raised if the signature fails to validate. 582 """ 583 try: 584 b64padding = b'=' * (-len(bstruct) % 4) 585 fstruct = base64.urlsafe_b64decode(bytes_(bstruct) + b64padding) 586 except (binascii.Error, TypeError) as e: 587 raise ValueError('Badly formed base64 data: %s' % e) 588 589 cstruct = fstruct[self.digest_size:] 590 expected_sig = fstruct[:self.digest_size] 591 592 sig = hmac.new( 593 self.salted_secret, bytes_(cstruct), self.digestmod).digest() 594 595 if strings_differ(sig, expected_sig): 596 raise ValueError('Invalid signature') 597 598 return self.serializer.loads(cstruct) 599 600 601 _default = object() 602 603 class CookieProfile(object): 604 """ 605 A helper class that helps bring some sanity to the insanity that is cookie 606 handling. 607 608 The helper is capable of generating multiple cookies if necessary to 609 support subdomains and parent domains. 610 611 ``cookie_name`` 612 The name of the cookie used for sessioning. Default: ``'session'``. 613 614 ``max_age`` 615 The maximum age of the cookie used for sessioning (in seconds). 616 Default: ``None`` (browser scope). 617 618 ``secure`` 619 The 'secure' flag of the session cookie. Default: ``False``. 620 621 ``httponly`` 622 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the 623 session cookie. Default: ``False``. 624 625 ``path`` 626 The path used for the session cookie. Default: ``'/'``. 627 628 ``domains`` 629 The domain(s) used for the session cookie. Default: ``None`` (no domain). 630 Can be passed an iterable containing multiple domains, this will set 631 multiple cookies one for each domain. 632 633 ``serializer`` 634 An object with two methods: ``loads`` and ``dumps``. The ``loads`` method 635 should accept a bytestring and return a Python object. The ``dumps`` 636 method should accept a Python object and return bytes. A ``ValueError`` 637 should be raised for malformed inputs. Default: ``None``, which will use 638 a derivation of :func:`json.dumps` and :func:`json.loads`. 639 640 """ 641 642 def __init__(self, 643 cookie_name, 644 secure=False, 645 max_age=None, 646 httponly=None, 647 path='/', 648 domains=None, 649 serializer=None 650 ): 651 self.cookie_name = cookie_name 652 self.secure = secure 653 self.max_age = max_age 654 self.httponly = httponly 655 self.path = path 656 self.domains = domains 657 658 if serializer is None: 659 serializer = Base64Serializer() 660 661 self.serializer = serializer 662 self.request = None 663 664 def __call__(self, request): 665 """ Bind a request to a copy of this instance and return it""" 666 667 return self.bind(request) 668 669 def bind(self, request): 670 """ Bind a request to a copy of this instance and return it""" 671 672 selfish = CookieProfile( 673 self.cookie_name, 674 self.secure, 675 self.max_age, 676 self.httponly, 677 self.path, 678 self.domains, 679 self.serializer, 680 ) 681 selfish.request = request 682 return selfish 683 684 def get_value(self): 685 """ Looks for a cookie by name in the currently bound request, and 686 returns its value. If the cookie profile is not bound to a request, 687 this method will raise a :exc:`ValueError`. 688 689 Looks for the cookie in the cookies jar, and if it can find it it will 690 attempt to deserialize it. Returns ``None`` if there is no cookie or 691 if the value in the cookie cannot be successfully deserialized. 692 """ 693 694 if not self.request: 695 raise ValueError('No request bound to cookie profile') 696 697 cookie = self.request.cookies.get(self.cookie_name) 698 699 if cookie is not None: 700 try: 701 return self.serializer.loads(bytes_(cookie)) 702 except ValueError: 703 return None 704 705 def set_cookies(self, response, value, domains=_default, max_age=_default, 706 path=_default, secure=_default, httponly=_default): 707 """ Set the cookies on a response.""" 708 cookies = self.get_headers( 709 value, 710 domains=domains, 711 max_age=max_age, 712 path=path, 713 secure=secure, 714 httponly=httponly 715 ) 716 response.headerlist.extend(cookies) 717 return response 718 719 def get_headers(self, value, domains=_default, max_age=_default, 720 path=_default, secure=_default, httponly=_default): 721 """ Retrieve raw headers for setting cookies. 722 723 Returns a list of headers that should be set for the cookies to 724 be correctly tracked. 725 """ 726 if value is None: 727 max_age = 0 728 bstruct = None 729 else: 730 bstruct = self.serializer.dumps(value) 731 732 return self._get_cookies( 733 bstruct, 734 domains=domains, 735 max_age=max_age, 736 path=path, 737 secure=secure, 738 httponly=httponly 739 ) 740 741 def _get_cookies(self, value, domains, max_age, path, secure, httponly): 742 """Internal function 743 744 This returns a list of cookies that are valid HTTP Headers. 745 746 :environ: The request environment 747 :value: The value to store in the cookie 748 :domains: The domains, overrides any set in the CookieProfile 749 :max_age: The max_age, overrides any set in the CookieProfile 750 :path: The path, overrides any set in the CookieProfile 751 :secure: Set this cookie to secure, overrides any set in CookieProfile 752 :httponly: Set this cookie to HttpOnly, overrides any set in CookieProfile 753 754 """ 755 756 # If the user doesn't provide values, grab the defaults 757 if domains is _default: 758 domains = self.domains 759 760 if max_age is _default: 761 max_age = self.max_age 762 763 if path is _default: 764 path = self.path 765 766 if secure is _default: 767 secure = self.secure 768 769 if httponly is _default: 770 httponly = self.httponly 771 772 # Length selected based upon http://browsercookielimits.x64.me 773 if value is not None and len(value) > 4093: 774 raise ValueError( 775 'Cookie value is too long to store (%s bytes)' % 776 len(value) 777 ) 778 779 cookies = [] 780 781 if not domains: 782 cookievalue = make_cookie( 783 self.cookie_name, 784 value, 785 path=path, 786 max_age=max_age, 787 httponly=httponly, 788 secure=secure 789 ) 790 cookies.append(('Set-Cookie', cookievalue)) 791 792 else: 793 for domain in domains: 794 cookievalue = make_cookie( 795 self.cookie_name, 796 value, 797 path=path, 798 domain=domain, 799 max_age=max_age, 800 httponly=httponly, 801 secure=secure, 802 ) 803 cookies.append(('Set-Cookie', cookievalue)) 804 805 return cookies 806 807 808 class SignedCookieProfile(CookieProfile): 809 """ 810 A helper for generating cookies that are signed to prevent tampering. 811 812 By default this will create a single cookie, given a value it will 813 serialize it, then use HMAC to cryptographically sign the data. Finally 814 the result is base64-encoded for transport. This way a remote user can 815 not tamper with the value without uncovering the secret/salt used. 816 817 ``secret`` 818 A string which is used to sign the cookie. The secret should be at 819 least as long as the block size of the selected hash algorithm. For 820 ``sha512`` this would mean a 128 bit (64 character) secret. 821 822 ``salt`` 823 A namespace to avoid collisions between different uses of a shared 824 secret. 825 826 ``hashalg`` 827 The HMAC digest algorithm to use for signing. The algorithm must be 828 supported by the :mod:`hashlib` library. Default: ``'sha512'``. 829 830 ``cookie_name`` 831 The name of the cookie used for sessioning. Default: ``'session'``. 832 833 ``max_age`` 834 The maximum age of the cookie used for sessioning (in seconds). 835 Default: ``None`` (browser scope). 836 837 ``secure`` 838 The 'secure' flag of the session cookie. Default: ``False``. 839 840 ``httponly`` 841 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the 842 session cookie. Default: ``False``. 843 844 ``path`` 845 The path used for the session cookie. Default: ``'/'``. 846 847 ``domains`` 848 The domain(s) used for the session cookie. Default: ``None`` (no domain). 849 Can be passed an iterable containing multiple domains, this will set 850 multiple cookies one for each domain. 851 852 ``serializer`` 853 An object with two methods: `loads`` and ``dumps``. The ``loads`` method 854 should accept bytes and return a Python object. The ``dumps`` method 855 should accept a Python object and return bytes. A ``ValueError`` should 856 be raised for malformed inputs. Default: ``None`, which will use a 857 derivation of :func:`json.dumps` and ``json.loads``. 858 """ 859 def __init__(self, 860 secret, 861 salt, 862 cookie_name, 863 secure=False, 864 max_age=None, 865 httponly=False, 866 path="/", 867 domains=None, 868 hashalg='sha512', 869 serializer=None, 870 ): 871 self.secret = secret 872 self.salt = salt 873 self.hashalg = hashalg 874 self.original_serializer = serializer 875 876 signed_serializer = SignedSerializer( 877 secret, 878 salt, 879 hashalg, 880 serializer=self.original_serializer, 881 ) 882 CookieProfile.__init__( 883 self, 884 cookie_name, 885 secure=secure, 886 max_age=max_age, 887 httponly=httponly, 888 path=path, 889 domains=domains, 890 serializer=signed_serializer, 891 ) 892 893 def bind(self, request): 894 """ Bind a request to a copy of this instance and return it""" 895 896 selfish = SignedCookieProfile( 897 self.secret, 898 self.salt, 899 self.cookie_name, 900 self.secure, 901 self.max_age, 902 self.httponly, 903 self.path, 904 self.domains, 905 self.hashalg, 906 self.original_serializer, 907 ) 908 selfish.request = request 909 return selfish 910 911