Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/webob/cookies.py : 30%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import base64
2import binascii
3import hashlib
4import hmac
5import json
6from datetime import (
7 date,
8 datetime,
9 timedelta,
10 )
11import re
12import string
13import time
14import warnings
16from webob.compat import (
17 MutableMapping,
18 PY2,
19 text_type,
20 bytes_,
21 text_,
22 native_,
23 string_types,
24 )
26from webob.util import strings_differ
28__all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer',
29 'JSONSerializer', 'Base64Serializer', 'make_cookie']
31_marker = object()
33# Module flag to handle validation of SameSite attributes
34# See the documentation for ``make_cookie`` for more information.
35SAMESITE_VALIDATION = True
38class RequestCookies(MutableMapping):
40 _cache_key = 'webob._parsed_cookies'
42 def __init__(self, environ):
43 self._environ = environ
45 @property
46 def _cache(self):
47 env = self._environ
48 header = env.get('HTTP_COOKIE', '')
49 cache, cache_header = env.get(self._cache_key, ({}, None))
50 if cache_header == header:
51 return cache
52 d = lambda b: b.decode('utf8')
53 cache = dict((d(k), d(v)) for k,v in parse_cookie(header))
54 env[self._cache_key] = (cache, header)
55 return cache
57 def _mutate_header(self, name, value):
58 header = self._environ.get('HTTP_COOKIE')
59 had_header = header is not None
60 header = header or ''
61 if not PY2:
62 header = header.encode('latin-1')
63 bytes_name = bytes_(name, 'ascii')
64 if value is None:
65 replacement = None
66 else:
67 bytes_val = _value_quote(bytes_(value, 'utf-8'))
68 replacement = bytes_name + b'=' + bytes_val
69 matches = _rx_cookie.finditer(header)
70 found = False
71 for match in matches:
72 start, end = match.span()
73 match_name = match.group(1)
74 if match_name == bytes_name:
75 found = True
76 if replacement is None: # remove value
77 header = header[:start].rstrip(b' ;') + header[end:]
78 else: # replace value
79 header = header[:start] + replacement + header[end:]
80 break
81 else:
82 if replacement is not None:
83 if header:
84 header += b'; ' + replacement
85 else:
86 header = replacement
88 if header:
89 self._environ['HTTP_COOKIE'] = native_(header, 'latin-1')
90 elif had_header:
91 self._environ['HTTP_COOKIE'] = ''
93 return found
95 def _valid_cookie_name(self, name):
96 if not isinstance(name, string_types):
97 raise TypeError(name, 'cookie name must be a string')
98 if not isinstance(name, text_type):
99 name = text_(name, 'utf-8')
100 try:
101 bytes_cookie_name = bytes_(name, 'ascii')
102 except UnicodeEncodeError:
103 raise TypeError('cookie name must be encodable to ascii')
104 if not _valid_cookie_name(bytes_cookie_name):
105 raise TypeError('cookie name must be valid according to RFC 6265')
106 return name
108 def __setitem__(self, name, value):
109 name = self._valid_cookie_name(name)
110 if not isinstance(value, string_types):
111 raise ValueError(value, 'cookie value must be a string')
112 if not isinstance(value, text_type):
113 try:
114 value = text_(value, 'utf-8')
115 except UnicodeDecodeError:
116 raise ValueError(
117 value, 'cookie value must be utf-8 binary or unicode')
118 self._mutate_header(name, value)
120 def __getitem__(self, name):
121 return self._cache[name]
123 def get(self, name, default=None):
124 return self._cache.get(name, default)
126 def __delitem__(self, name):
127 name = self._valid_cookie_name(name)
128 found = self._mutate_header(name, None)
129 if not found:
130 raise KeyError(name)
132 def keys(self):
133 return self._cache.keys()
135 def values(self):
136 return self._cache.values()
138 def items(self):
139 return self._cache.items()
141 if PY2:
142 def iterkeys(self):
143 return self._cache.iterkeys()
145 def itervalues(self):
146 return self._cache.itervalues()
148 def iteritems(self):
149 return self._cache.iteritems()
151 def __contains__(self, name):
152 return name in self._cache
154 def __iter__(self):
155 return self._cache.__iter__()
157 def __len__(self):
158 return len(self._cache)
160 def clear(self):
161 self._environ['HTTP_COOKIE'] = ''
163 def __repr__(self):
164 return '<RequestCookies (dict-like) with values %r>' % (self._cache,)
167class Cookie(dict):
168 def __init__(self, input=None):
169 if input:
170 self.load(input)
172 def load(self, data):
173 morsel = {}
174 for key, val in _parse_cookie(data):
175 if key.lower() in _c_keys:
176 morsel[key] = val
177 else:
178 morsel = self.add(key, val)
180 def add(self, key, val):
181 if not isinstance(key, bytes):
182 key = key.encode('ascii', 'replace')
183 if not _valid_cookie_name(key):
184 return {}
185 r = Morsel(key, val)
186 dict.__setitem__(self, key, r)
187 return r
188 __setitem__ = add
190 def serialize(self, full=True):
191 return '; '.join(m.serialize(full) for m in self.values())
193 def values(self):
194 return [m for _, m in sorted(self.items())]
196 __str__ = serialize
198 def __repr__(self):
199 return '<%s: [%s]>' % (self.__class__.__name__,
200 ', '.join(map(repr, self.values())))
203def _parse_cookie(data):
204 if not PY2:
205 data = data.encode('latin-1')
206 for key, val in _rx_cookie.findall(data):
207 yield key, _unquote(val)
209def parse_cookie(data):
210 """
211 Parse cookies ignoring anything except names and values
212 """
213 return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k))
216def cookie_property(key, serialize=lambda v: v):
217 def fset(self, v):
218 self[key] = serialize(v)
219 return property(lambda self: self[key], fset)
221def serialize_max_age(v):
222 if isinstance(v, timedelta):
223 v = str(v.seconds + v.days*24*60*60)
224 elif isinstance(v, int):
225 v = str(v)
226 return bytes_(v)
228def serialize_cookie_date(v):
229 if v is None:
230 return None
231 elif isinstance(v, bytes):
232 return v
233 elif isinstance(v, text_type):
234 return v.encode('ascii')
235 elif isinstance(v, int):
236 v = timedelta(seconds=v)
237 if isinstance(v, timedelta):
238 v = datetime.utcnow() + v
239 if isinstance(v, (datetime, date)):
240 v = v.timetuple()
241 r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v)
242 return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii')
245def serialize_samesite(v):
246 v = bytes_(v)
248 if SAMESITE_VALIDATION:
249 if v.lower() not in (b"strict", b"lax", b"none"):
250 raise ValueError("SameSite must be 'strict', 'lax', or 'none'")
252 return v
255class Morsel(dict):
256 __slots__ = ('name', 'value')
257 def __init__(self, name, value):
258 self.name = bytes_(name, encoding='ascii')
259 self.value = bytes_(value, encoding='ascii')
260 assert _valid_cookie_name(self.name)
261 self.update(dict.fromkeys(_c_keys, None))
263 path = cookie_property(b'path')
264 domain = cookie_property(b'domain')
265 comment = cookie_property(b'comment')
266 expires = cookie_property(b'expires', serialize_cookie_date)
267 max_age = cookie_property(b'max-age', serialize_max_age)
268 httponly = cookie_property(b'httponly', bool)
269 secure = cookie_property(b'secure', bool)
270 samesite = cookie_property(b'samesite', serialize_samesite)
272 def __setitem__(self, k, v):
273 k = bytes_(k.lower(), 'ascii')
274 if k in _c_keys:
275 dict.__setitem__(self, k, v)
277 def serialize(self, full=True):
278 result = []
279 add = result.append
280 add(self.name + b'=' + _value_quote(self.value))
281 if full:
282 for k in _c_valkeys:
283 v = self[k]
284 if v:
285 info = _c_renames[k]
286 name = info['name']
287 quoter = info['quoter']
288 add(name + b'=' + quoter(v))
289 expires = self[b'expires']
290 if expires:
291 add(b'expires=' + expires)
292 if self.secure:
293 add(b'secure')
294 if self.httponly:
295 add(b'HttpOnly')
296 if self.samesite:
297 if not self.secure and self.samesite.lower() == b"none":
298 raise ValueError(
299 "Incompatible cookie attributes: "
300 "when the samesite equals 'none', then the secure must be True"
301 )
302 add(b"SameSite=" + self.samesite)
304 return native_(b"; ".join(result), "ascii")
306 __str__ = serialize
308 def __repr__(self):
309 return '<%s: %s=%r>' % (self.__class__.__name__,
310 native_(self.name),
311 native_(self.value)
312 )
314#
315# parsing
316#
319_re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string
320_legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'"
321_re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars)
322_re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT"
323_re_cookie_str_key = r"(%s+?)" % _re_legal_char
324_re_cookie_str_equal = r"\s*=\s*"
325_re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char
326_re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val,
327 _re_unquoted_val)
328_re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val
330_rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii'))
331_rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii'))
333_bchr = chr if PY2 else (lambda i: bytes([i]))
334_ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i))
335 for i in range(256)
336)
337_ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values()))
339_b_dollar_sign = '$' if PY2 else ord('$')
340_b_quote_mark = '"' if PY2 else ord('"')
342def _unquote(v):
343 #assert isinstance(v, bytes)
344 if v and v[0] == v[-1] == _b_quote_mark:
345 v = v[1:-1]
346 return _rx_unquote.sub(_ch_unquote, v)
348def _ch_unquote(m):
349 return _ch_unquote_map[m.group(1)]
352#
353# serializing
354#
356# these chars can be in cookie value see
357# http://tools.ietf.org/html/rfc6265#section-4.1.1 and
358# https://github.com/Pylons/webob/pull/104#issuecomment-28044314
359#
360# ! (0x21), "#$%&'()*+" (0x25-0x2B), "-./0123456789:" (0x2D-0x3A),
361# "<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" (0x3C-0x5B),
362# "]^_`abcdefghijklmnopqrstuvwxyz{|}~" (0x5D-0x7E)
364_allowed_special_chars = "!#$%&'()*+-./:<=>?@[]^_`{|}~"
365_allowed_cookie_chars = (string.ascii_letters + string.digits +
366 _allowed_special_chars)
367_allowed_cookie_bytes = bytes_(_allowed_cookie_chars)
369# these are the characters accepted in cookie *names*
370# From http://tools.ietf.org/html/rfc2616#section-2.2:
371# token = 1*<any CHAR except CTLs or separators>
372# separators = "(" | ")" | "<" | ">" | "@"
373# | "," | ";" | ":" | "\" | <">
374# | "/" | "[" | "]" | "?" | "="
375# | "{" | "}" | SP | HT
376#
377# CTL = <any US-ASCII control character
378# (octets 0 - 31) and DEL (127)>
379#
380_valid_token_chars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~"
381_valid_token_bytes = bytes_(_valid_token_chars)
383# this is a map used to escape the values
385_escape_noop_chars = _allowed_cookie_chars + ' '
386_escape_map = dict((chr(i), '\\%03o' % i) for i in range(256))
387_escape_map.update(zip(_escape_noop_chars, _escape_noop_chars))
388if not PY2:
389 # convert to {int -> bytes}
390 _escape_map = dict(
391 (ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items()
392 )
393_escape_char = _escape_map.__getitem__
395weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
396months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
397 'Oct', 'Nov', 'Dec')
400# This is temporary, until we can remove this from _value_quote
401_should_raise = None
403def __warn_or_raise(text, warn_class, to_raise, raise_reason):
404 if _should_raise:
405 raise to_raise(raise_reason)
407 else:
408 warnings.warn(text, warn_class, stacklevel=2)
411def _value_quote(v):
412 # This looks scary, but is simple. We remove all valid characters from the
413 # string, if we end up with leftovers (string is longer than 0, we have
414 # invalid characters in our value)
416 leftovers = v.translate(None, _allowed_cookie_bytes)
417 if leftovers:
418 __warn_or_raise(
419 "Cookie value contains invalid bytes: (%r). Future versions "
420 "will raise ValueError upon encountering invalid bytes." %
421 (leftovers,),
422 RuntimeWarning, ValueError, 'Invalid characters in cookie value'
423 )
424 #raise ValueError('Invalid characters in cookie value')
425 return b'"' + b''.join(map(_escape_char, v)) + b'"'
427 return v
429def _valid_cookie_name(key):
430 return isinstance(key, bytes) and not (
431 key.translate(None, _valid_token_bytes)
432 # Not explicitly required by RFC6265, may consider removing later:
433 or key[0] == _b_dollar_sign
434 or key.lower() in _c_keys
435 )
437def _path_quote(v):
438 return b''.join(map(_escape_char, v))
440_domain_quote = _path_quote
441_max_age_quote = _path_quote
443_c_renames = {
444 b"path" : {'name':b"Path", 'quoter':_path_quote},
445 b"comment" : {'name':b"Comment", 'quoter':_value_quote},
446 b"domain" : {'name':b"Domain", 'quoter':_domain_quote},
447 b"max-age" : {'name':b"Max-Age", 'quoter':_max_age_quote},
448 }
449_c_valkeys = sorted(_c_renames)
450_c_keys = set(_c_renames)
451_c_keys.update([b'expires', b'secure', b'httponly', b'samesite'])
454def make_cookie(name, value, max_age=None, path='/', domain=None,
455 secure=False, httponly=False, comment=None, samesite=None):
456 """
457 Generate a cookie value.
459 ``name``
460 The name of the cookie.
462 ``value``
463 The ``value`` of the cookie. If it is ``None``, it will generate a cookie
464 value with an expiration date in the past.
466 ``max_age``
467 The maximum age of the cookie used for sessioning (in seconds).
468 Default: ``None`` (browser scope).
470 ``path``
471 The path used for the session cookie. Default: ``/``.
473 ``domain``
474 The domain used for the session cookie. Default: ``None`` (no domain).
476 ``secure``
477 The 'secure' flag of the session cookie. Default: ``False``.
479 ``httponly``
480 Hide the cookie from JavaScript by setting the 'HttpOnly' flag of the
481 session cookie. Default: ``False``.
483 ``comment``
484 Set a comment on the cookie. Default: ``None``
486 ``samesite``
487 The 'SameSite' attribute of the cookie, can be either ``"strict"``,
488 ``"lax"``, ``"none"``, or ``None``. By default, WebOb will validate the
489 value to ensure it conforms to the allowable options in the various draft
490 RFC's that exist.
492 To disable this check and send headers that are experimental or introduced
493 in a future RFC, set the module flag ``SAMESITE_VALIDATION`` to a
494 false value like:
496 .. code::
498 import webob.cookies
499 webob.cookies.SAMESITE_VALIDATION = False
501 ck = webob.cookies.make_cookie(cookie_name, value, samesite='future')
503 .. danger::
505 This feature has known compatibility issues with various user agents,
506 and is not yet an accepted RFC. It is therefore considered
507 experimental and subject to change.
509 For more information please see :ref:`Experimental: SameSite Cookies
510 <samesiteexp>`
511 """
513 # We are deleting the cookie, override max_age and expires
514 if value is None:
515 value = b''
516 # Note that the max-age value of zero is technically contraspec;
517 # RFC6265 says that max-age cannot be zero. However, all browsers
518 # appear to support this to mean "delete immediately".
519 # http://www.timwilson.id.au/news-three-critical-problems-with-rfc6265.html
520 max_age = 0
521 expires = 'Wed, 31-Dec-97 23:59:59 GMT'
523 # Convert max_age to seconds
524 elif isinstance(max_age, timedelta):
525 max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
526 expires = max_age
527 elif max_age is not None:
528 try:
529 max_age = int(max_age)
530 except ValueError:
531 raise ValueError('max_age should be an integer. Amount of seconds until expiration.')
533 expires = max_age
534 else:
535 expires = None
537 morsel = Morsel(name, value)
539 if domain is not None:
540 morsel.domain = bytes_(domain)
541 if path is not None:
542 morsel.path = bytes_(path)
543 if httponly:
544 morsel.httponly = True
545 if secure:
546 morsel.secure = True
547 if max_age is not None:
548 morsel.max_age = max_age
549 if expires is not None:
550 morsel.expires = expires
551 if comment is not None:
552 morsel.comment = bytes_(comment)
553 if samesite is not None:
554 morsel.samesite = samesite
555 return morsel.serialize()
557class JSONSerializer(object):
558 """ A serializer which uses `json.dumps`` and ``json.loads``"""
559 def dumps(self, appstruct):
560 return bytes_(json.dumps(appstruct), encoding='utf-8')
562 def loads(self, bstruct):
563 # NB: json.loads raises ValueError if no json object can be decoded
564 # so we don't have to do it explicitly here.
565 return json.loads(text_(bstruct, encoding='utf-8'))
567class Base64Serializer(object):
568 """ A serializer which uses base64 to encode/decode data"""
570 def __init__(self, serializer=None):
571 if serializer is None:
572 serializer = JSONSerializer()
574 self.serializer = serializer
576 def dumps(self, appstruct):
577 """
578 Given an ``appstruct``, serialize and sign the data.
580 Returns a bytestring.
581 """
582 cstruct = self.serializer.dumps(appstruct) # will be bytes
583 return base64.urlsafe_b64encode(cstruct)
585 def loads(self, bstruct):
586 """
587 Given a ``bstruct`` (a bytestring), verify the signature and then
588 deserialize and return the deserialized value.
590 A ``ValueError`` will be raised if the signature fails to validate.
591 """
592 try:
593 cstruct = base64.urlsafe_b64decode(bytes_(bstruct))
594 except (binascii.Error, TypeError) as e:
595 raise ValueError('Badly formed base64 data: %s' % e)
597 return self.serializer.loads(cstruct)
599class SignedSerializer(object):
600 """
601 A helper to cryptographically sign arbitrary content using HMAC.
603 The serializer accepts arbitrary functions for performing the actual
604 serialization and deserialization.
606 ``secret``
607 A string which is used to sign the cookie. The secret should be at
608 least as long as the block size of the selected hash algorithm. For
609 ``sha512`` this would mean a 512 bit (64 character) secret.
611 ``salt``
612 A namespace to avoid collisions between different uses of a shared
613 secret.
615 ``hashalg``
616 The HMAC digest algorithm to use for signing. The algorithm must be
617 supported by the :mod:`hashlib` library. Default: ``'sha512'``.
619 ``serializer``
620 An object with two methods: `loads`` and ``dumps``. The ``loads`` method
621 should accept bytes and return a Python object. The ``dumps`` method
622 should accept a Python object and return bytes. A ``ValueError`` should
623 be raised for malformed inputs. Default: ``None`, which will use a
624 derivation of :func:`json.dumps` and ``json.loads``.
626 """
628 def __init__(self,
629 secret,
630 salt,
631 hashalg='sha512',
632 serializer=None,
633 ):
634 self.salt = salt
635 self.secret = secret
636 self.hashalg = hashalg
638 try:
639 # bwcompat with webob <= 1.3.1, leave latin-1 as the default
640 self.salted_secret = bytes_(salt or '') + bytes_(secret)
641 except UnicodeEncodeError:
642 self.salted_secret = (
643 bytes_(salt or '', 'utf-8') + bytes_(secret, 'utf-8'))
645 self.digestmod = lambda string=b'': hashlib.new(self.hashalg, string)
646 self.digest_size = self.digestmod().digest_size
648 if serializer is None:
649 serializer = JSONSerializer()
651 self.serializer = serializer
653 def dumps(self, appstruct):
654 """
655 Given an ``appstruct``, serialize and sign the data.
657 Returns a bytestring.
658 """
659 cstruct = self.serializer.dumps(appstruct) # will be bytes
660 sig = hmac.new(self.salted_secret, cstruct, self.digestmod).digest()
661 return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=')
663 def loads(self, bstruct):
664 """
665 Given a ``bstruct`` (a bytestring), verify the signature and then
666 deserialize and return the deserialized value.
668 A ``ValueError`` will be raised if the signature fails to validate.
669 """
670 try:
671 b64padding = b'=' * (-len(bstruct) % 4)
672 fstruct = base64.urlsafe_b64decode(bytes_(bstruct) + b64padding)
673 except (binascii.Error, TypeError) as e:
674 raise ValueError('Badly formed base64 data: %s' % e)
676 cstruct = fstruct[self.digest_size:]
677 expected_sig = fstruct[:self.digest_size]
679 sig = hmac.new(
680 self.salted_secret, bytes_(cstruct), self.digestmod).digest()
682 if strings_differ(sig, expected_sig):
683 raise ValueError('Invalid signature')
685 return self.serializer.loads(cstruct)
688_default = object()
690class CookieProfile(object):
691 """
692 A helper class that helps bring some sanity to the insanity that is cookie
693 handling.
695 The helper is capable of generating multiple cookies if necessary to
696 support subdomains and parent domains.
698 ``cookie_name``
699 The name of the cookie used for sessioning. Default: ``'session'``.
701 ``max_age``
702 The maximum age of the cookie used for sessioning (in seconds).
703 Default: ``None`` (browser scope).
705 ``secure``
706 The 'secure' flag of the session cookie. Default: ``False``.
708 ``httponly``
709 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
710 session cookie. Default: ``False``.
712 ``samesite``
713 The 'SameSite' attribute of the cookie, can be either ``b"strict"``,
714 ``b"lax"``, ``b"none"``, or ``None``.
716 For more information please see the ``samesite`` documentation in
717 :meth:`webob.cookies.make_cookie`
719 ``path``
720 The path used for the session cookie. Default: ``'/'``.
722 ``domains``
723 The domain(s) used for the session cookie. Default: ``None`` (no domain).
724 Can be passed an iterable containing multiple domains, this will set
725 multiple cookies one for each domain.
727 ``serializer``
728 An object with two methods: ``loads`` and ``dumps``. The ``loads`` method
729 should accept a bytestring and return a Python object. The ``dumps``
730 method should accept a Python object and return bytes. A ``ValueError``
731 should be raised for malformed inputs. Default: ``None``, which will use
732 a derivation of :func:`json.dumps` and :func:`json.loads`.
734 """
736 def __init__(self,
737 cookie_name,
738 secure=False,
739 max_age=None,
740 httponly=None,
741 samesite=None,
742 path='/',
743 domains=None,
744 serializer=None
745 ):
746 self.cookie_name = cookie_name
747 self.secure = secure
748 self.max_age = max_age
749 self.httponly = httponly
750 self.samesite = samesite
751 self.path = path
752 self.domains = domains
754 if serializer is None:
755 serializer = Base64Serializer()
757 self.serializer = serializer
758 self.request = None
760 def __call__(self, request):
761 """ Bind a request to a copy of this instance and return it"""
763 return self.bind(request)
765 def bind(self, request):
766 """ Bind a request to a copy of this instance and return it"""
768 selfish = CookieProfile(
769 self.cookie_name,
770 self.secure,
771 self.max_age,
772 self.httponly,
773 self.samesite,
774 self.path,
775 self.domains,
776 self.serializer,
777 )
778 selfish.request = request
779 return selfish
781 def get_value(self):
782 """ Looks for a cookie by name in the currently bound request, and
783 returns its value. If the cookie profile is not bound to a request,
784 this method will raise a :exc:`ValueError`.
786 Looks for the cookie in the cookies jar, and if it can find it it will
787 attempt to deserialize it. Returns ``None`` if there is no cookie or
788 if the value in the cookie cannot be successfully deserialized.
789 """
791 if not self.request:
792 raise ValueError('No request bound to cookie profile')
794 cookie = self.request.cookies.get(self.cookie_name)
796 if cookie is not None:
797 try:
798 return self.serializer.loads(bytes_(cookie))
799 except ValueError:
800 return None
802 def set_cookies(self, response, value, domains=_default, max_age=_default,
803 path=_default, secure=_default, httponly=_default,
804 samesite=_default):
805 """ Set the cookies on a response."""
806 cookies = self.get_headers(
807 value,
808 domains=domains,
809 max_age=max_age,
810 path=path,
811 secure=secure,
812 httponly=httponly,
813 samesite=samesite,
814 )
815 response.headerlist.extend(cookies)
816 return response
818 def get_headers(self, value, domains=_default, max_age=_default,
819 path=_default, secure=_default, httponly=_default,
820 samesite=_default):
821 """ Retrieve raw headers for setting cookies.
823 Returns a list of headers that should be set for the cookies to
824 be correctly tracked.
825 """
826 if value is None:
827 max_age = 0
828 bstruct = None
829 else:
830 bstruct = self.serializer.dumps(value)
832 return self._get_cookies(
833 bstruct,
834 domains=domains,
835 max_age=max_age,
836 path=path,
837 secure=secure,
838 httponly=httponly,
839 samesite=samesite,
840 )
842 def _get_cookies(self, value, domains, max_age, path, secure, httponly,
843 samesite):
844 """Internal function
846 This returns a list of cookies that are valid HTTP Headers.
848 :environ: The request environment
849 :value: The value to store in the cookie
850 :domains: The domains, overrides any set in the CookieProfile
851 :max_age: The max_age, overrides any set in the CookieProfile
852 :path: The path, overrides any set in the CookieProfile
853 :secure: Set this cookie to secure, overrides any set in CookieProfile
854 :httponly: Set this cookie to HttpOnly, overrides any set in CookieProfile
855 :samesite: Set this cookie to be for only the same site, overrides any
856 set in CookieProfile.
858 """
860 # If the user doesn't provide values, grab the defaults
861 if domains is _default:
862 domains = self.domains
864 if max_age is _default:
865 max_age = self.max_age
867 if path is _default:
868 path = self.path
870 if secure is _default:
871 secure = self.secure
873 if httponly is _default:
874 httponly = self.httponly
876 if samesite is _default:
877 samesite = self.samesite
879 # Length selected based upon http://browsercookielimits.x64.me
880 if value is not None and len(value) > 4093:
881 raise ValueError(
882 'Cookie value is too long to store (%s bytes)' %
883 len(value)
884 )
886 cookies = []
888 if not domains:
889 cookievalue = make_cookie(
890 self.cookie_name,
891 value,
892 path=path,
893 max_age=max_age,
894 httponly=httponly,
895 samesite=samesite,
896 secure=secure
897 )
898 cookies.append(('Set-Cookie', cookievalue))
900 else:
901 for domain in domains:
902 cookievalue = make_cookie(
903 self.cookie_name,
904 value,
905 path=path,
906 domain=domain,
907 max_age=max_age,
908 httponly=httponly,
909 samesite=samesite,
910 secure=secure,
911 )
912 cookies.append(('Set-Cookie', cookievalue))
914 return cookies
917class SignedCookieProfile(CookieProfile):
918 """
919 A helper for generating cookies that are signed to prevent tampering.
921 By default this will create a single cookie, given a value it will
922 serialize it, then use HMAC to cryptographically sign the data. Finally
923 the result is base64-encoded for transport. This way a remote user can
924 not tamper with the value without uncovering the secret/salt used.
926 ``secret``
927 A string which is used to sign the cookie. The secret should be at
928 least as long as the block size of the selected hash algorithm. For
929 ``sha512`` this would mean a 512 bit (64 character) secret.
931 ``salt``
932 A namespace to avoid collisions between different uses of a shared
933 secret.
935 ``hashalg``
936 The HMAC digest algorithm to use for signing. The algorithm must be
937 supported by the :mod:`hashlib` library. Default: ``'sha512'``.
939 ``cookie_name``
940 The name of the cookie used for sessioning. Default: ``'session'``.
942 ``max_age``
943 The maximum age of the cookie used for sessioning (in seconds).
944 Default: ``None`` (browser scope).
946 ``secure``
947 The 'secure' flag of the session cookie. Default: ``False``.
949 ``httponly``
950 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
951 session cookie. Default: ``False``.
953 ``samesite``
954 The 'SameSite' attribute of the cookie, can be either ``b"strict"``,
955 ``b"lax"``, ``b"none"``, or ``None``.
957 ``path``
958 The path used for the session cookie. Default: ``'/'``.
960 ``domains``
961 The domain(s) used for the session cookie. Default: ``None`` (no domain).
962 Can be passed an iterable containing multiple domains, this will set
963 multiple cookies one for each domain.
965 ``serializer``
966 An object with two methods: `loads`` and ``dumps``. The ``loads`` method
967 should accept bytes and return a Python object. The ``dumps`` method
968 should accept a Python object and return bytes. A ``ValueError`` should
969 be raised for malformed inputs. Default: ``None`, which will use a
970 derivation of :func:`json.dumps` and ``json.loads``.
971 """
972 def __init__(self,
973 secret,
974 salt,
975 cookie_name,
976 secure=False,
977 max_age=None,
978 httponly=False,
979 samesite=None,
980 path="/",
981 domains=None,
982 hashalg='sha512',
983 serializer=None,
984 ):
985 self.secret = secret
986 self.salt = salt
987 self.hashalg = hashalg
988 self.original_serializer = serializer
990 signed_serializer = SignedSerializer(
991 secret,
992 salt,
993 hashalg,
994 serializer=self.original_serializer,
995 )
996 CookieProfile.__init__(
997 self,
998 cookie_name,
999 secure=secure,
1000 max_age=max_age,
1001 httponly=httponly,
1002 samesite=samesite,
1003 path=path,
1004 domains=domains,
1005 serializer=signed_serializer,
1006 )
1008 def bind(self, request):
1009 """ Bind a request to a copy of this instance and return it"""
1011 selfish = SignedCookieProfile(
1012 self.secret,
1013 self.salt,
1014 self.cookie_name,
1015 self.secure,
1016 self.max_age,
1017 self.httponly,
1018 self.samesite,
1019 self.path,
1020 self.domains,
1021 self.hashalg,
1022 self.original_serializer,
1023 )
1024 selfish.request = request
1025 return selfish