• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import collections
2
3import base64
4import binascii
5import hashlib
6import hmac
7import json
8from datetime import (
9    date,
10    datetime,
11    timedelta,
12    )
13import re
14import string
15import time
16import warnings
17
18from webob.compat import (
19    PY3,
20    text_type,
21    bytes_,
22    text_,
23    native_,
24    string_types,
25    )
26
27from webob.util import strings_differ
28
29__all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer',
30           'JSONSerializer', 'Base64Serializer', 'make_cookie']
31
32_marker = object()
33
34class 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
163class 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
199def _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
205def 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
212def 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
217def 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
224def 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
240class 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
318def _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
324def _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))
364if 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
371weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
372months = (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
379def __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
387def _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
405def _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
413def _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
430def 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
470class 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
480class 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
512class 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
603class 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
808class 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