From fd4332916fd980f522ef13790a8ae68e55b8dde9 Mon Sep 17 00:00:00 2001 From: Nate Prewitt Date: Fri, 15 Jul 2016 01:31:14 -0400 Subject: [PATCH 1/3] raise InvalidHeader on multiple Location values --- requests/exceptions.py | 4 ++++ requests/sessions.py | 7 ++++++- tests/test_requests.py | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/requests/exceptions.py b/requests/exceptions.py index 4a6a41c4..72fdfd05 100644 --- a/requests/exceptions.py +++ b/requests/exceptions.py @@ -83,6 +83,10 @@ class InvalidURL(RequestException, ValueError): """ The URL provided was somehow invalid. """ +class InvalidHeader(RequestException, ValueError): + """The header value provided was somehow invalid.""" + + class ChunkedEncodingError(RequestException): """The server declared chunked encoding but sent an invalid chunk.""" diff --git a/requests/sessions.py b/requests/sessions.py index 795c914f..d51897fa 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -20,7 +20,8 @@ from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT from .hooks import default_hooks, dispatch_hook from .utils import to_key_val_list, default_headers, to_native_string from .exceptions import ( - TooManyRedirects, InvalidScheme, ChunkedEncodingError, ContentDecodingError) + TooManyRedirects, InvalidScheme, ChunkedEncodingError, + ContentDecodingError, InvalidHeader) from .packages.urllib3._collections import RecentlyUsedContainer from .structures import CaseInsensitiveDict @@ -98,6 +99,10 @@ class SessionRedirectMixin(object): request = response.request while response.is_redirect: + if len(response.raw.headers.getlist('location')) > 1: + raise InvalidHeader('Response contains multiple Location headers. ' + 'Unable to perform redirect.') + prepared_request = request.copy() if redirect_count > 0: diff --git a/tests/test_requests.py b/tests/test_requests.py index 3effcf94..f8536f32 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -22,7 +22,7 @@ from requests.cookies import cookiejar_from_dict, morsel_to_cookie from requests.exceptions import ( ConnectionError, ConnectTimeout, InvalidScheme, InvalidURL, MissingScheme, ReadTimeout, Timeout, RetryError, TooManyRedirects, - ProxyError) + ProxyError, InvalidHeader) from requests.models import PreparedRequest from requests.structures import CaseInsensitiveDict from requests.sessions import SessionRedirectMixin @@ -222,6 +222,20 @@ class TestRequests: assert r.history[0].status_code == 303 assert r.history[0].is_redirect + def test_multiple_location_headers(self, httpbin): + headers = [('Location', 'http://example.com'), + ('Location', 'https://example.com/1')] + params = '&'.join(['%s=%s' % (k, v) for k, v in headers]) + ses = requests.Session() + req = requests.Request('GET', httpbin('response-headers?%s' % params)) + prep = ses.prepare_request(req) + resp = ses.send(prep) + # change response to redirect + resp.status_code = 302 + with pytest.raises(InvalidHeader): + # next triggers yield on generator + next(ses.resolve_redirects(resp, prep)) + # def test_HTTP_302_ALLOW_REDIRECT_POST(self): # r = requests.post(httpbin('status', '302'), data={'some': 'data'}) # self.assertEqual(r.status_code, 200) From c418c4c4aa4e8491db814e5139b836d8b6b42811 Mon Sep 17 00:00:00 2001 From: Nate Prewitt Date: Sat, 16 Jul 2016 10:37:13 -0400 Subject: [PATCH 2/3] moving implementation details into util func --- requests/sessions.py | 4 ++-- requests/utils.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/requests/sessions.py b/requests/sessions.py index d51897fa..a5083e4d 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -29,7 +29,7 @@ from .adapters import HTTPAdapter from .utils import ( requote_uri, get_environ_proxies, get_netrc_auth, should_bypass_proxies, - get_auth_from_url + get_auth_from_url, is_valid_location ) from .status_codes import codes @@ -99,7 +99,7 @@ class SessionRedirectMixin(object): request = response.request while response.is_redirect: - if len(response.raw.headers.getlist('location')) > 1: + if not is_valid_location(response): raise InvalidHeader('Response contains multiple Location headers. ' 'Unable to perform redirect.') diff --git a/requests/utils.py b/requests/utils.py index f9629112..17e5b428 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -665,6 +665,17 @@ def parse_header_links(value): return links +def is_valid_location(response): + """Verify that multiple Location headers weren't + returned from the last response. + """ + headers = getattr(response.raw, 'headers', None) + if headers is not None: + getlist = getattr(headers, 'getlist', None) + if getlist is not None: + return len(getlist('location')) <= 1 + # If response.raw isn't urllib3-like we can't reliably check this + return True # Null bytes; no need to recreate these on each call to guess_json_utf _null = '\x00'.encode('ascii') # encoding to ASCII for Python 3 From 715830fe2113c74db3da071484fe23fab743c1fe Mon Sep 17 00:00:00 2001 From: Nate Prewitt Date: Tue, 26 Jul 2016 11:35:56 -0600 Subject: [PATCH 3/3] removing incorrect param from resolve_redirects call --- tests/test_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_requests.py b/tests/test_requests.py index f8536f32..e8f7f8aa 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -1626,7 +1626,7 @@ def test_requests_are_updated_each_time(httpbin): r0 = session.send(prep) assert r0.request.method == 'POST' assert session.calls[-1] == SendCall((r0.request,), {}) - redirect_generator = session.resolve_redirects(r0, prep) + redirect_generator = session.resolve_redirects(r0) default_keyword_args = { 'stream': False, 'verify': True,