Skip to content

Commit

Permalink
Merge branch 'master' of github.com:Pylons/pyramid
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdonc committed Aug 23, 2012
2 parents b9b4657 + 75a8ff4 commit 0d9dccd
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 62 deletions.
16 changes: 15 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down Expand Up @@ -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
------------

Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 5 additions & 5 deletions docs/narr/firstapp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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`
Expand Down Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions docs/narr/helloworld.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
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()

2 changes: 1 addition & 1 deletion docs/narr/views.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions pyramid/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,9 @@ def view(context, request):
name=renderer, package=self.package,
registry = self.registry)

if accept is not None:
accept = accept.lower()

introspectables = []
pvals = predicates.copy()
pvals.update(
Expand Down
2 changes: 2 additions & 0 deletions pyramid/mako_templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def adjust_uri(self, uri, relativeto):
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)
Expand Down
106 changes: 58 additions & 48 deletions pyramid/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,53 @@ def set_cookie_callback(request, response):
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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -225,51 +283,3 @@ def _set_cookie(self, response):
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)

18 changes: 18 additions & 0 deletions pyramid/tests/test_config/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,24 @@ def view2(context, request):
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'
Expand Down
10 changes: 10 additions & 0 deletions pyramid/tests/test_mako_templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,16 @@ def test_adjust_uri_not_asset_spec_with_relativeto_not_asset_spec(self):
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])
Expand Down
42 changes: 42 additions & 0 deletions pyramid/tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,48 @@ def test_get_csrf_token_new(self):
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
Expand Down

0 comments on commit 0d9dccd

Please sign in to comment.