diff options
| author | Chris McDonough <chrism@plope.com> | 2013-12-12 15:49:41 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2013-12-12 15:49:41 -0500 |
| commit | 28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c (patch) | |
| tree | ec0daf06f63f433bd622f54e72c720b5125994ce | |
| parent | b13969deeb80dd9aa5130d16ea712b323ac3bafe (diff) | |
| parent | 51e5538d8e75a01fdb1c97d6b241071381cbc9fc (diff) | |
| download | pyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.tar.gz pyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.tar.bz2 pyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.zip | |
Merge branch 'master' of github.com:Pylons/pyramid
| -rw-r--r-- | CHANGES.txt | 25 | ||||
| -rw-r--r-- | docs/narr/i18n.rst | 6 | ||||
| -rw-r--r-- | docs/whatsnew-1.5.rst | 7 | ||||
| -rw-r--r-- | pyramid/authentication.py | 86 | ||||
| -rw-r--r-- | pyramid/i18n.py | 33 | ||||
| -rw-r--r-- | pyramid/session.py | 113 | ||||
| -rw-r--r-- | pyramid/testing.py | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_authentication.py | 129 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 27 | ||||
| -rw-r--r-- | pyramid/url.py | 2 | ||||
| -rw-r--r-- | setup.py | 4 |
11 files changed, 237 insertions, 196 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 98784f3d7..8ca6e7e9b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,5 @@ -Unreleased -========== +1.5a3 (2013-12-10) +================== Features -------- @@ -35,11 +35,13 @@ Features See https://github.com/Pylons/pyramid/pull/1149 - Added a new ``SignedCookieSessionFactory`` which is very similar to the - ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on - signing content. The custom serializer arguments to this function should - only focus on serializing, unlike its predecessor which required the - serializer to also perform signing. - See https://github.com/Pylons/pyramid/pull/1142 + ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on signing + content. The custom serializer arguments to this function should only focus + on serializing, unlike its predecessor which required the serializer to also + perform signing. See https://github.com/Pylons/pyramid/pull/1142 . Note + that cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using ``UnencryptedCookieSessionFactory``, + so existing user session data will be destroyed if you switch to it. - Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie factory that can be used by framework implementors to create their own @@ -66,6 +68,9 @@ Features to use a different query string format than ``x-www-form-urlencoded``. See https://github.com/Pylons/pyramid/pull/1183 +- ``pyramid.testing.DummyRequest`` now has a ``domain`` attribute to match the + new WebOb 1.3 API. Its value is ``example.com``. + Bug Fixes --------- @@ -149,6 +154,12 @@ Deprecations Instead, use the newly-added ``unauthenticated_userid`` attribute of the request object. +Dependencies +------------ + +- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile`` + from 1.3+). + 1.5a2 (2013-09-22) ================== diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst index c9b782c08..5f50ca212 100644 --- a/docs/narr/i18n.rst +++ b/docs/narr/i18n.rst @@ -607,10 +607,8 @@ object, but the domain and mapping information attached is ignored. def aview(request): localizer = request.localizer num = 1 - translated = localizer.pluralize( - _('item_plural', default="${number} items"), - None, num, 'mydomain', mapping={'number':num} - ) + translated = localizer.pluralize('item_plural', '${number} items', + num, 'mydomain', mapping={'number':num}) The corresponding message catalog must have language plural definitions and plural alternatives set. diff --git a/docs/whatsnew-1.5.rst b/docs/whatsnew-1.5.rst index 23613896a..9ccf097a8 100644 --- a/docs/whatsnew-1.5.rst +++ b/docs/whatsnew-1.5.rst @@ -348,7 +348,10 @@ The feature additions in Pyramid 1.5 follow. signing content. The custom serializer arguments to this function should only focus on serializing, unlike its predecessor which required the serializer to also perform signing. - See https://github.com/Pylons/pyramid/pull/1142 + See https://github.com/Pylons/pyramid/pull/1142 . Note + that cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using ``UnencryptedCookieSessionFactory``, + so existing user session data will be destroyed if you switch to it. - Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie factory that can be used by framework implementors to create their own @@ -504,3 +507,5 @@ Dependency Changes - Pyramid no longer depends upon ``Mako`` or ``Chameleon``. +- Pyramid now depends on WebOb>=1.3 (it uses ``webob.cookies.CookieProfile`` + from 1.3+). diff --git a/pyramid/authentication.py b/pyramid/authentication.py index 2c301bd29..ba7b864f9 100644 --- a/pyramid/authentication.py +++ b/pyramid/authentication.py @@ -10,6 +10,8 @@ import warnings from zope.interface import implementer +from webob.cookies import CookieProfile + from pyramid.compat import ( long, text_type, @@ -18,6 +20,7 @@ from pyramid.compat import ( url_quote, bytes_, ascii_native_, + native_, ) from pyramid.interfaces import ( @@ -798,8 +801,6 @@ def encode_ip_timestamp(ip, timestamp): ts_chars = ''.join(map(chr, ts)) return bytes_(ip_chars + ts_chars) -EXPIRE = object() - class AuthTktCookieHelper(object): """ A helper class for use in third-party authentication policy @@ -830,55 +831,32 @@ class AuthTktCookieHelper(object): include_ip=False, timeout=None, reissue_time=None, max_age=None, http_only=False, path="/", wild_domain=True, hashalg='md5', parent_domain=False, domain=None): + + serializer = _SimpleSerializer() + + self.cookie_profile = CookieProfile( + cookie_name = cookie_name, + secure = secure, + max_age = max_age, + httponly = http_only, + path = path, + serializer=serializer + ) + self.secret = secret self.cookie_name = cookie_name - self.include_ip = include_ip self.secure = secure + self.include_ip = include_ip self.timeout = timeout self.reissue_time = reissue_time self.max_age = max_age - self.http_only = http_only - self.path = path self.wild_domain = wild_domain self.parent_domain = parent_domain self.domain = domain self.hashalg = hashalg - static_flags = [] - if self.secure: - static_flags.append('; Secure') - if self.http_only: - static_flags.append('; HttpOnly') - self.static_flags = "".join(static_flags) - - def _get_cookies(self, environ, value, max_age=None): - if max_age is EXPIRE: - max_age = "; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT" - elif max_age is not None: - later = datetime.datetime.utcnow() + datetime.timedelta( - seconds=int(max_age)) - # Wdy, DD-Mon-YY HH:MM:SS GMT - expires = later.strftime('%a, %d %b %Y %H:%M:%S GMT') - # the Expires header is *required* at least for IE7 (IE7 does - # not respect Max-Age) - max_age = "; Max-Age=%s; Expires=%s" % (max_age, expires) - else: - max_age = '' - - cur_domain = environ.get('HTTP_HOST', environ.get('SERVER_NAME')) - - # While Chrome, IE, and Firefox can cope, Opera (at least) cannot - # cope with a port number in the cookie domain when the URL it - # receives the cookie from does not also have that port number in it - # (e.g via a proxy). In the meantime, HTTP_HOST is sent with port - # number, and neither Firefox nor Chrome do anything with the - # information when it's provided in a cookie domain except strip it - # out. So we strip out any port number from the cookie domain - # aggressively to avoid problems. See also - # https://github.com/Pylons/pyramid/issues/131 - if ':' in cur_domain: - cur_domain = cur_domain.split(':', 1)[0] - + def _get_cookies(self, request, value, max_age=None): + cur_domain = request.domain domains = [] if self.domain: @@ -892,14 +870,15 @@ class AuthTktCookieHelper(object): if self.wild_domain: domains.append('.' + cur_domain) - cookies = [] - base_cookie = '%s="%s"; Path=%s%s%s' % (self.cookie_name, value, - self.path, max_age, self.static_flags) - for domain in domains: - domain = '; Domain=%s' % domain if domain is not None else '' - cookies.append(('Set-Cookie', '%s%s' % (base_cookie, domain))) + profile = self.cookie_profile(request) - return cookies + kw = {} + kw['domains'] = domains + if max_age is not None: + kw['max_age'] = max_age + + headers = profile.get_headers(value, **kw) + return headers def identify(self, request): """ Return a dictionary with authentication information, or ``None`` @@ -968,9 +947,8 @@ class AuthTktCookieHelper(object): def forget(self, request): """ Return a set of expires Set-Cookie headers, which will destroy any existing auth_tkt cookie when attached to a response""" - environ = request.environ request._authtkt_reissue_revoked = True - return self._get_cookies(environ, '', max_age=EXPIRE) + return self._get_cookies(request, None) def remember(self, request, userid, max_age=None, tokens=()): """ Return a set of Set-Cookie headers; when set into a response, @@ -1037,7 +1015,7 @@ class AuthTktCookieHelper(object): ) cookie_value = ticket.cookie_value() - return self._get_cookies(environ, cookie_value, max_age) + return self._get_cookies(request, cookie_value, max_age) @implementer(IAuthenticationPolicy) class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): @@ -1196,3 +1174,11 @@ class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): except ValueError: # not enough values to unpack return None return username, password + +class _SimpleSerializer(object): + def loads(self, bstruct): + return native_(bstruct) + + def dumps(self, appstruct): + return bytes_(appstruct) + diff --git a/pyramid/i18n.py b/pyramid/i18n.py index 6ffd93e8f..aaba769c6 100644 --- a/pyramid/i18n.py +++ b/pyramid/i18n.py @@ -75,16 +75,16 @@ class Localizer(object): :term:`message identifier` objects as a singular/plural pair and an ``n`` value representing the number that appears in the message using gettext plural forms support. The ``singular`` - and ``plural`` objects passed may be translation strings or - unicode strings. ``n`` represents the number of elements. - ``domain`` is the translation domain to use to do the - pluralization, and ``mapping`` is the interpolation mapping - that should be used on the result. Note that if the objects - passed are translation strings, their domains and mappings are - ignored. The domain and mapping arguments must be used - instead. If the ``domain`` is not supplied, a default domain - is used (usually ``messages``). - + and ``plural`` objects should be unicode strings. There is no + reason to use translation string objects as arguments as all + metadata is ignored. + + ``n`` represents the number of elements. ``domain`` is the + translation domain to use to do the pluralization, and ``mapping`` + is the interpolation mapping that should be used on the result. If + the ``domain`` is not supplied, a default domain is used (usually + ``messages``). + Example:: num = 1 @@ -93,6 +93,19 @@ class Localizer(object): num, mapping={'num':num}) + If using the gettext plural support, which is required for + languages that have pluralisation rules other than n != 1, the + ``singular`` argument must be the message_id defined in the + translation file. The plural argument is not used in this case. + + Example:: + + num = 1 + translated = localizer.pluralize('item_plural', + '', + num, + mapping={'num':num}) + """ if self.pluralizer is None: diff --git a/pyramid/session.py b/pyramid/session.py index d3a4113b9..8c9900975 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: @@ -374,6 +382,10 @@ def UnencryptedCookieSessionFactoryConfig( """ .. deprecated:: 1.5 Use :func:`pyramid.session.SignedCookieSessionFactory` instead. + Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using + ``UnencryptedCookieSessionFactory``, so existing user session data will + be destroyed if you switch to it. Configure a :term:`session factory` which will provide unencrypted (but signed) cookie-based sessions. The return value of this @@ -430,9 +442,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, @@ -447,7 +470,10 @@ def UnencryptedCookieSessionFactoryConfig( deprecated( 'UnencryptedCookieSessionFactoryConfig', 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of ' - 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.' + 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead. ' + 'Caveat: Cookies generated using SignedCookieSessionFactory are not ' + 'compatible with cookies generated using UnencryptedCookieSessionFactory, ' + 'so existing user session data will be destroyed if you switch to it.' ) def SignedCookieSessionFactory( @@ -463,8 +489,7 @@ def SignedCookieSessionFactory( reissue_time=0, hashalg='sha512', salt='pyramid.session.', - serialize=None, - deserialize=None, + serializer=None, ): """ .. versionadded:: 1.5 @@ -546,53 +571,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/testing.py b/pyramid/testing.py index b3460d8aa..91dc41dd5 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -320,6 +320,7 @@ class DummyRequest( method = 'GET' application_url = 'http://example.com' host = 'example.com:80' + domain = 'example.com' content_length = 0 query_string = '' charset = 'UTF-8' diff --git a/pyramid/tests/test_authentication.py b/pyramid/tests/test_authentication.py index 3ac8f2d61..79d2a5923 100644 --- a/pyramid/tests/test_authentication.py +++ b/pyramid/tests/test_authentication.py @@ -572,7 +572,12 @@ class TestAuthTktCookieHelper(unittest.TestCase): return DummyRequest(environ, cookie=cookie) def _cookieValue(self, cookie): - return eval(cookie.value) + items = cookie.value.split('/') + D = {} + for item in items: + k, v = item.split('=', 1) + D[k] = v + return D def _parseHeaders(self, headers): return [ self._parseHeader(header) for header in headers ] @@ -838,7 +843,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): request.callbacks[0](None, response) self.assertEqual(len(response.headerlist), 3) self.assertEqual(response.headerlist[0][0], 'Set-Cookie') - self.assertTrue("'tokens': ()" in response.headerlist[0][1]) + self.assertTrue("/tokens=/" in response.headerlist[0][1]) def test_remember(self): helper = self._makeOne('secret') @@ -851,11 +856,11 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertTrue(result[0][1].startswith('auth_tkt=')) self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost')) + self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/')) self.assertTrue(result[1][1].startswith('auth_tkt=')) self.assertEqual(result[2][0], 'Set-Cookie') - self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost')) + self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/')) self.assertTrue(result[2][1].startswith('auth_tkt=')) def test_remember_include_ip(self): @@ -869,11 +874,11 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertTrue(result[0][1].startswith('auth_tkt=')) self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost')) + self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/')) self.assertTrue(result[1][1].startswith('auth_tkt=')) self.assertEqual(result[2][0], 'Set-Cookie') - self.assertTrue(result[2][1].endswith('; Path=/; Domain=.localhost')) + self.assertTrue(result[2][1].endswith('; Domain=.localhost; Path=/')) self.assertTrue(result[2][1].startswith('auth_tkt=')) def test_remember_path(self): @@ -889,12 +894,12 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(result[1][0], 'Set-Cookie') self.assertTrue(result[1][1].endswith( - '; Path=/cgi-bin/app.cgi/; Domain=localhost')) + '; Domain=localhost; Path=/cgi-bin/app.cgi/')) self.assertTrue(result[1][1].startswith('auth_tkt=')) self.assertEqual(result[2][0], 'Set-Cookie') self.assertTrue(result[2][1].endswith( - '; Path=/cgi-bin/app.cgi/; Domain=.localhost')) + '; Domain=.localhost; Path=/cgi-bin/app.cgi/')) self.assertTrue(result[2][1].startswith('auth_tkt=')) def test_remember_http_only(self): @@ -922,15 +927,15 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(len(result), 3) self.assertEqual(result[0][0], 'Set-Cookie') - self.assertTrue('; Secure' in result[0][1]) + self.assertTrue('; secure' in result[0][1]) self.assertTrue(result[0][1].startswith('auth_tkt=')) self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue('; Secure' in result[1][1]) + self.assertTrue('; secure' in result[1][1]) self.assertTrue(result[1][1].startswith('auth_tkt=')) self.assertEqual(result[2][0], 'Set-Cookie') - self.assertTrue('; Secure' in result[2][1]) + self.assertTrue('; secure' in result[2][1]) self.assertTrue(result[2][1].startswith('auth_tkt=')) def test_remember_wild_domain_disabled(self): @@ -944,62 +949,49 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertTrue(result[0][1].startswith('auth_tkt=')) self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue(result[1][1].endswith('; Path=/; Domain=localhost')) + self.assertTrue(result[1][1].endswith('; Domain=localhost; Path=/')) self.assertTrue(result[1][1].startswith('auth_tkt=')) def test_remember_parent_domain(self): helper = self._makeOne('secret', parent_domain=True) request = self._makeRequest() - request.environ['HTTP_HOST'] = 'www.example.com' + request.domain = 'www.example.com' result = helper.remember(request, 'other') self.assertEqual(len(result), 1) self.assertEqual(result[0][0], 'Set-Cookie') - self.assertTrue(result[0][1].endswith('; Path=/; Domain=.example.com')) + self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/')) self.assertTrue(result[0][1].startswith('auth_tkt=')) def test_remember_parent_domain_supercedes_wild_domain(self): helper = self._makeOne('secret', parent_domain=True, wild_domain=True) request = self._makeRequest() - request.environ['HTTP_HOST'] = 'www.example.com' + request.domain = 'www.example.com' result = helper.remember(request, 'other') self.assertEqual(len(result), 1) - self.assertTrue(result[0][1].endswith('; Domain=.example.com')) + self.assertTrue(result[0][1].endswith('; Domain=.example.com; Path=/')) def test_remember_explicit_domain(self): helper = self._makeOne('secret', domain='pyramid.bazinga') request = self._makeRequest() - request.environ['HTTP_HOST'] = 'www.example.com' + request.domain = 'www.example.com' result = helper.remember(request, 'other') self.assertEqual(len(result), 1) self.assertEqual(result[0][0], 'Set-Cookie') - self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga')) + self.assertTrue(result[0][1].endswith( + '; Domain=pyramid.bazinga; Path=/')) self.assertTrue(result[0][1].startswith('auth_tkt=')) def test_remember_domain_supercedes_parent_and_wild_domain(self): helper = self._makeOne('secret', domain='pyramid.bazinga', parent_domain=True, wild_domain=True) request = self._makeRequest() - request.environ['HTTP_HOST'] = 'www.example.com' + request.domain = 'www.example.com' result = helper.remember(request, 'other') self.assertEqual(len(result), 1) - self.assertTrue(result[0][1].endswith('; Path=/; Domain=pyramid.bazinga')) - - def test_remember_domain_has_port(self): - helper = self._makeOne('secret', wild_domain=False) - request = self._makeRequest() - request.environ['HTTP_HOST'] = 'example.com:80' - result = helper.remember(request, 'other') - self.assertEqual(len(result), 2) - - self.assertEqual(result[0][0], 'Set-Cookie') - self.assertTrue(result[0][1].endswith('; Path=/')) - self.assertTrue(result[0][1].startswith('auth_tkt=')) - - self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue(result[1][1].endswith('; Path=/; Domain=example.com')) - self.assertTrue(result[1][1].startswith('auth_tkt=')) + self.assertTrue(result[0][1].endswith( + '; Domain=pyramid.bazinga; Path=/')) def test_remember_binary_userid(self): import base64 @@ -1010,7 +1002,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(len(result), 3) val = self._cookieValue(values[0]) self.assertEqual(val['userid'], - bytes_(base64.b64encode(b'userid').strip())) + text_(base64.b64encode(b'userid').strip())) self.assertEqual(val['user_data'], 'userid_type:b64str') def test_remember_int_userid(self): @@ -1044,7 +1036,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(len(result), 3) val = self._cookieValue(values[0]) self.assertEqual(val['userid'], - base64.b64encode(userid.encode('utf-8'))) + text_(base64.b64encode(userid.encode('utf-8')))) self.assertEqual(val['user_data'], 'userid_type:b64unicode') def test_remember_insane_userid(self): @@ -1074,13 +1066,13 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(len(result), 3) self.assertEqual(result[0][0], 'Set-Cookie') - self.assertTrue("'tokens': ('foo', 'bar')" in result[0][1]) + self.assertTrue("/tokens=foo|bar/" in result[0][1]) self.assertEqual(result[1][0], 'Set-Cookie') - self.assertTrue("'tokens': ('foo', 'bar')" in result[1][1]) + self.assertTrue("/tokens=foo|bar/" in result[1][1]) self.assertEqual(result[2][0], 'Set-Cookie') - self.assertTrue("'tokens': ('foo', 'bar')" in result[2][1]) + self.assertTrue("/tokens=foo|bar/" in result[2][1]) def test_remember_unicode_but_ascii_token(self): helper = self._makeOne('secret') @@ -1088,7 +1080,7 @@ class TestAuthTktCookieHelper(unittest.TestCase): la = text_(b'foo', 'utf-8') result = helper.remember(request, 'other', tokens=(la,)) # tokens must be str type on both Python 2 and 3 - self.assertTrue("'tokens': ('foo',)" in result[0][1]) + self.assertTrue("/tokens=foo/" in result[0][1]) def test_remember_nonascii_token(self): helper = self._makeOne('secret') @@ -1112,18 +1104,25 @@ class TestAuthTktCookieHelper(unittest.TestCase): self.assertEqual(len(headers), 3) name, value = headers[0] self.assertEqual(name, 'Set-Cookie') - self.assertEqual(value, - 'auth_tkt=""; Path=/; Max-Age=0; Expires=Wed, 31-Dec-97 23:59:59 GMT') + self.assertEqual( + value, + 'auth_tkt=; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT' + ) name, value = headers[1] self.assertEqual(name, 'Set-Cookie') - self.assertEqual(value, - 'auth_tkt=""; Path=/; Max-Age=0; ' - 'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=localhost') + self.assertEqual( + value, + 'auth_tkt=; Domain=localhost; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT' + ) name, value = headers[2] self.assertEqual(name, 'Set-Cookie') - self.assertEqual(value, - 'auth_tkt=""; Path=/; Max-Age=0; ' - 'Expires=Wed, 31-Dec-97 23:59:59 GMT; Domain=.localhost') + self.assertEqual( + value, + 'auth_tkt=; Domain=.localhost; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT' + ) class TestAuthTicket(unittest.TestCase): def _makeOne(self, *arg, **kw): @@ -1417,7 +1416,19 @@ class TestBasicAuthAuthenticationPolicy(unittest.TestCase): self.assertEqual(policy.forget(None), [ ('WWW-Authenticate', 'Basic realm="SomeRealm"')]) +class TestSimpleSerializer(unittest.TestCase): + def _makeOne(self): + from pyramid.authentication import _SimpleSerializer + return _SimpleSerializer() + + def test_loads(self): + inst = self._makeOne() + self.assertEqual(inst.loads(b'abc'), text_('abc')) + def test_dumps(self): + inst = self._makeOne() + self.assertEqual(inst.dumps('abc'), bytes_('abc')) + class DummyContext: pass @@ -1429,6 +1440,7 @@ class DummyCookies(object): return self.cookie class DummyRequest: + domain = 'localhost' def __init__(self, environ=None, session=None, registry=None, cookie=None): self.environ = environ or {} self.session = session or {} @@ -1486,10 +1498,23 @@ class DummyAuthTktModule(object): self.kw = kw def cookie_value(self): - result = {'secret':self.secret, 'userid':self.userid, - 'remote_addr':self.remote_addr} + result = { + 'secret':self.secret, + 'userid':self.userid, + 'remote_addr':self.remote_addr + } result.update(self.kw) - result = repr(result) + tokens = result.pop('tokens', None) + if tokens is not None: + tokens = '|'.join(tokens) + result['tokens'] = tokens + items = sorted(result.items()) + new_items = [] + for k, v in items: + if isinstance(v, bytes): + v = text_(v) + new_items.append((k,v)) + result = '/'.join(['%s=%s' % (k, v) for k,v in new_items ]) return result self.AuthTicket = AuthTicket 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 diff --git a/pyramid/url.py b/pyramid/url.py index 484ee775f..78dd297d5 100644 --- a/pyramid/url.py +++ b/pyramid/url.py @@ -359,7 +359,7 @@ class URLMethodsMixin(object): .. warning:: if no ``elements`` arguments are specified, the resource URL will end with a trailing slash. If any ``elements`` are used, the generated URL will *not* - end in trailing a slash. + end in a trailing slash. If a keyword argument ``query`` is present, it will be used to compose a query string that will be tacked on to the end of the URL. The value @@ -39,7 +39,7 @@ except IOError: install_requires=[ 'setuptools', - 'WebOb >= 1.2b3', # request.path_info is unicode + 'WebOb >= 1.3', # request.domain and CookieProfile 'repoze.lru >= 0.4', # py3 compat 'zope.interface >= 3.8.0', # has zope.interface.registry 'zope.deprecation >= 3.5.0', # py3 compat @@ -69,7 +69,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.5a2', + version='1.5a3', description='The Pyramid Web Framework, a Pylons project', long_description=README + '\n\n' + CHANGES, classifiers=[ |
