diff --git a/.gitignore b/.gitignore index 74b9957..080bef9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /itsdangerous-git-0.11.tar.xz +/itsdangerous-0.21.tar.gz diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..7907ba9 --- /dev/null +++ b/CHANGES @@ -0,0 +1,79 @@ +It's Dangerous Changelog +------------------------ + +Version 0.21 +~~~~~~~~~~~~ + +- Fixed an issue on Python 3 which caused invalid errors to be + generated. + +Version 0.20 +~~~~~~~~~~~~ + +- Fixed an incorrect call into `want_bytes` that broke some + uses of itsdangerous on Python 2.6. + +Version 0.19 +~~~~~~~~~~~~ + +- Dropped support for 2.5 and added support for 3.3. + +Version 0.18 +~~~~~~~~~~~~ + +- Added support for JSON Web Signatures (JWS). + +Version 0.17 +~~~~~~~~~~~~ + +- Fixed a name error when overriding the digest method. + +Version 0.16 +~~~~~~~~~~~~ + +- made it possible to pass unicode values to `load_payload` to make it + easier to debug certain things. + +Version 0.15 +~~~~~~~~~~~~ + +- made standalone `load_payload` more robust by raising one specific + error if something goes wrong. +- refactored exceptions to catch more cases individually, added more + attributes. +- fixed an issue that caused `load_payload` not work in some situations + with timestamp based serializers +- added an `loads_unsafe` method. + +Version 0.14 +~~~~~~~~~~~~ + +- API refactoring to support different key derivations. +- Added attributes to exceptions so that you can inspect the data even + if the signature check failed. + +Version 0.13 +~~~~~~~~~~~~ + +- Small API change that enables customization of the digest module. + +Version 0.12 +~~~~~~~~~~~~ + +- Fixed a problem with the local timezone being used for the epoch + calculation. This might invalidate some of your signatures if you + were not running in UTC timezone. You can revert to the old behavior + by monkey patching itsdangerous.EPOCH. + +Version 0.11 +~~~~~~~~~~~~ + +- Fixed an uncought value error. + +Version 0.10 +~~~~~~~~~~~~ + +- Refactored interface that the underlying serializers can be swapped by + passing in a module instead of having to override the payload loaders + and dumpers. This makes the interface more compatible with Django's + recent changes. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..183d7f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2011 by Armin Ronacher and the Django Software Foundation. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/python-itsdangerous.spec b/python-itsdangerous.spec index ced5cfa..04c8359 100644 --- a/python-itsdangerous.spec +++ b/python-itsdangerous.spec @@ -1,37 +1,108 @@ -%global srcname itsdangerous +%global upstream_name itsdangerous -Name: python-itsdangerous -Version: 0.11 +%if 0%{?fedora} || 0%{?rhel} > 6 +%bcond_without python3 +%else +%bcond_with python3 +%endif + +Name: python-%{upstream_name} +Version: 0.21 Release: 1%{?dist} Summary: Python library for passing trusted data to untrusted environments -Group: Development/Languages License: BSD -URL: http://packages.python.org/itsdangerous/ -#Source0: http://pypi.python.org/packages/source/i/%{srcname}/%{srcname}-%{version}.tar.gz -# Tarballs on PyPi lack LICENSE and tests, so we generate our own from git instead: -# git archive --format=tar --prefix=itsdangerous-0.11/ 0.11 | xz >itsdangerous-git-0.11.tar.xz -Source0: %{srcname}-git-%{version}.tar.xz +URL: http://pythonhosted.org/itsdangerous/ +Source0: http://pypi.python.org/packages/source/i/%{upstream_name}/%{upstream_name}-%{version}.tar.gz +# Tarballs on PyPi lack LICENSE, CHANGES, and tests. +# https://github.com/mitsuhiko/itsdangerous/pull/22 +Source1: LICENSE +Source2: CHANGES +Source3: tests.py BuildArch: noarch -BuildRequires: python-setuptools-devel +BuildRequires: python2-devel +BuildRequires: python-setuptools +%if %{with python3} +BuildRequires: python3-devel +BuildRequires: python3-setuptools +%endif %description +Itsdangerous is a Python library for passing data through untrusted +environments (for example, HTTP cookies) while ensuring the data is not +tampered with. + +Internally itsdangerous uses HMAC and SHA1 for signing by default and bases the +implementation on the Django signing module. It also however supports JSON Web +Signatures (JWS). + +%if %{with python3} +%package -n python3-%{upstream_name} +Summary: Python 3 library for passing trusted data to untrusted environments + +%description -n python3-%{upstream_name} +Itsdangerous is a Python 3 library for passing data through untrusted +environments (for example, HTTP cookies) while ensuring the data is not +tampered with. + +Internally itsdangerous uses HMAC and SHA1 for signing by default and bases the +implementation on the Django signing module. It also however supports JSON Web +Signatures (JWS). +%endif %prep -%setup -q -n %{srcname}-%{version} +%setup -q -n %{upstream_name}-%{version} +rm -r *.egg-info +cp -p %{SOURCE1} %{SOURCE2} %{SOURCE3} . + +%if %{with python3} +rm -rf %{py3dir} +cp -a . %{py3dir} +%endif %build %{__python} setup.py build +%if %{with python3} +pushd %{py3dir} +%{__python3} setup.py build +popd +%endif + %install +%if %{with python3} +pushd %{py3dir} +%{__python3} setup.py install --skip-build --root %{buildroot} +popd +%endif + %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT %check PYTHONPATH=$RPM_BUILD_ROOT%{python_sitelib} %{__python} tests.py +%if %{with python3} +pushd %{py3dir} +PYTHONPATH=$RPM_BUILD_ROOT%{python3_sitelib} %{__python3} tests.py +popd +%endif + %files %doc LICENSE CHANGES README -%{python_sitelib}/itsdangerous* +%{python_sitelib}/%{upstream_name}.py* +%{python_sitelib}/%{upstream_name}*.egg-info + +%if %{with python3} +%files -n python3-%{upstream_name} +%doc LICENSE CHANGES README +%{python3_sitelib}/%{upstream_name}.py +%{python3_sitelib}/%{upstream_name}*.egg-info +%{python3_sitelib}/__pycache__/%{upstream_name}* +%endif %changelog +* Fri Jun 14 2013 Dan Callaghan - 0.21-1 +- updated to upstream release 0.21 +- added Python 3 subpackage + * Wed Nov 11 2011 Dan Callaghan - 0.11-1 - initial version diff --git a/sources b/sources index db96a58..83368e2 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -e23b61563793dd428963352166d8e80f itsdangerous-git-0.11.tar.xz +84d4b33f0a1e4d0f7f8f8755a5eb2580 itsdangerous-0.21.tar.gz diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..2cc6b28 --- /dev/null +++ b/tests.py @@ -0,0 +1,278 @@ +import time +import pickle +import hashlib +import unittest +from datetime import datetime + +import itsdangerous as idmod +from itsdangerous import want_bytes, text_type, PY2 + + +# Helper function for some unsafe string manipulation on encoded +# data. This is required for Python 3 but would break on Python 2 +if PY2: + def _coerce_string(reference_string, value): + return value +else: + def _coerce_string(reference_string, value): + assert isinstance(value, text_type), 'rhs needs to be a string' + if type(reference_string) != type(value): + value = value.encode('utf-8') + return value + + +class UtilityTestCase(unittest.TestCase): + def test_want_bytes(self): + self.assertEqual(want_bytes(b"foobar"), b"foobar") + self.assertEqual(want_bytes(u"foobar"), b"foobar") + + +class SerializerTestCase(unittest.TestCase): + serializer_class = idmod.Serializer + + def make_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + def test_dumps_loads(self): + objects = (['a', 'list'], 'a string', u'a unicode string \u2019', + {'a': 'dictionary'}, 42, 42.5) + s = self.make_serializer('Test') + for o in objects: + value = s.dumps(o) + self.assertNotEqual(o, value) + self.assertEqual(o, s.loads(value)) + + def test_decode_detects_tampering(self): + s = self.make_serializer('Test') + + transforms = ( + lambda s: s.upper(), + lambda s: s + _coerce_string(s, 'a'), + lambda s: _coerce_string(s, 'a') + s[1:], + lambda s: s.replace(_coerce_string(s, '.'), _coerce_string(s, '')), + ) + value = { + 'foo': 'bar', + 'baz': 1, + } + encoded = s.dumps(value) + self.assertEqual(value, s.loads(encoded)) + for transform in transforms: + self.assertRaises( + idmod.BadSignature, s.loads, transform(encoded)) + + def test_accepts_unicode(self): + objects = (['a', 'list'], 'a string', u'a unicode string \u2019', + {'a': 'dictionary'}, 42, 42.5) + s = self.make_serializer('Test') + for o in objects: + value = s.dumps(o) + self.assertNotEqual(o, value) + self.assertEqual(o, s.loads(value)) + + def test_exception_attributes(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + + try: + s.loads(ts + _coerce_string(ts, 'x')) + except idmod.BadSignature as e: + self.assertEqual(want_bytes(e.payload), + want_bytes(ts).rsplit(b'.', 1)[0]) + self.assertEqual(s.load_payload(e.payload), value) + else: + self.fail('Did not get bad signature') + + def test_unsafe_load(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + self.assertEqual(s.loads_unsafe(ts), (True, u'hello')) + self.assertEqual(s.loads_unsafe(ts, salt='modified'), (False, u'hello')) + + def test_load_unsafe_with_unicode_strings(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + self.assertEqual(s.loads_unsafe(ts), (True, u'hello')) + self.assertEqual(s.loads_unsafe(ts, salt='modified'), (False, u'hello')) + + try: + s.loads(ts, salt='modified') + except idmod.BadSignature as e: + self.assertEqual(s.load_payload(e.payload), u'hello') + + def test_signer_kwargs(self): + secret_key = 'predictable-key' + value = 'hello' + s = self.make_serializer(secret_key, signer_kwargs=dict( + digest_method=hashlib.md5, + key_derivation='hmac' + )) + ts = s.dumps(value) + self.assertEqual(s.loads(ts), u'hello') + + +class TimedSerializerTestCase(SerializerTestCase): + serializer_class = idmod.TimedSerializer + + def setUp(self): + self._time = time.time + time.time = lambda: idmod.EPOCH + + def tearDown(self): + time.time = self._time + + def test_decode_with_timeout(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + self.assertNotEqual(ts, idmod.Serializer(secret_key).dumps(value)) + + self.assertEqual(s.loads(ts), value) + time.time = lambda: idmod.EPOCH + 10 + self.assertEqual(s.loads(ts, max_age=11), value) + self.assertEqual(s.loads(ts, max_age=10), value) + self.assertRaises( + idmod.SignatureExpired, s.loads, ts, max_age=9) + + def test_decode_return_timestamp(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + loaded, timestamp = s.loads(ts, return_timestamp=True) + self.assertEqual(loaded, value) + self.assertEqual(timestamp, datetime.utcfromtimestamp(time.time())) + + def test_exception_attributes(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + try: + s.loads(ts, max_age=-1) + except idmod.SignatureExpired as e: + self.assertEqual(e.date_signed, + datetime.utcfromtimestamp(time.time())) + self.assertEqual(want_bytes(e.payload), + want_bytes(ts).rsplit(b'.', 2)[0]) + self.assertEqual(s.load_payload(e.payload), value) + else: + self.fail('Did not get expiration') + + +class JSONWebSignatureSerializerTestCase(SerializerTestCase): + serializer_class = idmod.JSONWebSignatureSerializer + + def test_decode_return_header(self): + secret_key = 'predictable-key' + value = u'hello' + header = {"typ": "dummy"} + + s = self.make_serializer(secret_key) + full_header = header.copy() + full_header['alg'] = s.algorithm_name + + ts = s.dumps(value, header_fields=header) + loaded, loaded_header = s.loads(ts, return_header=True) + self.assertEqual(loaded, value) + self.assertEqual(loaded_header, full_header) + + def test_hmac_algorithms(self): + secret_key = 'predictable-key' + value = u'hello' + + algorithms = ('HS256', 'HS384', 'HS512') + for algorithm in algorithms: + s = self.make_serializer(secret_key, algorithm_name=algorithm) + ts = s.dumps(value) + self.assertEqual(s.loads(ts), value) + + def test_none_algorithm(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key) + ts = s.dumps(value) + self.assertEqual(s.loads(ts), value) + + def test_algorithm_mismatch(self): + secret_key = 'predictable-key' + value = u'hello' + + s = self.make_serializer(secret_key, algorithm_name='HS256') + ts = s.dumps(value) + + s = self.make_serializer(secret_key, algorithm_name='HS384') + try: + s.loads(ts) + except idmod.BadSignature as e: + self.assertEqual(s.load_payload(e.payload), value) + else: + self.fail('Did not get algorithm mismatch') + + +class URLSafeSerializerMixin(object): + + def test_is_base62(self): + allowed = frozenset(b'0123456789abcdefghijklmnopqrstuvwxyz' + + b'ABCDEFGHIJKLMNOPQRSTUVWXYZ_-.') + objects = (['a', 'list'], 'a string', u'a unicode string \u2019', + {'a': 'dictionary'}, 42, 42.5) + s = self.make_serializer('Test') + for o in objects: + value = want_bytes(s.dumps(o)) + self.assertTrue(set(value).issubset(set(allowed))) + self.assertNotEqual(o, value) + self.assertEqual(o, s.loads(value)) + + def test_invalid_base64_does_not_fail_load_payload(self): + s = idmod.URLSafeSerializer('aha!') + self.assertRaises(idmod.BadPayload, s.load_payload, b'kZ4m3du844lIN') + + +class PickleSerializerMixin(object): + + def make_serializer(self, *args, **kwargs): + kwargs.setdefault('serializer', pickle) + return super(PickleSerializerMixin, self).make_serializer(*args, **kwargs) + + +class URLSafeSerializerTestCase(URLSafeSerializerMixin, SerializerTestCase): + serializer_class = idmod.URLSafeSerializer + + +class URLSafeTimedSerializerTestCase(URLSafeSerializerMixin, TimedSerializerTestCase): + serializer_class = idmod.URLSafeTimedSerializer + + +class PickleSerializerTestCase(PickleSerializerMixin, SerializerTestCase): + pass + + +class PickleTimedSerializerTestCase(PickleSerializerMixin, TimedSerializerTestCase): + pass + + +class PickleURLSafeSerializerTestCase(PickleSerializerMixin, URLSafeSerializerTestCase): + pass + + +class PickleURLSafeTimedSerializerTestCase(PickleSerializerMixin, URLSafeTimedSerializerTestCase): + pass + + +if __name__ == '__main__': + unittest.main()