Skip to content

Commit 4989447

Browse files
committed
ngclient: Add proxy environment variable handling
urllib3 does not handle this but we do want to support proxy users. The environment variable handling is slightly simplified from the requests implementation. Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent bc99c31 commit 4989447

File tree

3 files changed

+107
-6
lines changed

3 files changed

+107
-6
lines changed

tests/test_updater_ng.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,10 @@ def test_non_existing_target_file(self) -> None:
330330
def test_user_agent(self) -> None:
331331
# test default
332332
self.updater.refresh()
333-
session = self.updater._fetcher._poolManager
334-
ua = session.headers["User-Agent"]
333+
poolmgr = self.updater._fetcher._proxy_env.get_pool_manager(
334+
"http", "localhost"
335+
)
336+
ua = poolmgr.headers["User-Agent"]
335337
self.assertEqual(ua[:11], "python-tuf/")
336338

337339
# test custom UA
@@ -343,8 +345,10 @@ def test_user_agent(self) -> None:
343345
config=UpdaterConfig(app_user_agent="MyApp/1.2.3"),
344346
)
345347
updater.refresh()
346-
session = updater._fetcher._poolManager
347-
ua = session.headers["User-Agent"]
348+
poolmgr = updater._fetcher._proxy_env.get_pool_manager(
349+
"http", "localhost"
350+
)
351+
ua = poolmgr.headers["User-Agent"]
348352

349353
self.assertEqual(ua[:23], "MyApp/1.2.3 python-tuf/")
350354

tuf/ngclient/_internal/proxy.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Proxy environment variable handling with Urllib3"""
5+
6+
from typing import Any
7+
from urllib.request import getproxies
8+
9+
from urllib3 import BaseHTTPResponse, PoolManager, ProxyManager
10+
from urllib3.util.url import parse_url
11+
12+
13+
# TODO: ProxyEnvironment could implement the whole PoolManager.RequestMethods
14+
# Mixin: We only need request() so nothing else is currently implemented
15+
class ProxyEnvironment:
16+
"""A PoolManager manager for automatic proxy handling based on env variables
17+
18+
Keeps track of PoolManagers for different proxy urls based on proxy
19+
environment variables. Use `get_pool_manager()` or `request()` to access
20+
the right manager for a scheme/host.
21+
22+
Supports '*_proxy' variables, with special handling for 'no_proxy' and
23+
'all_proxy'.
24+
"""
25+
26+
def __init__(
27+
self,
28+
**kw_args: Any, # noqa: ANN401
29+
) -> None:
30+
self._pool_managers: dict[str | None, PoolManager] = {}
31+
self._kw_args = kw_args
32+
33+
self._proxies = getproxies()
34+
self._all_proxy = self._proxies.pop("all", None)
35+
no_proxy = self._proxies.pop("no", None)
36+
if no_proxy is None:
37+
self._no_proxy_hosts = []
38+
else:
39+
self._no_proxy_hosts = [
40+
h for h in no_proxy.replace(" ", "").split(",") if h
41+
]
42+
43+
def _get_proxy(self, scheme: str | None, host: str | None) -> str | None:
44+
"""Get a proxy url for scheme and host based on proxy env variables"""
45+
46+
if host is None:
47+
# urllib3 only handles http/https but we can do something reasonable
48+
# even for schemes that don't require host (like file)
49+
return None
50+
51+
# does host match "no_proxy" hosts?
52+
for no_proxy_host in self._no_proxy_hosts:
53+
# exact hostname match or parent domain match
54+
if host == no_proxy_host or host.endswith(f".{no_proxy_host}"):
55+
return None
56+
57+
if scheme in self._proxies:
58+
return self._proxies[scheme]
59+
if self._all_proxy is not None:
60+
return self._all_proxy
61+
62+
return None
63+
64+
def get_pool_manager(
65+
self, scheme: str | None, host: str | None
66+
) -> PoolManager:
67+
"""Get a poolmanager for scheme and host.
68+
69+
Returns a ProxyManager if that is correct based on current proxy env
70+
variables, otherwise returns a PoolManager
71+
"""
72+
73+
proxy = self._get_proxy(scheme, host)
74+
if proxy not in self._pool_managers:
75+
if proxy is None:
76+
self._pool_managers[proxy] = PoolManager(**self._kw_args)
77+
else:
78+
self._pool_managers[proxy] = ProxyManager(
79+
proxy,
80+
**self._kw_args,
81+
)
82+
83+
return self._pool_managers[proxy]
84+
85+
def request(
86+
self,
87+
method: str,
88+
url: str,
89+
**request_kw: Any, # noqa: ANN401
90+
) -> BaseHTTPResponse:
91+
"""Make a request using a PoolManager chosen based on url and
92+
proxy environment variables.
93+
"""
94+
u = parse_url(url)
95+
manager = self.get_pool_manager(u.scheme, u.host)
96+
return manager.request(method, url, **request_kw)

tuf/ngclient/urllib3_fetcher.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import tuf
1717
from tuf.api import exceptions
18+
from tuf.ngclient._internal.proxy import ProxyEnvironment
1819
from tuf.ngclient.fetcher import FetcherInterface
1920

2021
if TYPE_CHECKING:
@@ -49,7 +50,7 @@ def __init__(
4950
if app_user_agent is not None:
5051
ua = f"{app_user_agent} {ua}"
5152

52-
self._poolManager = urllib3.PoolManager(headers={"User-Agent": ua})
53+
self._proxy_env = ProxyEnvironment(headers={"User-Agent": ua})
5354

5455
def _fetch(self, url: str) -> Iterator[bytes]:
5556
"""Fetch the contents of HTTP/HTTPS url from a remote server.
@@ -72,7 +73,7 @@ def _fetch(self, url: str) -> Iterator[bytes]:
7273
# - connect timeout (max delay before first byte is received)
7374
# - read (gap) timeout (max delay between bytes received)
7475
try:
75-
response = self._poolManager.request(
76+
response = self._proxy_env.request(
7677
"GET",
7778
url,
7879
preload_content=False,

0 commit comments

Comments
 (0)