You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							323 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
	
	
							323 lines
						
					
					
						
							12 KiB
						
					
					
				# -*- coding: utf-8 -*-
 | 
						|
r"""
 | 
						|
    werkzeug.contrib.securecookie
 | 
						|
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | 
						|
 | 
						|
    This module implements a cookie that is not alterable from the client
 | 
						|
    because it adds a checksum the server checks for.  You can use it as
 | 
						|
    session replacement if all you have is a user id or something to mark
 | 
						|
    a logged in user.
 | 
						|
 | 
						|
    Keep in mind that the data is still readable from the client as a
 | 
						|
    normal cookie is.  However you don't have to store and flush the
 | 
						|
    sessions you have at the server.
 | 
						|
 | 
						|
    Example usage:
 | 
						|
 | 
						|
    >>> from werkzeug.contrib.securecookie import SecureCookie
 | 
						|
    >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
 | 
						|
 | 
						|
    Dumping into a string so that one can store it in a cookie:
 | 
						|
 | 
						|
    >>> value = x.serialize()
 | 
						|
 | 
						|
    Loading from that string again:
 | 
						|
 | 
						|
    >>> x = SecureCookie.unserialize(value, "deadbeef")
 | 
						|
    >>> x["baz"]
 | 
						|
    (1, 2, 3)
 | 
						|
 | 
						|
    If someone modifies the cookie and the checksum is wrong the unserialize
 | 
						|
    method will fail silently and return a new empty `SecureCookie` object.
 | 
						|
 | 
						|
    Keep in mind that the values will be visible in the cookie so do not
 | 
						|
    store data in a cookie you don't want the user to see.
 | 
						|
 | 
						|
    Application Integration
 | 
						|
    =======================
 | 
						|
 | 
						|
    If you are using the werkzeug request objects you could integrate the
 | 
						|
    secure cookie into your application like this::
 | 
						|
 | 
						|
        from werkzeug.utils import cached_property
 | 
						|
        from werkzeug.wrappers import BaseRequest
 | 
						|
        from werkzeug.contrib.securecookie import SecureCookie
 | 
						|
 | 
						|
        # don't use this key but a different one; you could just use
 | 
						|
        # os.urandom(20) to get something random
 | 
						|
        SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea'
 | 
						|
 | 
						|
        class Request(BaseRequest):
 | 
						|
 | 
						|
            @cached_property
 | 
						|
            def client_session(self):
 | 
						|
                data = self.cookies.get('session_data')
 | 
						|
                if not data:
 | 
						|
                    return SecureCookie(secret_key=SECRET_KEY)
 | 
						|
                return SecureCookie.unserialize(data, SECRET_KEY)
 | 
						|
 | 
						|
        def application(environ, start_response):
 | 
						|
            request = Request(environ)
 | 
						|
 | 
						|
            # get a response object here
 | 
						|
            response = ...
 | 
						|
 | 
						|
            if request.client_session.should_save:
 | 
						|
                session_data = request.client_session.serialize()
 | 
						|
                response.set_cookie('session_data', session_data,
 | 
						|
                                    httponly=True)
 | 
						|
            return response(environ, start_response)
 | 
						|
 | 
						|
    A less verbose integration can be achieved by using shorthand methods::
 | 
						|
 | 
						|
        class Request(BaseRequest):
 | 
						|
 | 
						|
            @cached_property
 | 
						|
            def client_session(self):
 | 
						|
                return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET)
 | 
						|
 | 
						|
        def application(environ, start_response):
 | 
						|
            request = Request(environ)
 | 
						|
 | 
						|
            # get a response object here
 | 
						|
            response = ...
 | 
						|
 | 
						|
            request.client_session.save_cookie(response)
 | 
						|
            return response(environ, start_response)
 | 
						|
 | 
						|
    :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
 | 
						|
    :license: BSD, see LICENSE for more details.
 | 
						|
"""
 | 
						|
import pickle
 | 
						|
import base64
 | 
						|
from hmac import new as hmac
 | 
						|
from time import time
 | 
						|
from hashlib import sha1 as _default_hash
 | 
						|
 | 
						|
from werkzeug._compat import iteritems, text_type, to_bytes
 | 
						|
from werkzeug.urls import url_quote_plus, url_unquote_plus
 | 
						|
from werkzeug._internal import _date_to_unix
 | 
						|
from werkzeug.contrib.sessions import ModificationTrackingDict
 | 
						|
from werkzeug.security import safe_str_cmp
 | 
						|
from werkzeug._compat import to_native
 | 
						|
 | 
						|
 | 
						|
class UnquoteError(Exception):
 | 
						|
 | 
						|
    """Internal exception used to signal failures on quoting."""
 | 
						|
 | 
						|
 | 
						|
