Home | History | Annotate | Download | only in webob
      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