Skip to content

Commit

Permalink
Add support for the no_proxy env var mechanism in the HTTP client
Browse files Browse the repository at this point in the history
PR #4445 by @scirelli.
Fixes #4431.

Co-authored-by: Steve Cirelli <[email protected]>
Co-authored-by: Sviatoslav Sydorenko <[email protected]>
(cherry picked from commit 1a4126a)
  • Loading branch information
scirelli committed Mar 23, 2021
1 parent 8090594 commit 86515c8
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 7 deletions.
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 @@ -254,6 +254,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 @@ -8,6 +8,7 @@
import sys
import traceback
import warnings
from contextlib import suppress
from types import SimpleNamespace, TracebackType
from typing import (
Any,
Expand Down Expand Up @@ -76,8 +77,8 @@
BasicAuth,
TimeoutHandle,
ceil_timeout,
get_env_proxy_for_url,
get_running_loop,
proxies_from_env,
sentinel,
strip_auth_from_url,
)
Expand Down Expand Up @@ -483,11 +484,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
18 changes: 17 additions & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import base64
import binascii
import cgi
import dataclasses
import datetime
import functools
import inspect
Expand Down Expand Up @@ -41,7 +42,7 @@
cast,
)
from urllib.parse import quote
from urllib.request import getproxies
from urllib.request import getproxies, proxy_bypass

import async_timeout
import attr
Expand Down Expand Up @@ -298,6 +299,21 @@ def isasyncgenfunction(obj: Any) -> bool:


@attr.s(auto_attribs=True, frozen=True, slots=True)
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
subtype: str
Expand Down
91 changes: 91 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import tempfile
from math import isclose, modf
from unittest import mock
from urllib.request import getproxies_environment

import pytest
from multidict import MultiDict
Expand Down Expand Up @@ -497,6 +498,96 @@ async def test_get_running_loop_ok(loop) -> None:
assert helpers.get_running_loop() is loop


# --------------------- get_env_proxy_for_url ------------------------------


@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",
),
)
@pytest.mark.usefixtures("proxy_env_vars")
def test_get_env_proxy_for_url_negative(url_input, expected_err_msg) -> None:
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(proxy_env_vars, url_input) -> None:
url = URL(url_input)
proxy, proxy_auth = helpers.get_env_proxy_for_url(url)
proxy_list = proxy_env_vars[url.scheme + "_proxy"]
assert proxy == URL(proxy_list)
assert proxy_auth is None


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


Expand Down

0 comments on commit 86515c8

Please sign in to comment.