class SecureCookie(ModificationTrackingDict):
 | 
						|
 | 
						|
    """Represents a secure cookie.  You can subclass this class and provide
 | 
						|
    an alternative mac method.  The import thing is that the mac method
 | 
						|
    is a function with a similar interface to the hashlib.  Required
 | 
						|
    methods are update() and digest().
 | 
						|
 | 
						|
    Example usage:
 | 
						|
 | 
						|
    >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
 | 
						|
    >>> x["foo"]
 | 
						|
    42
 | 
						|
    >>> x["baz"]
 | 
						|
    (1, 2, 3)
 | 
						|
    >>> x["blafasel"] = 23
 | 
						|
    >>> x.should_save
 | 
						|
    True
 | 
						|
 | 
						|
    :param data: the initial data.  Either a dict, list of tuples or `None`.
 | 
						|
    :param secret_key: the secret key.  If not set `None` or not specified
 | 
						|
                       it has to be set before :meth:`serialize` is called.
 | 
						|
    :param new: The initial value of the `new` flag.
 | 
						|
    """
 | 
						|
 | 
						|
    #: The hash method to use.  This has to be a module with a new function
 | 
						|
    #: or a function that creates a hashlib object.  Such as `hashlib.md5`
 | 
						|
    #: Subclasses can override this attribute.  The default hash is sha1.
 | 
						|
    #: Make sure to wrap this in staticmethod() if you store an arbitrary
 | 
						|
    #: function there such as hashlib.sha1 which  might be implemented
 | 
						|
    #: as a function.
 | 
						|
    hash_method = staticmethod(_default_hash)
 | 
						|
 | 
						|
    #: the module used for serialization.  Unless overriden by subclasses
 | 
						|
    #: the standard pickle module is used.
 | 
						|
    serialization_method = pickle
 | 
						|
 | 
						|
    #: if the contents should be base64 quoted.  This can be disabled if the
 | 
						|
    #: serialization process returns cookie safe strings only.
 | 
						|
    quote_base64 = True
 | 
						|
 | 
						|
    def __init__(self, data=None, secret_key=None, new=True):
 | 
						|
        ModificationTrackingDict.__init__(self, data or ())
 | 
						|
        # explicitly convert it into a bytestring because python 2.6
 | 
						|
        # no longer performs an implicit string conversion on hmac
 | 
						|
        if secret_key is not None:
 | 
						|
            secret_key = to_bytes(secret_key, 'utf-8')
 | 
						|
        self.secret_key = secret_key
 | 
						|
        self.new = new
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return '<%s %s%s>' % (
 | 
						|
            self.__class__.__name__,
 | 
						|
            dict.__repr__(self),
 | 
						|
            self.should_save and '*' or ''
 | 
						|
        )
 | 
						|
 | 
						|
    @property
 | 
						|
    def should_save(self):
 | 
						|
        """True if the session should be saved.  By default this is only true
 | 
						|
        for :attr:`modified` cookies, not :attr:`new`.
 | 
						|
        """
 | 
						|
        return self.modified
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def quote(cls, value):
 | 
						|
        """Quote the value for the cookie.  This can be any object supported
 | 
						|
        by :attr:`serialization_method`.
 | 
						|
 | 
						|
        :param value: the value to quote.
 | 
						|
        """
 | 
						|
        if cls.serialization_method is not None:
 | 
						|
            value = cls.serialization_method.dumps(value)
 | 
						|
        if cls.quote_base64:
 | 
						|
            value = b''.join(base64.b64encode(value).splitlines()).strip()
 | 
						|
        return value
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def unquote(cls, value):
 | 
						|
        """Unquote the value for the cookie.  If unquoting does not work a
 | 
						|
        :exc:`UnquoteError` is raised.
 | 
						|
 | 
						|
        :param value: the value to unquote.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            if cls.quote_base64:
 | 
						|
                value = base64.b64decode(value)
 | 
						|
            if cls.serialization_method is not None:
 | 
						|
                value = cls.serialization_method.loads(value)
 | 
						|
            return value
 | 
						|
        except Exception:
 | 
						|
            # unfortunately pickle and other serialization modules can
 | 
						|
            # cause pretty every error here.  if we get one we catch it
 | 
						|
            # and convert it into an UnquoteError
 | 
						|
            raise UnquoteError()
 | 
						|
 | 
						|
    def serialize(self, expires=None):
 | 
						|
        """Serialize the secure cookie into a string.
 | 
						|
 | 
						|
        If expires is provided, the session will be automatically invalidated
 | 
						|
        after expiration when you unseralize it. This provides better
 | 
						|
        protection against session cookie theft.
 | 
						|
 | 
						|
        :param expires: an optional expiration date for the cookie (a
 | 
						|
                        :class:`datetime.datetime` object)
 | 
						|
        """
 | 
						|
        if self.secret_key is None:
 | 
						|
            raise RuntimeError('no secret key defined')
 | 
						|
        if expires:
 | 
						|
            self['_expires'] = _date_to_unix(expires)
 | 
						|
        result = []
 | 
						|
        mac = hmac(self.secret_key, None, self.hash_method)
 | 
						|
        for key, value in sorted(self.items()):
 | 
						|
            result.append(('%s=%s' % (
 | 
						|
                url_quote_plus(key),
 | 
						|
                self.quote(value).decode('ascii')
 | 
						|
            )).encode('ascii'))
 | 
						|
            mac.update(b'|' + result[-1])
 | 
						|
        return b'?'.join([
 | 
						|
            base64.b64encode(mac.digest()).strip(),
 | 
						|
            b'&'.join(result)
 | 
						|
        ])
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def unserialize(cls, string, secret_key):
 | 
						|
        """Load the secure cookie from a serialized string.
 | 
						|
 | 
						|
        :param string: the cookie value to unserialize.
 | 
						|
        :param secret_key: the secret key used to serialize the cookie.
 | 
						|
        :return: a new :class:`SecureCookie`.
 | 
						|
        """
 | 
						|
        if isinstance(string, text_type):
 | 
						|
            string = string.encode('utf-8', 'replace')
 | 
						|
        if isinstance(secret_key, text_type):
 | 
						|
            secret_key = secret_key.encode('utf-8', 'replace')
 | 
						|
        try:
 | 
						|
            base64_hash, data = string.split(b'?', 1)
 | 
						|
        except (ValueError, IndexError):
 | 
						|
            items = ()
 | 
						|
        else:
 | 
						|
            items = {}
 | 
						|
            mac = hmac(secret_key, None, cls.hash_method)
 | 
						|
            for item in data.split(b'&'):
 | 
						|
                mac.update(b'|' + item)
 | 
						|
                if b'=' not in item:
 | 
						|
                    items = None
 | 
						|
                    break
 | 
						|
                key, value = item.split(b'=', 1)
 | 
						|
                # try to make the key a string
 | 
						|
                key = url_unquote_plus(key.decode('ascii'))
 | 
						|
                try:
 | 
						|
                    key = to_native(key)
 | 
						|
                except UnicodeError:
 | 
						|
                    pass
 | 
						|
                items[key] = value
 | 
						|
 | 
						|
            # no parsing error and the mac looks okay, we can now
 | 
						|
            # sercurely unpickle our cookie.
 | 
						|
            try:
 | 
						|
                client_hash = base64.b64decode(base64_hash)
 | 
						|
            except TypeError:
 | 
						|
                items = client_hash = None
 | 
						|
            if items is not None and safe_str_cmp(client_hash, mac.digest()):
 | 
						|
                try:
 | 
						|
                    for key, value in iteritems(items):
 | 
						|
                        items[key] = cls.unquote(value)
 | 
						|
                except UnquoteError:
 | 
						|
                    items = ()
 | 
						|
                else:
 | 
						|
                    if '_expires' in items:
 | 
						|
                        if time() > items['_expires']:
 | 
						|
                            items = ()
 | 
						|
                        else:
 | 
						|
                            del items['_expires']
 | 
						|
            else:
 | 
						|
                items = ()
 | 
						|
        return cls(items, secret_key, False)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def load_cookie(cls, request, key='session', secret_key=None):
 | 
						|
        """Loads a :class:`SecureCookie` from a cookie in request.  If the
 | 
						|
        cookie is not set, a new :class:`SecureCookie` instanced is
 | 
						|
        returned.
 | 
						|
 | 
						|
        :param request: a request object that has a `cookies` attribute
 | 
						|
                        which is a dict of all cookie values.
 | 
						|
        :param key: the name of the cookie.
 | 
						|
        :param secret_key: the secret key used to unquote the cookie.
 | 
						|
                           Always provide the value even though it has
 | 
						|
                           no default!
 | 
						|
        """
 | 
						|
        data = request.cookies.get(key)
 | 
						|
        if not data:
 | 
						|
            return cls(secret_key=secret_key)
 | 
						|
        return cls.unserialize(data, secret_key)
 | 
						|
 | 
						|
    def save_cookie(self, response, key='session', expires=None,
 | 
						|
                    session_expires=None, max_age=None, path='/', domain=None,
 | 
						|
                    secure=None, httponly=False, force=False):
 | 
						|
        """Saves the SecureCookie in a cookie on response object.  All
 | 
						|
        parameters that are not described here are forwarded directly
 | 
						|
        to :meth:`~BaseResponse.set_cookie`.
 | 
						|
 | 
						|
        :param response: a response object that has a
 | 
						|
                         :meth:`~BaseResponse.set_cookie` method.
 | 
						|
        :param key: the name of the cookie.
 | 
						|
        :param session_expires: the expiration date of the secure cookie
 | 
						|
                                stored information.  If this is not provided
 | 
						|
                                the cookie `expires` date is used instead.
 | 
						|
        """
 | 
						|
        if force or self.should_save:
 | 
						|
            data = self.serialize(session_expires or expires)
 | 
						|
            response.set_cookie(key, data, expires=expires, max_age=max_age,
 | 
						|
                                path=path, domain=domain, secure=secure,
 | 
						|
                                httponly=httponly)
 | 
						|
 |