diff options
| -rw-r--r-- | pyramid/session.py | 104 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 27 |
2 files changed, 63 insertions, 68 deletions
diff --git a/pyramid/session.py b/pyramid/session.py index d3a4113b9..c9d738b9c 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -8,6 +8,8 @@ import time 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 @@ def check_csrf_token(request, 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 @@ def BaseCookieSessionFactory( 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 @@ def BaseCookieSessionFactory( 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 @@ def BaseCookieSessionFactory( 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 @@ def UnencryptedCookieSessionFactoryConfig( 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 @@ def SignedCookieSessionFactory( reissue_time=0, hashalg='sha512', salt='pyramid.session.', - serialize=None, - deserialize=None, + serializer=None, ): """ .. versionadded:: 1.5 @@ -546,53 +564,27 @@ def SignedCookieSessionFactory( 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, diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index a9f70d6a0..1ad0729b3 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -264,8 +264,8 @@ class SharedCookieSessionTests(object): 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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): 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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): 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 @@ class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): 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 @@ class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase): 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 @@ class Test_check_csrf_token(unittest.TestCase): 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 |
