diff options
| author | Chris McDonough <chrism@plope.com> | 2012-08-23 12:33:54 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2012-08-23 12:33:54 -0400 |
| commit | 0d9dccd259d52be9be2400cfe6ee5b709b812bfc (patch) | |
| tree | ddb6d0174bab3a49c19e9f678c1c7ce4a43c5a8f | |
| parent | b9b4657e79e7e1ed1f0c79bf82c3fe19a0ab5687 (diff) | |
| parent | 75a8ff4ad0ba8eed13592eb5e7a211da3035e209 (diff) | |
| download | pyramid-0d9dccd259d52be9be2400cfe6ee5b709b812bfc.tar.gz pyramid-0d9dccd259d52be9be2400cfe6ee5b709b812bfc.tar.bz2 pyramid-0d9dccd259d52be9be2400cfe6ee5b709b812bfc.zip | |
Merge branch 'master' of github.com:Pylons/pyramid
| -rw-r--r-- | CHANGES.txt | 16 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 4 | ||||
| -rw-r--r-- | docs/narr/firstapp.rst | 10 | ||||
| -rw-r--r-- | docs/narr/helloworld.py | 15 | ||||
| -rw-r--r-- | docs/narr/views.rst | 2 | ||||
| -rw-r--r-- | pyramid/config/views.py | 3 | ||||
| -rw-r--r-- | pyramid/mako_templating.py | 2 | ||||
| -rw-r--r-- | pyramid/session.py | 106 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 18 | ||||
| -rw-r--r-- | pyramid/tests/test_mako_templating.py | 10 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 42 |
11 files changed, 166 insertions, 62 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 0291d924a..e799c8f00 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -17,12 +17,21 @@ 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 + +- 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 -------- @@ -105,6 +114,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 ------------ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a2da7fbfd..264acf048 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -173,8 +173,12 @@ Contributors - Marin Rukavina, 2012/05/03 +- Lorenzo M. Catucci, 2012/06/08 + - Marc Abramowitz, 2012/06/13 - Jeff Cook, 2012/06/16 +- Ian Wilson, 2012/06/17 + - Roman Kozlovskyi, 2012/08/11 diff --git a/docs/narr/firstapp.rst b/docs/narr/firstapp.rst index a86826d86..ccaa6e9e2 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-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 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() 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. diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1c4e20dd6..b61a71914 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1047,6 +1047,9 @@ class ViewsConfiguratorMixin(object): name=renderer, package=self.package, registry = self.registry) + if accept is not None: + accept = accept.lower() + introspectables = [] pvals = predicates.copy() pvals.update( 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/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_config/test_views.py b/pyramid/tests/test_config/test_views.py index 38f60d79b..72a0d8ebd 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -646,6 +646,24 @@ 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' + 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('text/HTML' in wrapper.media_views) + 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' 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]) 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 |
