From 85400d8d6751071ef78f042d1efa72bdcf76cc0e Mon Sep 17 00:00:00 2001
From: "John L. Villalovos"
Date: Tue, 6 Sep 2016 16:24:35 -0700
Subject: [PATCH 1/7] Allow use of 'no_proxy' in the proxies argument
Add the ability to add 'no_proxy' and a value to the 'proxies'
dictionary argument.
https://github.com/kennethreitz/requests/issues/2817
Closes gh-2817
---
requests/sessions.py | 12 ++++++----
requests/utils.py | 42 ++++++++++++++++++++++++++++-------
tests/test_utils.py | 52 +++++++++++++++++++++++++++++++++++++++++---
3 files changed, 91 insertions(+), 15 deletions(-)
diff --git a/requests/sessions.py b/requests/sessions.py
index 7983282a..51bad2ca 100644
--- a/requests/sessions.py
+++ b/requests/sessions.py
@@ -231,13 +231,16 @@ class SessionRedirectMixin(object):
:rtype: dict
"""
+ proxies = proxies if proxies is not None else {}
headers = prepared_request.headers
url = prepared_request.url
scheme = urlparse(url).scheme
- new_proxies = proxies.copy() if proxies is not None else {}
+ new_proxies = proxies.copy()
+ no_proxy = proxies.get('no_proxy')
- if self.trust_env and not should_bypass_proxies(url):
- environ_proxies = get_environ_proxies(url)
+ bypass_proxy = should_bypass_proxies(url, no_proxy=no_proxy)
+ if self.trust_env and not bypass_proxy:
+ environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)
proxy = environ_proxies.get(scheme, environ_proxies.get('all'))
@@ -651,7 +654,8 @@ class Session(SessionRedirectMixin):
# Gather clues from the surrounding environment.
if self.trust_env:
# Set environment's proxies.
- env_proxies = get_environ_proxies(url) or {}
+ no_proxy = proxies.get('no_proxy') if proxies is not None else None
+ env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
for (k, v) in env_proxies.items():
proxies.setdefault(k, v)
diff --git a/requests/utils.py b/requests/utils.py
index 47325090..e5ecd350 100644
--- a/requests/utils.py
+++ b/requests/utils.py
@@ -11,6 +11,7 @@ that are also useful for external consumption.
import cgi
import codecs
import collections
+import contextlib
import io
import os
import re
@@ -554,7 +555,29 @@ def is_valid_cidr(string_network):
return True
-def should_bypass_proxies(url):
+@contextlib.contextmanager
+def set_environ(env_name, value):
+ """Set the environment variable 'env_name' to 'value'
+
+ Save previous value, yield, and then restore the previous value stored in
+ the environment variable 'env_name'.
+
+ If 'value' is None, do nothing"""
+ if value is not None:
+ old_value = os.environ.get(env_name)
+ os.environ[env_name] = value
+ try:
+ yield
+ finally:
+ if value is None:
+ return
+ if old_value is None:
+ del os.environ[env_name]
+ else:
+ os.environ[env_name] = old_value
+
+
+def should_bypass_proxies(url, no_proxy):
"""
Returns whether we should bypass proxies or not.
@@ -564,7 +587,9 @@ def should_bypass_proxies(url):
# First check whether no_proxy is defined. If it is, check that the URL
# we're getting isn't in the no_proxy list.
- no_proxy = get_proxy('no_proxy')
+ no_proxy_arg = no_proxy
+ if no_proxy is None:
+ no_proxy = get_proxy('no_proxy')
netloc = urlparse(url).netloc
if no_proxy:
@@ -597,10 +622,11 @@ def should_bypass_proxies(url):
# of Python 2.6, so allow this call to fail. Only catch the specific
# exceptions we've seen, though: this call failing in other ways can reveal
# legitimate problems.
- try:
- bypass = proxy_bypass(netloc)
- except (TypeError, socket.gaierror):
- bypass = False
+ with set_environ('no_proxy', no_proxy_arg):
+ try:
+ bypass = proxy_bypass(netloc)
+ except (TypeError, socket.gaierror):
+ bypass = False
if bypass:
return True
@@ -608,13 +634,13 @@ def should_bypass_proxies(url):
return False
-def get_environ_proxies(url):
+def get_environ_proxies(url, no_proxy):
"""
Return a dict of environment proxies.
:rtype: dict
"""
- if should_bypass_proxies(url):
+ if should_bypass_proxies(url, no_proxy=no_proxy):
return {}
else:
return getproxies()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 1edf6218..11ebf617 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -161,7 +161,7 @@ class TestGetEnvironProxies:
'http://localhost.localdomain:5000/v1.0/',
))
def test_bypass(self, url):
- assert get_environ_proxies(url) == {}
+ assert get_environ_proxies(url, no_proxy=None) == {}
@pytest.mark.parametrize(
'url', (
@@ -170,7 +170,32 @@ class TestGetEnvironProxies:
'http://www.requests.com/',
))
def test_not_bypass(self, url):
- assert get_environ_proxies(url) != {}
+ assert get_environ_proxies(url, no_proxy=None) != {}
+
+ @pytest.mark.parametrize(
+ 'url', (
+ 'http://192.168.1.1:5000/',
+ 'http://192.168.1.1/',
+ 'http://www.requests.com/',
+ ))
+ def test_bypass_no_proxy_keyword(self, url):
+ no_proxy = '192.168.1.1,requests.com'
+ assert get_environ_proxies(url, no_proxy=no_proxy) == {}
+
+ @pytest.mark.parametrize(
+ 'url', (
+ 'http://192.168.0.1:5000/',
+ 'http://192.168.0.1/',
+ 'http://172.16.1.1/',
+ 'http://172.16.1.1:5000/',
+ 'http://localhost.localdomain:5000/v1.0/',
+ ))
+ def test_not_bypass_no_proxy_keyword(self, url, monkeypatch):
+ # This is testing that the 'no_proxy' argument overrides the
+ # environment variable 'no_proxy'
+ monkeypatch.setenv('http_proxy', 'http://proxy.example.com:3128/')
+ no_proxy = '192.168.1.1,requests.com'
+ assert get_environ_proxies(url, no_proxy=no_proxy) != {}
class TestIsIPv4Address:
@@ -525,7 +550,7 @@ def test_should_bypass_proxies(url, expected, monkeypatch):
"""
monkeypatch.setenv('no_proxy', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1')
monkeypatch.setenv('NO_PROXY', '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1')
- assert should_bypass_proxies(url) == expected
+ assert should_bypass_proxies(url, no_proxy=None) == expected
@pytest.mark.parametrize(
@@ -553,3 +578,24 @@ def test_add_dict_to_cookiejar(cookiejar):
)
def test_unicode_is_ascii(value, expected):
assert unicode_is_ascii(value) is expected
+
+
+@pytest.mark.parametrize(
+ 'url, expected', (
+ ('http://192.168.0.1:5000/', True),
+ ('http://192.168.0.1/', True),
+ ('http://172.16.1.1/', True),
+ ('http://172.16.1.1:5000/', True),
+ ('http://localhost.localdomain:5000/v1.0/', True),
+ ('http://172.16.1.12/', False),
+ ('http://172.16.1.12:5000/', False),
+ ('http://google.com:5000/v1.0/', False),
+ ))
+def test_should_bypass_proxies_no_proxy(
+ url, expected, monkeypatch):
+ """Tests for function should_bypass_proxies to check if proxy
+ can be bypassed or not using the 'no_proxy' argument
+ """
+ no_proxy = '192.168.0.0/24,127.0.0.1,localhost.localdomain,172.16.1.1'
+ # Test 'no_proxy' argument
+ assert should_bypass_proxies(url, no_proxy=no_proxy) == expected
From 70f31a3166c1f9470b5cfad888f828357c1daadd Mon Sep 17 00:00:00 2001
From: jonathan vanasco
Date: Fri, 10 Feb 2017 13:53:23 -0500
Subject: [PATCH 2/7] * initial attempt at `get_redirect_target` * removing the
`i` from the redirect detection while-loop
---
AUTHORS.rst | 2 ++
HISTORY.rst | 9 +++++++++
requests/sessions.py | 27 +++++++++++++++-----------
tests/test_requests.py | 43 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 70 insertions(+), 11 deletions(-)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index d29fa812..48cd155b 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -177,3 +177,5 @@ Patches and Suggestions
- Andrii Soldatenko (`@a_soldatenko `_)
- Moinuddin Quadri (`@moin18 `_)
- Matt Kohl (`@mattkohl `_)
+- Jonathan Vanasco (`@jvanasco `_)
+
diff --git a/HISTORY.rst b/HISTORY.rst
index 5ec8bb21..c26036c1 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -3,6 +3,15 @@
Release History
---------------
+**Unreleased**
++++++++++++++++++++
+
+- The behavior of ``SessionRedirectMixin`` was slightly altered.
+ ``resolve_redirects`` will now detect a redirect by calling
+ ``get_redirect_target(response)`` instead of directly
+ querying ``Response.is_redirect`` and ``Response.headers['location']``.
+ Advanced users will be able to process malformed redirects more easily.
+
2.13.0 (2017-01-24)
+++++++++++++++++++
diff --git a/requests/sessions.py b/requests/sessions.py
index 7983282a..72ef179f 100644
--- a/requests/sessions.py
+++ b/requests/sessions.py
@@ -86,35 +86,39 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
class SessionRedirectMixin(object):
+
+ def get_redirect_target(self, resp):
+ """Receives a Response. Returns a redirect URI or ``None``"""
+ if resp.is_redirect:
+ return resp.headers['location']
+ return None
+
def resolve_redirects(self, resp, req, stream=False, timeout=None,
verify=True, cert=None, proxies=None, **adapter_kwargs):
"""Receives a Response. Returns a generator of Responses."""
- i = 0
hist = [] # keep track of history
- while resp.is_redirect:
+ url = self.get_redirect_target(resp)
+ while url:
prepared_request = req.copy()
- if i > 0:
- # Update history and keep track of redirects.
- hist.append(resp)
- new_hist = list(hist)
- resp.history = new_hist
+ # Update history and keep track of redirects.
+ # resp.history must ignore the original request in this loop
+ hist.append(resp)
+ resp.history = hist[1:]
try:
resp.content # Consume socket so it can be released
except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
resp.raw.read(decode_content=False)
- if i >= self.max_redirects:
+ if len(resp.history) >= self.max_redirects:
raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp)
# Release the connection back into the pool.
resp.close()
- url = resp.headers['location']
-
# Handle redirection without scheme (see: RFC 1808 Section 4)
if url.startswith('//'):
parsed_rurl = urlparse(resp.url)
@@ -192,7 +196,8 @@ class SessionRedirectMixin(object):
extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
- i += 1
+ # extract redirect url, if any, for the next loop
+ url = self.get_redirect_target(resp)
yield resp
def rebuild_auth(self, prepared_request, response):
diff --git a/tests/test_requests.py b/tests/test_requests.py
index fd35103b..cd4c68db 100755
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -1740,6 +1740,49 @@ class TestRequests:
assert 'Transfer-Encoding' in prepared_request.headers
assert 'Content-Length' not in prepared_request.headers
+ def test_custom_redirect_mixin(self, httpbin):
+ """Tests a custom mixin to overwrite ``get_redirect_target``.
+
+ Ensures a subclassed ``requests.Session`` can handle a certain type of
+ malformed redirect responses.
+
+ 1. original request receives a proper response: 302 redirect
+ 2. following the redirect, a malformed response is given:
+ status code = HTTP 200
+ location = alternate url
+ 3. the custom session catches the edge case and follows the redirect
+ """
+ url_final = httpbin('html')
+ querystring_malformed = urlencode({'location': url_final})
+ url_redirect_malformed = httpbin('response-headers?%s' % querystring_malformed)
+ querystring_redirect = urlencode({'url': url_redirect_malformed})
+ url_redirect = httpbin('redirect-to?%s' % querystring_redirect)
+ urls_test = [url_redirect,
+ url_redirect_malformed,
+ url_final,
+ ]
+
+ class CustomRedirectSession(requests.Session):
+ def get_redirect_target(self, resp):
+ # default behavior
+ if resp.is_redirect:
+ return resp.headers['location']
+ # edge case - check to see if 'location' is in headers anyways
+ location = resp.headers.get('location')
+ if location and (location != resp.url):
+ return location
+ return None
+
+ session = CustomRedirectSession()
+ r = session.get(urls_test[0])
+ assert len(r.history) == 2
+ assert r.status_code == 200
+ assert r.history[0].status_code == 302
+ assert r.history[0].is_redirect
+ assert r.history[1].status_code == 200
+ assert not r.history[1].is_redirect
+ assert r.url == urls_test[2]
+
class TestCaseInsensitiveDict:
From e7b7574cfe0df12c19193757c0493582818cf5fb Mon Sep 17 00:00:00 2001
From: Kenneth Reitz
Date: Sat, 11 Feb 2017 15:12:04 -0500
Subject: [PATCH 3/7] hhg2p
---
docs/_templates/sidebarintro.html | 8 ++++++++
docs/_templates/sidebarlogo.html | 9 +++++++++
2 files changed, 17 insertions(+)
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
index 457688d9..d325d845 100644
--- a/docs/_templates/sidebarintro.html
+++ b/docs/_templates/sidebarintro.html
@@ -24,6 +24,14 @@
Say Thanks!
Join Mailing List.
+Hitchhiker's Guide to Python
+
+This guide is now available in tangible book form!
+
+
+
+All proceeds are being directly
+
Other Projects
More Kenneth Reitz projects:
diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html
index 798f8091..271af911 100644
--- a/docs/_templates/sidebarlogo.html
+++ b/docs/_templates/sidebarlogo.html
@@ -28,6 +28,15 @@
+Hitchhiker's Guide to Python
+
+This guide is now available in tangible book form!
+
+
+
+All proceeds are being directly donated to the DjangoGirls organization.
+
+
Other Projects
More Kenneth Reitz projects:
From f2fe7358469cf844bce04bf04478b8f95fd634f9 Mon Sep 17 00:00:00 2001
From: Kenneth Reitz
Date: Sat, 11 Feb 2017 15:16:09 -0500
Subject: [PATCH 4/7] fixes
---
docs/_templates/sidebarintro.html | 5 +++--
docs/_templates/sidebarlogo.html | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
index d325d845..e2aebc9f 100644
--- a/docs/_templates/sidebarintro.html
+++ b/docs/_templates/sidebarintro.html
@@ -24,13 +24,14 @@
Say Thanks!
Join Mailing List.
-Hitchhiker's Guide to Python
+The Hitchhiker's Guide to Python
This guide is now available in tangible book form!

-All proceeds are being directly
+
All proceeds are being directly donated to the DjangoGirls organization.
+
Other Projects
diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html
index 271af911..0c8bccf8 100644
--- a/docs/_templates/sidebarlogo.html
+++ b/docs/_templates/sidebarlogo.html
@@ -28,7 +28,7 @@
-Hitchhiker's Guide to Python
+The Hitchhiker's Guide to Python
This guide is now available in tangible book form!
From 6cbebf782a4309010eef3bba088867a206f617b4 Mon Sep 17 00:00:00 2001
From: Kenneth Reitz
Date: Sat, 11 Feb 2017 15:17:27 -0500
Subject: [PATCH 5/7] fixes
---
docs/_templates/sidebarintro.html | 19 +++++++++++--------
docs/_templates/sidebarlogo.html | 15 ++++++++-------
2 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
index e2aebc9f..fc9a659a 100644
--- a/docs/_templates/sidebarintro.html
+++ b/docs/_templates/sidebarintro.html
@@ -14,6 +14,17 @@
human beings.
+
+The Hitchhiker's Guide to Python
+
+This guide is now available in tangible book form!
+
+
+
+All proceeds are being directly donated to the DjangoGirls organization.
+
+
+
Stay Informed
Receive updates on new releases and upcoming projects.
@@ -24,14 +35,6 @@
Say Thanks!
Join Mailing List.
-The Hitchhiker's Guide to Python
-
-This guide is now available in tangible book form!
-
-
-
-All proceeds are being directly donated to the DjangoGirls organization.
-
Other Projects
diff --git a/docs/_templates/sidebarlogo.html b/docs/_templates/sidebarlogo.html
index 0c8bccf8..4c05a538 100644
--- a/docs/_templates/sidebarlogo.html
+++ b/docs/_templates/sidebarlogo.html
@@ -21,13 +21,6 @@
-If you enjoy using this project, Say Thanks!
-
-
-
-
-
The Hitchhiker's Guide to Python
This guide is now available in tangible book form!
@@ -37,6 +30,14 @@
All proceeds are being directly donated to the DjangoGirls organization.
+If you enjoy using this project, Say Thanks!
+
+
+
+
+
+
Other Projects
More Kenneth Reitz projects:
From 90f3842ed60f91ff101189f792ce21f7c762b1a0 Mon Sep 17 00:00:00 2001
From: Kenneth Reitz
Date: Sun, 12 Feb 2017 02:18:50 -0500
Subject: [PATCH 6/7] edmsynths
---
docs/_templates/sidebarintro.html | 1 +
docs/_templates/sidebarlogo.html | 1 +
2 files changed, 2 insertions(+)
diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html
index fc9a659a..fe113734 100644
--- a/docs/_templates/sidebarintro.html
+++ b/docs/_templates/sidebarintro.html
@@ -40,6 +40,7 @@
More Kenneth Reitz projects: