From cf46a12292cf15303d68d27c6ba4155ecc2b4586 Mon Sep 17 00:00:00 2001 From: Ian Wilson Date: Sun, 3 Jun 2012 13:01:15 -0700 Subject: Expose signed_serialize and signed_deserialize in UnencryptedCookieSessionFactoryConfig. --- pyramid/session.py | 106 +++++++++++++++++++++++------------------- pyramid/tests/test_session.py | 42 +++++++++++++++++ 2 files changed, 100 insertions(+), 48 deletions(-) diff --git a/pyramid/session.py b/pyramid/session.py index 76b2b30b1..40e21ddbc 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -33,6 +33,53 @@ def manage_accessed(wrapped): accessed.__doc__ = wrapped.__doc__ return accessed +def signed_serialize(data, secret): + """ Serialize any pickleable structure (``data``) and sign it + using the ``secret`` (must be a string). Return the + serialization, which includes the signature as its first 40 bytes. + The ``signed_deserialize`` method will deserialize such a value. + + This function is useful for creating signed cookies. For example: + + .. code-block:: python + + cookieval = signed_serialize({'a':1}, 'secret') + response.set_cookie('signed_cookie', cookieval) + """ + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +def signed_deserialize(serialized, secret, hmac=hmac): + """ Deserialize the value returned from ``signed_serialize``. If + the value cannot be deserialized for any reason, a + :exc:`ValueError` exception will be raised. + + This function is useful for deserializing a signed cookie value + created by ``signed_serialize``. For example: + + .. code-block:: python + + cookieval = request.cookies['signed_cookie'] + data = signed_deserialize(cookieval, 'secret') + """ + # hmac parameterized only for unit tests + try: + input_sig, pickled = (serialized[:40], + base64.b64decode(bytes_(serialized[40:]))) + except (binascii.Error, TypeError) as e: + # Badly formed data can make base64 die + raise ValueError('Badly formed base64 data: %s' % e) + + sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') + + return pickle.loads(pickled) + def UnencryptedCookieSessionFactoryConfig( secret, timeout=1200, @@ -43,6 +90,8 @@ def UnencryptedCookieSessionFactoryConfig( cookie_secure=False, cookie_httponly=False, cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, ): """ Configure a :term:`session factory` which will provide unencrypted @@ -89,6 +138,15 @@ def UnencryptedCookieSessionFactoryConfig( If ``True``, set a session cookie even if an exception occurs while rendering a view. Default: ``True``. + ``signed_serialize`` + A callable which takes more or less arbitrary python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). """ @implementer(ISession) @@ -225,51 +283,3 @@ def UnencryptedCookieSessionFactoryConfig( return True return UnencryptedCookieSessionFactory - -def signed_serialize(data, secret): - """ Serialize any pickleable structure (``data``) and sign it - using the ``secret`` (must be a string). Return the - serialization, which includes the signature as its first 40 bytes. - The ``signed_deserialize`` method will deserialize such a value. - - This function is useful for creating signed cookies. For example: - - .. code-block:: python - - cookieval = signed_serialize({'a':1}, 'secret') - response.set_cookie('signed_cookie', cookieval) - """ - pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - return sig + native_(base64.b64encode(pickled)) - -def signed_deserialize(serialized, secret, hmac=hmac): - """ Deserialize the value returned from ``signed_serialize``. If - the value cannot be deserialized for any reason, a - :exc:`ValueError` exception will be raised. - - This function is useful for deserializing a signed cookie value - created by ``signed_serialize``. For example: - - .. code-block:: python - - cookieval = request.cookies['signed_cookie'] - data = signed_deserialize(cookieval, 'secret') - """ - # hmac parameterized only for unit tests - try: - input_sig, pickled = (serialized[:40], - base64.b64decode(bytes_(serialized[40:]))) - except (binascii.Error, TypeError) as e: - # Badly formed data can make base64 die - raise ValueError('Badly formed base64 data: %s' % e) - - sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest() - - # Avoid timing attacks (see - # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) - if strings_differ(sig, input_sig): - raise ValueError('Invalid signature') - - return pickle.loads(pickled) - diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 6d75c7950..5143b7a95 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -205,6 +205,48 @@ class TestUnencryptedCookieSession(unittest.TestCase): self.assertTrue(token) self.assertTrue('_csrft_' in session) + def test_serialize_option(self): + from pyramid.response import Response + secret = 'secret' + request = testing.DummyRequest() + session = self._makeOne(request, + signed_serialize=dummy_signed_serialize) + session['key'] = 'value' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookie = response.headerlist[-1][1] + expected_cookieval = dummy_signed_serialize( + (session.accessed, session.created, {'key': 'value'}), secret) + response = Response() + response.set_cookie('session', expected_cookieval) + expected_cookie = response.headerlist[-1][1] + self.assertEqual(cookie, expected_cookie) + + def test_deserialize_option(self): + import time + secret = 'secret' + request = testing.DummyRequest() + accessed = time.time() + state = {'key': 'value'} + cookieval = dummy_signed_serialize((accessed, accessed, state), secret) + request.cookies['session'] = cookieval + session = self._makeOne(request, + signed_deserialize=dummy_signed_deserialize) + self.assertEqual(dict(session), state) + +def dummy_signed_serialize(data, secret): + import base64 + from pyramid.compat import pickle, bytes_ + pickled = pickle.dumps(data) + return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) + +def dummy_signed_deserialize(serialized, secret): + import base64 + from pyramid.compat import pickle, bytes_ + serialized_data = base64.b64decode( + serialized[len(base64.b64encode(bytes_(secret))):]) + return pickle.loads(serialized_data) + class Test_manage_accessed(unittest.TestCase): def _makeOne(self, wrapped): from pyramid.session import manage_accessed -- cgit v1.2.3 From 34f00aede8634810164baaac0fd9214b683ff181 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Fri, 8 Jun 2012 12:31:50 +0200 Subject: RFC 2616 sec. 3.7 case insensitive match test Since "... The type, subtype, and parameter attribute names are case- insensitive..." the type and subtype in the accept parameter in add_view should be treated in a case insensitive way. --- pyramid/tests/test_config/test_views.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 9b46f83c9..4e1be2190 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -645,6 +645,26 @@ class TestViewsConfigurationMixin(unittest.TestCase): request.accept = DummyAccept('text/html', 'text/html') self.assertEqual(wrapper(None, request), 'OK2') + def test_add_view_mixed_case_replaces_existing_view(self): + from pyramid.renderers import null_renderer + def view(context, request): return 'OK' + def view2(context, request): return 'OK2' + def view3(context, request): return 'OK3' + def get_val(obj, key): + return obj.get(key) + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + config.add_view(view=view3, accept='text/HTML', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.media_views.items()),1) + self.assertFalse(wrapper.media_views.has_key('text/HTML')) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK3') + def test_add_views_with_accept_multiview_replaces_existing(self): from pyramid.renderers import null_renderer def view(context, request): return 'OK' -- cgit v1.2.3 From ed9663c861b5da5a684a3ebd26700e52e49ce39f Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Fri, 8 Jun 2012 13:32:55 +0200 Subject: Lowercase the accept parameter in add_view Fix the RFC 2616 sec. 3.7 compliance by storing a canonical cased version of the parameter. --- pyramid/config/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9e9b5321b..d54976988 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1001,6 +1001,9 @@ class ViewsConfiguratorMixin(object): # GET implies HEAD too request_method = as_sorted_tuple(request_method + ('HEAD',)) + if accept is not None: + accept = accept.lower() + order, predicates, phash = make_predicates(xhr=xhr, request_method=request_method, path_info=path_info, request_param=request_param, header=header, accept=accept, -- cgit v1.2.3 From ce5b5e4b842fe0e88d9ba5055b419b756723e8ec Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Wed, 13 Jun 2012 16:25:14 +0200 Subject: Accept the Contributor Agreement. as suggested by "Contributing Source Code and Documentation" document. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 98f73d5f9..dacc48765 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -172,3 +172,5 @@ Contributors - Wayne Witzel III, 2012/03/27 - Marin Rukavina, 2012/05/03 + +- Lorenzo M. Catucci, 2012/06/08 -- cgit v1.2.3 From 2cc0710ecf9b47e5a6f41caf6b1c56b29bab2db0 Mon Sep 17 00:00:00 2001 From: Ian Joseph Wilson Date: Sun, 17 Jun 2012 10:40:51 -0700 Subject: Sign contributors agreement. --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c3995aaba..5ccac50d3 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -176,3 +176,5 @@ Contributors - Marc Abramowitz, 2012/06/13 - Jeff Cook, 2012/06/16 + +- Ian Wilson, 2012/06/17 -- cgit v1.2.3 From 4aeb44f1ea912c3c3611bc677f654602488fedb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Fidosz?= Date: Fri, 3 Aug 2012 22:56:49 +0200 Subject: fixed indentation in narr/helloworld.py --- docs/narr/helloworld.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/narr/helloworld.py b/docs/narr/helloworld.py index 7c26c8cdc..c01329af9 100644 --- a/docs/narr/helloworld.py +++ b/docs/narr/helloworld.py @@ -2,14 +2,15 @@ from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response + def hello_world(request): - return Response('Hello %(name)s!' % request.matchdict) + return Response('Hello %(name)s!' % request.matchdict) if __name__ == '__main__': - config = Configurator() - config.add_route('hello', '/hello/{name}') - config.add_view(hello_world, route_name='hello') - app = config.make_wsgi_app() - server = make_server('0.0.0.0', 8080, app) - server.serve_forever() + config = Configurator() + config.add_route('hello', '/hello/{name}') + config.add_view(hello_world, route_name='hello') + app = config.make_wsgi_app() + server = make_server('0.0.0.0', 8080, app) + server.serve_forever() -- cgit v1.2.3 From a0243a5264371a3e69fd188cee17c6fbfd8341a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Fidosz?= Date: Sun, 5 Aug 2012 10:33:07 +0200 Subject: fixed line numbers in firstapp --- docs/narr/firstapp.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index a86826d86..dbdc48549 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -127,7 +127,7 @@ defined imports and function definitions, placed within the confines of an .. literalinclude:: helloworld.py :linenos: - :lines: 8-13 + :lines: 9-15 Let's break this down piece-by-piece. @@ -136,7 +136,7 @@ Configurator Construction .. literalinclude:: helloworld.py :linenos: - :lines: 8-9 + :lines: 9-10 The ``if __name__ == '__main__':`` line in the code sample above represents a Python idiom: the code inside this if clause is not invoked unless the script @@ -169,7 +169,7 @@ Adding Configuration .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 10-11 + :lines: 11-12 First line above calls the :meth:`pyramid.config.Configurator.add_route` method, which registers a :term:`route` to match any URL path that begins @@ -189,7 +189,7 @@ WSGI Application Creation .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 12 + :lines: 13 After configuring views and ending configuration, the script creates a WSGI *application* via the :meth:`pyramid.config.Configurator.make_wsgi_app` @@ -218,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 13 + :lines: 14 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server -- cgit v1.2.3 From 909dfee23c43709f2aa07942fb633cc740a9a6b3 Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 22 Aug 2012 10:23:40 -0400 Subject: fixed bug with mixing up asset spec and absolute uri in mako template inheritance --- pyramid/mako_templating.py | 2 ++ pyramid/tests/test_mako_templating.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/pyramid/mako_templating.py b/pyramid/mako_templating.py index 489c1f11a..2b09e8d45 100644 --- a/pyramid/mako_templating.py +++ b/pyramid/mako_templating.py @@ -43,6 +43,8 @@ class PkgResourceTemplateLookup(TemplateLookup): if relativeto is not None: relativeto = relativeto.replace('$', ':') if not(':' in uri) and (':' in relativeto): + if uri.startswith('/'): + return uri pkg, relto = relativeto.split(':') _uri = posixpath.join(posixpath.dirname(relto), uri) return '{0}:{1}'.format(pkg, _uri) diff --git a/pyramid/tests/test_mako_templating.py b/pyramid/tests/test_mako_templating.py index aced6c586..97b2c679b 100644 --- a/pyramid/tests/test_mako_templating.py +++ b/pyramid/tests/test_mako_templating.py @@ -499,6 +499,16 @@ class TestPkgResourceTemplateLookup(unittest.TestCase): result = inst.adjust_uri('b', '../a') self.assertEqual(result, '../b') + def test_adjust_uri_not_asset_spec_abs_with_relativeto_asset_spec(self): + inst = self._makeOne() + result = inst.adjust_uri('/c', 'a:b') + self.assertEqual(result, '/c') + + def test_adjust_uri_asset_spec_with_relativeto_not_asset_spec_abs(self): + inst = self._makeOne() + result = inst.adjust_uri('a:b', '/c') + self.assertEqual(result, 'a:b') + def test_get_template_not_asset_spec(self): fixturedir = self.get_fixturedir() inst = self._makeOne(directories=[fixturedir]) -- cgit v1.2.3 From 7853bc001283db8c5b87d026dcc130d1bece8dcc Mon Sep 17 00:00:00 2001 From: Blaise Laflamme Date: Wed, 22 Aug 2012 10:49:18 -0400 Subject: garden --- CHANGES.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0291d924a..30df788b3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,12 +17,16 @@ Bug Fixes during iteration`` exception. It no longer does. See https://github.com/Pylons/pyramid/issues/635 for more information. -- In Mako Templates lookup, cyheck if the uri is already adjusted and bring +- In Mako Templates lookup, check if the uri is already adjusted and bring it back to an asset spec. Normally occurs with inherited templates or included components. https://github.com/Pylons/pyramid/issues/606 https://github.com/Pylons/pyramid/issues/607 +- In Mako Templates lookup, check for absolute uri (using mako directories) + when mixing up inheritance with asset specs. + https://github.com/Pylons/pyramid/issues/662 + Features -------- -- cgit v1.2.3 From 75a5885afc250db7f19a4c80d947b2bef5826356 Mon Sep 17 00:00:00 2001 From: Ronan Amicel Date: Thu, 23 Aug 2012 17:02:22 +0300 Subject: Fixed typo in docs/narr/views.rst --- docs/narr/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/views.rst b/docs/narr/views.rst index f6ee9a8d5..9e41464a6 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -177,7 +177,7 @@ HTTP Exceptions ~~~~~~~~~~~~~~~ All classes documented in the :mod:`pyramid.httpexceptions` module documented -as inheriting from the :class:`pryamid.httpexceptions.HTTPException` are +as inheriting from the :class:`pyramid.httpexceptions.HTTPException` are :term:`http exception` objects. Instances of an HTTP exception object may either be *returned* or *raised* from within view code. In either case (return or raise) the instance will be used as as the view's response. -- cgit v1.2.3 From eb403ffe22501ae0546c02c7c24b54b5e3d1eb83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 09:57:35 -0500 Subject: exposed the serve_forever line to the helloworld narrative --- docs/narr/firstapp.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index dbdc48549..ccaa6e9e2 100644 --- a/docs/narr/firstapp.rst +++ b/docs/narr/firstapp.rst @@ -218,7 +218,7 @@ WSGI Application Serving .. ignore-next-block .. literalinclude:: helloworld.py :linenos: - :lines: 14 + :lines: 14-15 Finally, we actually serve the application to requestors by starting up a WSGI server. We happen to use the :mod:`wsgiref` ``make_server`` server -- cgit v1.2.3 From 45b6e192c44ebee124317a90a9a6dcc044407a2a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:31:38 -0500 Subject: added cookie session changes to CHANGES.txt --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 30df788b3..b8747f06c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -109,6 +109,11 @@ Features config = Configurator() config.add_permission('view') +- The ``UnencryptedCookieSessionFactoryConfig`` now accepts + ``signed_serialize`` and ``signed_deserialize`` hooks which may be used + to influence how the sessions are marshalled (by default this is done + with HMAC+pickle). + Deprecations ------------ -- cgit v1.2.3 From 9822a18731616e53bd77045ad8a7879d1a969c23 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:52:31 -0500 Subject: fixed an incompatibility with accept header test in py3 --- pyramid/tests/test_config/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 1a59b3c5c..72a0d8ebd 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -658,7 +658,7 @@ class TestViewsConfigurationMixin(unittest.TestCase): wrapper = self._getViewCallable(config) self.assertTrue(IMultiView.providedBy(wrapper)) self.assertEqual(len(wrapper.media_views.items()),1) - self.assertFalse(wrapper.media_views.has_key('text/HTML')) + self.assertFalse('text/HTML' in wrapper.media_views) self.assertEqual(wrapper(None, None), 'OK') request = DummyRequest() request.accept = DummyAccept('text/html', 'text/html') -- cgit v1.2.3 From 75a8ff4ad0ba8eed13592eb5e7a211da3035e209 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 23 Aug 2012 10:53:04 -0500 Subject: updated CHANGES with accept-header bug fix --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index b8747f06c..e799c8f00 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,11 @@ Bug Fixes when mixing up inheritance with asset specs. https://github.com/Pylons/pyramid/issues/662 +- HTTP Accept headers were not being normalized causing potentially + conflicting view registrations to go unnoticed. Two views that only + differ in the case ('text/html' vs. 'text/HTML') will now raise an error. + https://github.com/Pylons/pyramid/pull/620 + Features -------- -- cgit v1.2.3