diff --git a/0001-Backport-limits-for-multiple-form-parts.patch b/0001-Backport-limits-for-multiple-form-parts.patch index 2fe8cff..1270f3d 100644 --- a/0001-Backport-limits-for-multiple-form-parts.patch +++ b/0001-Backport-limits-for-multiple-form-parts.patch @@ -1,7 +1,7 @@ From 722f58f8221c013e2dd6cf1fde59fd686619f483 Mon Sep 17 00:00:00 2001 From: "Brian C. Lane" Date: Tue, 18 Apr 2023 15:57:50 -0700 -Subject: [PATCH] Backport limits for multiple form parts +Subject: [PATCH 1/2] Backport limits for multiple form parts This fixes CVE-2023-25577, and is backported from the fix for 2.2.3 here: diff --git a/0002-Backport-fix-for-cookie-prefixed-with.patch b/0002-Backport-fix-for-cookie-prefixed-with.patch new file mode 100644 index 0000000..0cf3aac --- /dev/null +++ b/0002-Backport-fix-for-cookie-prefixed-with.patch @@ -0,0 +1,74 @@ +From ab00d73bdc48b7a2d06a44b989b4f161310768a6 Mon Sep 17 00:00:00 2001 +From: "Brian C. Lane" +Date: Tue, 18 Apr 2023 16:44:22 -0700 +Subject: [PATCH 2/2] Backport fix for cookie prefixed with = + +This fixes CVE-2023-23934 by backporting the fix from upstream: +https://github.com/pallets/werkzeug/commit/cf275f42acad1b5950c50ffe8ef58fe62cdce028 + +Resolves: rhbz#2170317 +--- + tests/test_http.py | 6 ++++-- + werkzeug/_internal.py | 11 +++++++---- + 2 files changed, 11 insertions(+), 6 deletions(-) + +diff --git a/tests/test_http.py b/tests/test_http.py +index b77e3c38..c1582fd6 100644 +--- a/tests/test_http.py ++++ b/tests/test_http.py +@@ -354,13 +354,15 @@ class TestHTTPUtility(object): + def test_cookies(self): + strict_eq( + dict(http.parse_cookie('dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cd' +- 'c762809248d4beed; a=42; b="\\\";"')), ++ 'c762809248d4beed; a=42; b="\\\";";' ++ '==__Host-eq=bad;__Host-eq=good;')), + { + 'CP': u'null*', + 'PHPSESSID': u'0a539d42abc001cdc762809248d4beed', + 'a': u'42', + 'dismiss-top': u'6', +- 'b': u'\";' ++ 'b': u'\";', ++ '__Host-eq': u'good', + } + ) + rv = http.dump_cookie('foo', 'bar baz blub', 360, httponly=True, +diff --git a/werkzeug/_internal.py b/werkzeug/_internal.py +index 3d1ee090..0bf9fb2a 100644 +--- a/werkzeug/_internal.py ++++ b/werkzeug/_internal.py +@@ -44,7 +44,7 @@ _octal_re = re.compile(b'\\\\[0-3][0-7][0-7]') + _quote_re = re.compile(b'[\\\\].') + _legal_cookie_chars_re = b'[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]' + _cookie_re = re.compile(b""" +- (?P[^=]+) ++ (?P[^=]*) + \s*=\s* + (?P + "(?:[^\\\\"]|\\\\.)*" | +@@ -276,15 +276,18 @@ def _cookie_parse_impl(b): + """Lowlevel cookie parsing facility that operates on bytes.""" + i = 0 + n = len(b) ++ b += b";" + + while i < n: +- match = _cookie_re.search(b + b';', i) ++ match = _cookie_re.match(b, i) + if not match: + break + +- key = match.group('key').strip() +- value = match.group('val') + i = match.end(0) ++ key = match.group('key').strip() ++ if not key: ++ continue ++ value = match.group('val') or b"" + + # Ignore parameters. We have no interest in them. + if key.lower() not in _cookie_params: +-- +2.40.0 + diff --git a/python-werkzeug.spec b/python-werkzeug.spec index b629aff..347f825 100644 --- a/python-werkzeug.spec +++ b/python-werkzeug.spec @@ -21,6 +21,7 @@ Source0: https://files.pythonhosted.org/packages/source/W/Werkzeug/%{srcn Source1: werkzeug-sphinx-theme.tar.gz Patch0001: 0001-Backport-limits-for-multiple-form-parts.patch +Patch0002: 0002-Backport-fix-for-cookie-prefixed-with.patch BuildArch: noarch @@ -145,6 +146,8 @@ popd - Add new test for formdata - Backport fix for CVE-2023-25577 Resolves: rhbz#2188442 +- Backport fix for CVE-2023-23934 + Resolves: rhbz#2170317 * Fri Jun 22 2018 Charalampos Stratakis - 0.12.2-4 - Use python3-sphinx for the docs diff --git a/tests/scripts/run_tests.sh b/tests/scripts/run_tests.sh index f2315d8..7602612 100755 --- a/tests/scripts/run_tests.sh +++ b/tests/scripts/run_tests.sh @@ -1,4 +1,4 @@ #!/usr/bin/bash set -eux -pytest-3 ./test_wsgi.py ./test_formparser.py +pytest-3 ./test_wsgi.py ./test_formparser.py ./test_http.py diff --git a/tests/scripts/test_http.py b/tests/scripts/test_http.py new file mode 100644 index 0000000..bfd4f56 --- /dev/null +++ b/tests/scripts/test_http.py @@ -0,0 +1,545 @@ +# -*- coding: utf-8 -*- +""" + tests.http + ~~~~~~~~~~ + + HTTP parsing utilities. + + :copyright: (c) 2014 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import pytest + +from datetime import datetime + +#from tests import strict_eq +from werkzeug._compat import itervalues, wsgi_encoding_dance + +from werkzeug import http, datastructures +from werkzeug.test import create_environ + + +def strict_eq(x, y): + '''Equality test bypassing the implicit string conversion in Python 2''' + __tracebackhide__ = True + assert x == y + assert issubclass(type(x), type(y)) or issubclass(type(y), type(x)) + if isinstance(x, dict) and isinstance(y, dict): + x = sorted(x.items()) + y = sorted(y.items()) + elif isinstance(x, set) and isinstance(y, set): + x = sorted(x) + y = sorted(y) + assert repr(x) == repr(y) + + +class TestHTTPUtility(object): + + def test_accept(self): + a = http.parse_accept_header('en-us,ru;q=0.5') + assert list(itervalues(a)) == ['en-us', 'ru'] + assert a.best == 'en-us' + assert a.find('ru') == 1 + pytest.raises(ValueError, a.index, 'de') + assert a.to_header() == 'en-us,ru;q=0.5' + + def test_mime_accept(self): + a = http.parse_accept_header('text/xml,application/xml,' + 'application/xhtml+xml,' + 'application/foo;quiet=no; bar=baz;q=0.6,' + 'text/html;q=0.9,text/plain;q=0.8,' + 'image/png,*/*;q=0.5', + datastructures.MIMEAccept) + pytest.raises(ValueError, lambda: a['missing']) + assert a['image/png'] == 1 + assert a['text/plain'] == 0.8 + assert a['foo/bar'] == 0.5 + assert a['application/foo;quiet=no; bar=baz'] == 0.6 + assert a[a.find('foo/bar')] == ('*/*', 0.5) + + def test_accept_matches(self): + a = http.parse_accept_header('text/xml,application/xml,application/xhtml+xml,' + 'text/html;q=0.9,text/plain;q=0.8,' + 'image/png', datastructures.MIMEAccept) + assert a.best_match(['text/html', 'application/xhtml+xml']) == \ + 'application/xhtml+xml' + assert a.best_match(['text/html']) == 'text/html' + assert a.best_match(['foo/bar']) is None + assert a.best_match(['foo/bar', 'bar/foo'], default='foo/bar') == 'foo/bar' + assert a.best_match(['application/xml', 'text/xml']) == 'application/xml' + + def test_charset_accept(self): + a = http.parse_accept_header('ISO-8859-1,utf-8;q=0.7,*;q=0.7', + datastructures.CharsetAccept) + assert a['iso-8859-1'] == a['iso8859-1'] + assert a['iso-8859-1'] == 1 + assert a['UTF8'] == 0.7 + assert a['ebcdic'] == 0.7 + + def test_language_accept(self): + a = http.parse_accept_header('de-AT,de;q=0.8,en;q=0.5', + datastructures.LanguageAccept) + assert a.best == 'de-AT' + assert 'de_AT' in a + assert 'en' in a + assert a['de-at'] == 1 + assert a['en'] == 0.5 + + def test_set_header(self): + hs = http.parse_set_header('foo, Bar, "Blah baz", Hehe') + assert 'blah baz' in hs + assert 'foobar' not in hs + assert 'foo' in hs + assert list(hs) == ['foo', 'Bar', 'Blah baz', 'Hehe'] + hs.add('Foo') + assert hs.to_header() == 'foo, Bar, "Blah baz", Hehe' + + def test_list_header(self): + hl = http.parse_list_header('foo baz, blah') + assert hl == ['foo baz', 'blah'] + + def test_dict_header(self): + d = http.parse_dict_header('foo="bar baz", blah=42') + assert d == {'foo': 'bar baz', 'blah': '42'} + + def test_cache_control_header(self): + cc = http.parse_cache_control_header('max-age=0, no-cache') + assert cc.max_age == 0 + assert cc.no_cache + cc = http.parse_cache_control_header('private, community="UCI"', None, + datastructures.ResponseCacheControl) + assert cc.private + assert cc['community'] == 'UCI' + + c = datastructures.ResponseCacheControl() + assert c.no_cache is None + assert c.private is None + c.no_cache = True + assert c.no_cache == '*' + c.private = True + assert c.private == '*' + del c.private + assert c.private is None + assert c.to_header() == 'no-cache' + + def test_authorization_header(self): + a = http.parse_authorization_header('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') + assert a.type == 'basic' + assert a.username == 'Aladdin' + assert a.password == 'open sesame' + + a = http.parse_authorization_header('''Digest username="Mufasa", + realm="testrealm@host.invalid", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + uri="/dir/index.html", + qop=auth, + nc=00000001, + cnonce="0a4f113b", + response="6629fae49393a05397450978507c4ef1", + opaque="5ccc069c403ebaf9f0171e9517f40e41"''') + assert a.type == 'digest' + assert a.username == 'Mufasa' + assert a.realm == 'testrealm@host.invalid' + assert a.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093' + assert a.uri == '/dir/index.html' + assert 'auth' in a.qop + assert a.nc == '00000001' + assert a.cnonce == '0a4f113b' + assert a.response == '6629fae49393a05397450978507c4ef1' + assert a.opaque == '5ccc069c403ebaf9f0171e9517f40e41' + + a = http.parse_authorization_header('''Digest username="Mufasa", + realm="testrealm@host.invalid", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + uri="/dir/index.html", + response="e257afa1414a3340d93d30955171dd0e", + opaque="5ccc069c403ebaf9f0171e9517f40e41"''') + assert a.type == 'digest' + assert a.username == 'Mufasa' + assert a.realm == 'testrealm@host.invalid' + assert a.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093' + assert a.uri == '/dir/index.html' + assert a.response == 'e257afa1414a3340d93d30955171dd0e' + assert a.opaque == '5ccc069c403ebaf9f0171e9517f40e41' + + assert http.parse_authorization_header('') is None + assert http.parse_authorization_header(None) is None + assert http.parse_authorization_header('foo') is None + + def test_www_authenticate_header(self): + wa = http.parse_www_authenticate_header('Basic realm="WallyWorld"') + assert wa.type == 'basic' + assert wa.realm == 'WallyWorld' + wa.realm = 'Foo Bar' + assert wa.to_header() == 'Basic realm="Foo Bar"' + + wa = http.parse_www_authenticate_header('''Digest + realm="testrealm@host.com", + qop="auth,auth-int", + nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", + opaque="5ccc069c403ebaf9f0171e9517f40e41"''') + assert wa.type == 'digest' + assert wa.realm == 'testrealm@host.com' + assert 'auth' in wa.qop + assert 'auth-int' in wa.qop + assert wa.nonce == 'dcd98b7102dd2f0e8b11d0f600bfb0c093' + assert wa.opaque == '5ccc069c403ebaf9f0171e9517f40e41' + + wa = http.parse_www_authenticate_header('broken') + assert wa.type == 'broken' + + assert not http.parse_www_authenticate_header('').type + assert not http.parse_www_authenticate_header('') + + def test_etags(self): + assert http.quote_etag('foo') == '"foo"' + assert http.quote_etag('foo', True) == 'W/"foo"' + assert http.unquote_etag('"foo"') == ('foo', False) + assert http.unquote_etag('W/"foo"') == ('foo', True) + es = http.parse_etags('"foo", "bar", W/"baz", blar') + assert sorted(es) == ['bar', 'blar', 'foo'] + assert 'foo' in es + assert 'baz' not in es + assert es.contains_weak('baz') + assert 'blar' in es + assert es.contains_raw('W/"baz"') + assert es.contains_raw('"foo"') + assert sorted(es.to_header().split(', ')) == ['"bar"', '"blar"', '"foo"', 'W/"baz"'] + + def test_etags_nonzero(self): + etags = http.parse_etags('W/"foo"') + assert bool(etags) + assert etags.contains_raw('W/"foo"') + + def test_parse_date(self): + assert http.parse_date('Sun, 06 Nov 1994 08:49:37 GMT ') == datetime( + 1994, 11, 6, 8, 49, 37) + assert http.parse_date('Sunday, 06-Nov-94 08:49:37 GMT') == datetime(1994, 11, 6, 8, 49, 37) + assert http.parse_date(' Sun Nov 6 08:49:37 1994') == datetime(1994, 11, 6, 8, 49, 37) + assert http.parse_date('foo') is None + + def test_parse_date_overflows(self): + assert http.parse_date(' Sun 02 Feb 1343 08:49:37 GMT') == datetime(1343, 2, 2, 8, 49, 37) + assert http.parse_date('Thu, 01 Jan 1970 00:00:00 GMT') == datetime(1970, 1, 1, 0, 0) + assert http.parse_date('Thu, 33 Jan 1970 00:00:00 GMT') is None + + def test_remove_entity_headers(self): + now = http.http_date() + headers1 = [('Date', now), ('Content-Type', 'text/html'), ('Content-Length', '0')] + headers2 = datastructures.Headers(headers1) + + http.remove_entity_headers(headers1) + assert headers1 == [('Date', now)] + + http.remove_entity_headers(headers2) + assert headers2 == datastructures.Headers([(u'Date', now)]) + + def test_remove_hop_by_hop_headers(self): + headers1 = [('Connection', 'closed'), ('Foo', 'bar'), + ('Keep-Alive', 'wtf')] + headers2 = datastructures.Headers(headers1) + + http.remove_hop_by_hop_headers(headers1) + assert headers1 == [('Foo', 'bar')] + + http.remove_hop_by_hop_headers(headers2) + assert headers2 == datastructures.Headers([('Foo', 'bar')]) + + def test_parse_options_header(self): + assert http.parse_options_header(None) == \ + ('', {}) + assert http.parse_options_header("") == \ + ('', {}) + assert http.parse_options_header(r'something; foo="other\"thing"') == \ + ('something', {'foo': 'other"thing'}) + assert http.parse_options_header(r'something; foo="other\"thing"; meh=42') == \ + ('something', {'foo': 'other"thing', 'meh': '42'}) + assert http.parse_options_header(r'something; foo="other\"thing"; meh=42; bleh') == \ + ('something', {'foo': 'other"thing', 'meh': '42', 'bleh': None}) + assert http.parse_options_header('something; foo="other;thing"; meh=42; bleh') == \ + ('something', {'foo': 'other;thing', 'meh': '42', 'bleh': None}) + assert http.parse_options_header('something; foo="otherthing"; meh=; bleh') == \ + ('something', {'foo': 'otherthing', 'meh': None, 'bleh': None}) + # Issue #404 + assert http.parse_options_header('multipart/form-data; name="foo bar"; ' + 'filename="bar foo"') == \ + ('multipart/form-data', {'name': 'foo bar', 'filename': 'bar foo'}) + # Examples from RFC + assert http.parse_options_header('audio/*; q=0.2, audio/basic') == \ + ('audio/*', {'q': '0.2'}) + assert http.parse_options_header('audio/*; q=0.2, audio/basic', multiple=True) == \ + ('audio/*', {'q': '0.2'}, "audio/basic", {}) + assert http.parse_options_header( + 'text/plain; q=0.5, text/html\n ' + 'text/x-dvi; q=0.8, text/x-c', + multiple=True) == \ + ('text/plain', {'q': '0.5'}, "text/html", {}, + "text/x-dvi", {'q': '0.8'}, "text/x-c", {}) + assert http.parse_options_header('text/plain; q=0.5, text/html\n' + ' ' + 'text/x-dvi; q=0.8, text/x-c') == \ + ('text/plain', {'q': '0.5'}) + # Issue #932 + assert http.parse_options_header( + 'form-data; ' + 'name="a_file"; ' + 'filename*=UTF-8\'\'' + '"%c2%a3%20and%20%e2%82%ac%20rates"') == \ + ('form-data', {'name': 'a_file', + 'filename': u'\xa3 and \u20ac rates'}) + assert http.parse_options_header( + 'form-data; ' + 'name*=UTF-8\'\'"%C5%AAn%C4%ADc%C5%8Dde%CC%BD"; ' + 'filename="some_file.txt"') == \ + ('form-data', {'name': u'\u016an\u012dc\u014dde\u033d', + 'filename': 'some_file.txt'}) + + def test_parse_options_header_broken_values(self): + # Issue #995 + assert http.parse_options_header(' ') == ('', {}) + assert http.parse_options_header(' , ') == ('', {}) + assert http.parse_options_header(' ; ') == ('', {}) + assert http.parse_options_header(' ,; ') == ('', {}) + assert http.parse_options_header(' , a ') == ('', {}) + assert http.parse_options_header(' ; a ') == ('', {}) + + def test_dump_options_header(self): + assert http.dump_options_header('foo', {'bar': 42}) == \ + 'foo; bar=42' + assert http.dump_options_header('foo', {'bar': 42, 'fizz': None}) in \ + ('foo; bar=42; fizz', 'foo; fizz; bar=42') + + def test_dump_header(self): + assert http.dump_header([1, 2, 3]) == '1, 2, 3' + assert http.dump_header([1, 2, 3], allow_token=False) == '"1", "2", "3"' + assert http.dump_header({'foo': 'bar'}, allow_token=False) == 'foo="bar"' + assert http.dump_header({'foo': 'bar'}) == 'foo=bar' + + def test_is_resource_modified(self): + env = create_environ() + + # ignore POST + env['REQUEST_METHOD'] = 'POST' + assert not http.is_resource_modified(env, etag='testing') + env['REQUEST_METHOD'] = 'GET' + + # etagify from data + pytest.raises(TypeError, http.is_resource_modified, env, + data='42', etag='23') + env['HTTP_IF_NONE_MATCH'] = http.generate_etag(b'awesome') + assert not http.is_resource_modified(env, data=b'awesome') + + env['HTTP_IF_MODIFIED_SINCE'] = http.http_date(datetime(2008, 1, 1, 12, 30)) + assert not http.is_resource_modified(env, + last_modified=datetime(2008, 1, 1, 12, 00)) + assert http.is_resource_modified(env, + last_modified=datetime(2008, 1, 1, 13, 00)) + + def test_is_resource_modified_for_range_requests(self): + env = create_environ() + + env['HTTP_IF_MODIFIED_SINCE'] = http.http_date(datetime(2008, 1, 1, 12, 30)) + env['HTTP_IF_RANGE'] = http.generate_etag(b'awesome_if_range') + # Range header not present, so If-Range should be ignored + assert not http.is_resource_modified(env, data=b'not_the_same', + ignore_if_range=False, + last_modified=datetime(2008, 1, 1, 12, 30)) + + env['HTTP_RANGE'] = '' + assert not http.is_resource_modified(env, data=b'awesome_if_range', + ignore_if_range=False) + assert http.is_resource_modified(env, data=b'not_the_same', + ignore_if_range=False) + + env['HTTP_IF_RANGE'] = http.http_date(datetime(2008, 1, 1, 13, 30)) + assert http.is_resource_modified(env, last_modified=datetime(2008, 1, 1, 14, 00), + ignore_if_range=False) + assert not http.is_resource_modified(env, last_modified=datetime(2008, 1, 1, 13, 30), + ignore_if_range=False) + assert http.is_resource_modified(env, last_modified=datetime(2008, 1, 1, 13, 30), + ignore_if_range=True) + + def test_date_formatting(self): + assert http.cookie_date(0) == 'Thu, 01-Jan-1970 00:00:00 GMT' + assert http.cookie_date(datetime(1970, 1, 1)) == 'Thu, 01-Jan-1970 00:00:00 GMT' + assert http.http_date(0) == 'Thu, 01 Jan 1970 00:00:00 GMT' + assert http.http_date(datetime(1970, 1, 1)) == 'Thu, 01 Jan 1970 00:00:00 GMT' + + def test_cookies(self): + strict_eq( + dict(http.parse_cookie('dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cd' + 'c762809248d4beed; a=42; b="\\\";";' + '==__Host-eq=bad;__Host-eq=good;')), + { + 'CP': u'null*', + 'PHPSESSID': u'0a539d42abc001cdc762809248d4beed', + 'a': u'42', + 'dismiss-top': u'6', + 'b': u'\";', + '__Host-eq': u'good', + } + ) + rv = http.dump_cookie('foo', 'bar baz blub', 360, httponly=True, + sync_expires=False) + assert type(rv) is str + assert set(rv.split('; ')) == set(['HttpOnly', 'Max-Age=360', + 'Path=/', 'foo="bar baz blub"']) + + strict_eq(dict(http.parse_cookie('fo234{=bar; blub=Blah')), + {'fo234{': u'bar', 'blub': u'Blah'}) + + def test_cookie_quoting(self): + val = http.dump_cookie("foo", "?foo") + strict_eq(val, 'foo="?foo"; Path=/') + strict_eq(dict(http.parse_cookie(val)), {'foo': u'?foo'}) + + strict_eq(dict(http.parse_cookie(r'foo="foo\054bar"')), + {'foo': u'foo,bar'}) + + def test_cookie_domain_resolving(self): + val = http.dump_cookie('foo', 'bar', domain=u'\N{SNOWMAN}.com') + strict_eq(val, 'foo=bar; Domain=xn--n3h.com; Path=/') + + def test_cookie_unicode_dumping(self): + val = http.dump_cookie('foo', u'\N{SNOWMAN}') + h = datastructures.Headers() + h.add('Set-Cookie', val) + assert h['Set-Cookie'] == 'foo="\\342\\230\\203"; Path=/' + + cookies = http.parse_cookie(h['Set-Cookie']) + assert cookies['foo'] == u'\N{SNOWMAN}' + + def test_cookie_unicode_keys(self): + # Yes, this is technically against the spec but happens + val = http.dump_cookie(u'fö', u'fö') + assert val == wsgi_encoding_dance(u'fö="f\\303\\266"; Path=/', 'utf-8') + cookies = http.parse_cookie(val) + assert cookies[u'fö'] == u'fö' + + def test_cookie_unicode_parsing(self): + # This is actually a correct test. This is what is being submitted + # by firefox if you set an unicode cookie and we get the cookie sent + # in on Python 3 under PEP 3333. + cookies = http.parse_cookie(u'fö=fö') + assert cookies[u'fö'] == u'fö' + + def test_cookie_domain_encoding(self): + val = http.dump_cookie('foo', 'bar', domain=u'\N{SNOWMAN}.com') + strict_eq(val, 'foo=bar; Domain=xn--n3h.com; Path=/') + + val = http.dump_cookie('foo', 'bar', domain=u'.\N{SNOWMAN}.com') + strict_eq(val, 'foo=bar; Domain=.xn--n3h.com; Path=/') + + val = http.dump_cookie('foo', 'bar', domain=u'.foo.com') + strict_eq(val, 'foo=bar; Domain=.foo.com; Path=/') + + +class TestRange(object): + + def test_if_range_parsing(self): + rv = http.parse_if_range_header('"Test"') + assert rv.etag == 'Test' + assert rv.date is None + assert rv.to_header() == '"Test"' + + # weak information is dropped + rv = http.parse_if_range_header('W/"Test"') + assert rv.etag == 'Test' + assert rv.date is None + assert rv.to_header() == '"Test"' + + # broken etags are supported too + rv = http.parse_if_range_header('bullshit') + assert rv.etag == 'bullshit' + assert rv.date is None + assert rv.to_header() == '"bullshit"' + + rv = http.parse_if_range_header('Thu, 01 Jan 1970 00:00:00 GMT') + assert rv.etag is None + assert rv.date == datetime(1970, 1, 1) + assert rv.to_header() == 'Thu, 01 Jan 1970 00:00:00 GMT' + + for x in '', None: + rv = http.parse_if_range_header(x) + assert rv.etag is None + assert rv.date is None + assert rv.to_header() == '' + + def test_range_parsing(self): + rv = http.parse_range_header('bytes=52') + assert rv is None + + rv = http.parse_range_header('bytes=52-') + assert rv.units == 'bytes' + assert rv.ranges == [(52, None)] + assert rv.to_header() == 'bytes=52-' + + rv = http.parse_range_header('bytes=52-99') + assert rv.units == 'bytes' + assert rv.ranges == [(52, 100)] + assert rv.to_header() == 'bytes=52-99' + + rv = http.parse_range_header('bytes=52-99,-1000') + assert rv.units == 'bytes' + assert rv.ranges == [(52, 100), (-1000, None)] + assert rv.to_header() == 'bytes=52-99,-1000' + + rv = http.parse_range_header('bytes = 1 - 100') + assert rv.units == 'bytes' + assert rv.ranges == [(1, 101)] + assert rv.to_header() == 'bytes=1-100' + + rv = http.parse_range_header('AWesomes=0-999') + assert rv.units == 'awesomes' + assert rv.ranges == [(0, 1000)] + assert rv.to_header() == 'awesomes=0-999' + + rv = http.parse_range_header('bytes=-') + assert rv is None + + rv = http.parse_range_header('bytes=bullshit') + assert rv is None + + rv = http.parse_range_header('bytes=bullshit-1') + assert rv is None + + rv = http.parse_range_header('bytes=-bullshit') + assert rv is None + + rv = http.parse_range_header('bytes=52-99, bullshit') + assert rv is None + + def test_content_range_parsing(self): + rv = http.parse_content_range_header('bytes 0-98/*') + assert rv.units == 'bytes' + assert rv.start == 0 + assert rv.stop == 99 + assert rv.length is None + assert rv.to_header() == 'bytes 0-98/*' + + rv = http.parse_content_range_header('bytes 0-98/*asdfsa') + assert rv is None + + rv = http.parse_content_range_header('bytes 0-99/100') + assert rv.to_header() == 'bytes 0-99/100' + rv.start = None + rv.stop = None + assert rv.units == 'bytes' + assert rv.to_header() == 'bytes */100' + + rv = http.parse_content_range_header('bytes */100') + assert rv.start is None + assert rv.stop is None + assert rv.length == 100 + assert rv.units == 'bytes' + + +class TestRegression(object): + + def test_best_match_works(self): + # was a bug in 0.6 + rv = http.parse_accept_header('foo=,application/xml,application/xhtml+xml,' + 'text/html;q=0.9,text/plain;q=0.8,' + 'image/png,*/*;q=0.5', + datastructures.MIMEAccept).best_match(['foo/bar']) + assert rv == 'foo/bar'