summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2013-12-12 15:49:41 -0500
committerChris McDonough <chrism@plope.com>2013-12-12 15:49:41 -0500
commit28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c (patch)
treeec0daf06f63f433bd622f54e72c720b5125994ce
parentb13969deeb80dd9aa5130d16ea712b323ac3bafe (diff)
parent51e5538d8e75a01fdb1c97d6b241071381cbc9fc (diff)
downloadpyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.tar.gz
pyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.tar.bz2
pyramid-28c26c17d66b563140e60b95ca1ea7e6dc3ccb6c.zip
Merge branch 'master' of github.com:Pylons/pyramid
-rw-r--r--CHANGES.txt25
-rw-r--r--docs/narr/i18n.rst6
-rw-r--r--docs/whatsnew-1.5.rst7
-rw-r--r--pyramid/authentication.py86
-rw-r--r--pyramid/i18n.py33
-rw-r--r--pyramid/session.py113
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_authentication.py129
-rw-r--r--pyramid/tests/test_session.py27
-rw-r--r--pyramid/url.py2
-rw-r--r--setup.py4
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
diff --git a/setup.py b/setup.py
index 2d49717b7..e6c9a490a 100644
--- a/setup.py
+++ b/setup.py
@@ -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=[