Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the no_proxy env var mechanism in the HTTP client #4445

Merged
merged 23 commits into from
Mar 22, 2021
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
01303f4
Added no proxy check for issue #4431
scirelli-c1 Dec 15, 2019
40a109e
Updated CONTRIBUTORS file.
scirelli-c1 Dec 15, 2019
a9d23cd
Added CHANGES file
scirelli-c1 Dec 15, 2019
1ba92e3
Merge branch 'master' into no_proxy2
scirelli Feb 4, 2021
5fc8f4c
Fixed linting errors
scirelli Feb 4, 2021
8061077
Moved proxy_bypass into client.py
scirelli Feb 4, 2021
7566d86
Drop unrelated gitignore change
webknjaz Mar 6, 2021
532d979
Rephrase the change note to be more specific
webknjaz Mar 6, 2021
9d90569
Merge branch 'master' of github.com:aio-libs/aiohttp into no_proxy2
scirelli Mar 7, 2021
d3aa076
Merge branch 'no_proxy2' of github.com:scirelli/aiohttp into no_proxy2
scirelli Mar 7, 2021
8199ab4
Factored out proxy check, and wrote unit tests
scirelli Mar 8, 2021
4b2a032
Refactor to remove for loop.
scirelli Mar 8, 2021
445428c
Update aiohttp/client.py
scirelli Mar 9, 2021
74a17c2
Update aiohttp/helpers.py
scirelli Mar 9, 2021
37393c6
Updated based on PR comments.
scirelli Mar 9, 2021
5df1e7b
Turn the parametrize args into separate tuple items
webknjaz Mar 18, 2021
d1966e4
Add explicit pytest ids for the positive case
webknjaz Mar 18, 2021
a73ddac
Parametrize the negative tests
webknjaz Mar 18, 2021
3353d1d
Adjust the formatting of the tests
webknjaz Mar 18, 2021
e717b2b
Updated based on PR comments.
scirelli Mar 20, 2021
ad462ec
Updated based on PR comments
scirelli Mar 22, 2021
9796cb5
Drop unnecessary fixtures
webknjaz Mar 22, 2021
9e4408a
Format the usefixtures invocation
webknjaz Mar 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/4431.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed HTTP client requests to honor ``no_proxy`` environment variables.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ Stanislav Prokop
Stefan Tjarks
Stepan Pletnev
Stephan Jaensch
Stephen Cirelli
Stephen Granade
Steven Seguin
Sunghyun Hwang
Expand Down
10 changes: 4 additions & 6 deletions aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import sys
import traceback
import warnings
from contextlib import suppress
from types import SimpleNamespace, TracebackType
from typing import (
Any,
Expand Down Expand Up @@ -80,7 +81,7 @@
BasicAuth,
TimeoutHandle,
ceil_timeout,
proxies_from_env,
get_env_proxy_for_url,
sentinel,
strip_auth_from_url,
)
Expand Down Expand Up @@ -444,11 +445,8 @@ async def _request(
if proxy is not None:
proxy = URL(proxy)
elif self._trust_env:
for scheme, proxy_info in proxies_from_env().items():
if scheme == url.scheme:
proxy = proxy_info.proxy
proxy_auth = proxy_info.proxy_auth
break
with suppress(LookupError):
proxy, proxy_auth = get_env_proxy_for_url(url)

req = self._request_class(
method,
Expand Down
16 changes: 15 additions & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
cast,
)
from urllib.parse import quote
from urllib.request import getproxies
from urllib.request import getproxies, proxy_bypass

import async_timeout
from multidict import CIMultiDict, MultiDict, MultiDictProxy
Expand Down Expand Up @@ -269,6 +269,20 @@ def proxies_from_env() -> Dict[str, ProxyInfo]:
return ret


def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
"""Get a permitted proxy for the given URL from the env."""
if url.host is not None and proxy_bypass(url.host):
raise LookupError(f"Proxying is disallowed for `{url.host!r}`")

proxies_in_env = proxies_from_env()
try:
proxy_info = proxies_in_env[url.scheme]
except KeyError:
raise LookupError(f"No proxies found for `{url!s}` in the env")
else:
return proxy_info.proxy, proxy_info.proxy_auth


@dataclasses.dataclass(frozen=True)
class MimeType:
type: str
Expand Down
92 changes: 92 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import platform
from math import isclose, modf
from unittest import mock
from urllib.request import getproxies_environment

import pytest
from multidict import CIMultiDict, MultiDict
Expand Down Expand Up @@ -515,6 +516,97 @@ def test_proxies_from_env_http_with_auth(mocker) -> None:
assert proxy_auth.encoding == "latin1"


# --------------------- get_env_proxy_for_url ------------------------------
webknjaz marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture
def proxy_env_vars(monkeypatch, request):
for schema in getproxies_environment().keys():
monkeypatch.delenv(f"{schema}_proxy", False)

for proxy_type, proxy_list in request.param.items():
monkeypatch.setenv(proxy_type, proxy_list)

return request.param


@pytest.mark.parametrize(
("proxy_env_vars", "url_input", "expected_err_msg"),
(
(
{"no_proxy": "aiohttp.io"},
"http://aiohttp.io/path",
r"Proxying is disallowed for `'aiohttp.io'`",
),
(
{"no_proxy": "aiohttp.io,proxy.com"},
"http://aiohttp.io/path",
r"Proxying is disallowed for `'aiohttp.io'`",
),
(
{"http_proxy": "http://example.com"},
"https://aiohttp.io/path",
r"No proxies found for `https://aiohttp.io/path` in the env",
),
(
{"https_proxy": "https://example.com"},
"http://aiohttp.io/path",
r"No proxies found for `http://aiohttp.io/path` in the env",
),
(
{},
"https://aiohttp.io/path",
r"No proxies found for `https://aiohttp.io/path` in the env",
),
(
{"https_proxy": "https://example.com"},
"",
r"No proxies found for `` in the env",
),
),
indirect=["proxy_env_vars"],
ids=(
"url_matches_the_no_proxy_list",
"url_matches_the_no_proxy_list_multiple",
"url_scheme_does_not_match_http_proxy_list",
"url_scheme_does_not_match_https_proxy_list",
"no_proxies_are_set",
"url_is_empty",
),
)
def test_get_env_proxy_for_url_negative(
monkeypatch, proxy_env_vars, url_input, expected_err_msg
) -> None:
webknjaz marked this conversation as resolved.
Show resolved Hide resolved
url = URL(url_input)
with pytest.raises(LookupError, match=expected_err_msg):
helpers.get_env_proxy_for_url(url)


@pytest.mark.parametrize(
("proxy_env_vars", "url_input"),
(
({"http_proxy": "http://example.com"}, "http://aiohttp.io/path"),
({"https_proxy": "http://example.com"}, "https://aiohttp.io/path"),
(
{"http_proxy": "http://example.com,http://proxy.org"},
"http://aiohttp.io/path",
),
),
indirect=["proxy_env_vars"],
ids=(
"url_scheme_match_http_proxy_list",
"url_scheme_match_https_proxy_list",
"url_scheme_match_http_proxy_list_multiple",
),
)
def test_get_env_proxy_for_url(monkeypatch, proxy_env_vars, url_input) -> None:
webknjaz marked this conversation as resolved.
Show resolved Hide resolved
url = URL(url_input)
proxy, proxy_auth = helpers.get_env_proxy_for_url(url)
proxy_list = proxy_env_vars[url.scheme + "_proxy"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally, it's best to avoid dynamism like this in tests because it makes it harder to reason about what's expected because one should first dig through several levels of indirection correctly.

But since I don't want an infinite number of reviews on this PR, I'll just accept it. I think it's rather good now.

assert proxy == URL(proxy_list)
assert proxy_auth is None


# ------------- set_result / set_exception ----------------------


Expand Down