Chris McDonough
2013-12-07 8134a723b12221663babc855ccc941daf88ba5c3
use a single serializer instead of serialize/deserialize in session.py, use SignedSerializer from (yet to be released) webob 1.3 instead of local logic in session.py
2 files modified
131 ■■■■ changed files
pyramid/session.py 104 ●●●● patch | view | raw | blame | history
pyramid/tests/test_session.py 27 ●●●●● patch | view | raw | blame | history
pyramid/session.py
@@ -8,6 +8,8 @@
from zope.deprecation import deprecated
from zope.interface import implementer
from webob.cookies import SignedSerializer
from pyramid.compat import (
    pickle,
    PY3,
@@ -119,9 +121,17 @@
        return False
    return True
class PickleSerializer(object):
    """ A Webob cookie serializer that uses the pickle protocol to dump Python
    data to bytes."""
    def loads(self, bstruct):
        return pickle.loads(bstruct)
    def dumps(self, appstruct):
        return pickle.dumps(appstruct, pickle.HIGHEST_PROTOCOL)
def BaseCookieSessionFactory(
    serialize,
    deserialize,
    serializer,
    cookie_name='session',
    max_age=None,
    path='/',
@@ -154,13 +164,11 @@
    Parameters:
    ``serialize``
      A callable accepting a Python object and returning a bytestring. A
      ``ValueError`` should be raised for malformed inputs.
    ``deserialize``
      A callable accepting a bytestring and returning a Python object. A
      ``ValueError`` should be raised for malformed inputs.
    ``serializer``
      An object with two methods: `loads`` and ``dumps``.  The ``loads`` method
      should accept bytes and return a Python object.  The ``dumps`` method
      should accept a Python object and return bytes.  A ``ValueError`` should
      be raised for malformed inputs.
    ``cookie_name``
      The name of the cookie used for sessioning. Default: ``'session'``.
@@ -238,7 +246,7 @@
            cookieval = request.cookies.get(self._cookie_name)
            if cookieval is not None:
                try:
                    value = deserialize(bytes_(cookieval))
                    value = serializer.loads(bytes_(cookieval))
                except ValueError:
                    # the cookie failed to deserialize, dropped
                    value = None
@@ -336,7 +344,7 @@
                exception = getattr(self.request, 'exception', None)
                if exception is not None: # dont set a cookie during exceptions
                    return False
            cookieval = native_(serialize(
            cookieval = native_(serializer.dumps(
                (self.accessed, self.created, dict(self))
                ))
            if len(cookieval) > 4064:
@@ -430,9 +438,20 @@
      is valid. Default: ``signed_deserialize`` (using pickle).
    """
    class SerializerWrapper(object):
        def __init__(self, secret):
            self.secret = secret
        def loads(self, bstruct):
            return signed_deserialize(bstruct, secret)
        def dumps(self, appstruct):
            return signed_serialize(appstruct, secret)
    serializer = SerializerWrapper(secret)
    return BaseCookieSessionFactory(
        lambda v: signed_serialize(v, secret),
        lambda v: signed_deserialize(v, secret),
        serializer,
        cookie_name=cookie_name,
        max_age=cookie_max_age,
        path=cookie_path,
@@ -463,8 +482,7 @@
    reissue_time=0,
    hashalg='sha512',
    salt='pyramid.session.',
    serialize=None,
    deserialize=None,
    serializer=None,
    ):
    """
    .. versionadded:: 1.5
@@ -546,53 +564,27 @@
      If ``True``, set a session cookie even if an exception occurs
      while rendering a view. Default: ``True``.
    ``serialize``
      A callable accepting a Python object and returning a bytestring. A
      ``ValueError`` should be raised for malformed inputs.
      Default: :func:`pickle.dumps`.
    ``deserialize``
      A callable accepting a bytestring and returning a Python object. A
      ``ValueError`` should be raised for malformed inputs.
      Default: :func:`pickle.loads`.
    ``serializer``
      An object with two methods: `loads`` and ``dumps``.  The ``loads`` method
      should accept bytes and return a Python object.  The ``dumps`` method
      should accept a Python object and return bytes.  A ``ValueError`` should
      be raised for malformed inputs.  If a serializer is not passed, the
      :class:`pyramid.session.PickleSerializer` serializer will be used.
    .. versionadded: 1.5a3
    """
    if serializer is None:
        serializer = PickleSerializer()
    if serialize is None:
        serialize = lambda v: pickle.dumps(v, pickle.HIGHEST_PROTOCOL)
    if deserialize is None:
        deserialize = pickle.loads
    digestmod = lambda string=b'': hashlib.new(hashalg, string)
    digest_size = digestmod().digest_size
    salted_secret = bytes_(salt or '') + bytes_(secret)
    def signed_serialize(appstruct):
        cstruct = serialize(appstruct)
        sig = hmac.new(salted_secret, cstruct, digestmod).digest()
        return base64.b64encode(cstruct + sig)
    def signed_deserialize(bstruct):
        try:
            fstruct = base64.b64decode(bstruct)
        except (binascii.Error, TypeError) as e:
            raise ValueError('Badly formed base64 data: %s' % e)
        cstruct = fstruct[:-digest_size]
        expected_sig = fstruct[-digest_size:]
        sig = hmac.new(salted_secret, cstruct, digestmod).digest()
        if strings_differ(sig, expected_sig):
            raise ValueError('Invalid signature')
        return deserialize(cstruct)
    signed_serializer = SignedSerializer(
        secret,
        salt,
        hashalg,
        serializer=serializer,
        )
    return BaseCookieSessionFactory(
        signed_serialize,
        signed_deserialize,
        signed_serializer,
        cookie_name=cookie_name,
        max_age=max_age,
        path=path,
pyramid/tests/test_session.py
@@ -264,8 +264,8 @@
class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase):
    def _makeOne(self, request, **kw):
        from pyramid.session import BaseCookieSessionFactory
        return BaseCookieSessionFactory(
            dummy_serialize, dummy_deserialize, **kw)(request)
        serializer = DummySerializer()
        return BaseCookieSessionFactory(serializer, **kw)(request)
    def _serialize(self, value):
        return json.dumps(value)
@@ -294,7 +294,7 @@
        digestmod = lambda: hashlib.new(hashalg)
        cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
        sig = hmac.new(salt + b'secret', cstruct, digestmod).digest()
        return base64.b64encode(cstruct + sig)
        return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=')
    def test_reissue_not_triggered(self):
        import time
@@ -353,11 +353,12 @@
        import hmac
        import time
        request = testing.DummyRequest()
        cstruct = dummy_serialize((time.time(), 0, {'state': 1}))
        serializer = DummySerializer()
        cstruct = serializer.dumps((time.time(), 0, {'state': 1}))
        sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest()
        cookieval = base64.b64encode(cstruct + sig)
        cookieval = base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=')
        request.cookies['session'] = cookieval
        session = self._makeOne(request, deserialize=dummy_deserialize)
        session = self._makeOne(request, serializer=serializer)
        self.assertEqual(session['state'], 1)
    def test_invalid_data_size(self):
@@ -382,7 +383,7 @@
        try:
            result = callbacks[0](request, response)
        except TypeError as e: # pragma: no cover
        except TypeError: # pragma: no cover
            self.fail('HMAC failed to initialize due to key length.')
        self.assertEqual(result, None)
@@ -413,8 +414,9 @@
            kw.setdefault(dest, kw.pop(src))
    def _serialize(self, value):
        from pyramid.compat import bytes_
        from pyramid.session import signed_serialize
        return signed_serialize(value, 'secret')
        return bytes_(signed_serialize(value, 'secret'))
    def test_serialize_option(self):
        from pyramid.response import Response
@@ -596,11 +598,12 @@
        result = self._callFUT(request, 'csrf_token', raises=False)
        self.assertEqual(result, False)
def dummy_serialize(value):
    return json.dumps(value).encode('utf-8')
class DummySerializer(object):
    def dumps(self, value):
        return json.dumps(value).encode('utf-8')
def dummy_deserialize(value):
    return json.loads(value.decode('utf-8'))
    def loads(self, value):
        return json.loads(value.decode('utf-8'))
class DummySessionFactory(dict):
    _dirty = False