Skip to content

Commit 53ed14e

Browse files
committed
♻️ Move functions from _utils.py to _client.py
The following functions are moved because they are only used in _client.py * port_or_default * same_origin * is_https_redirect * get_environment_proxies * is_ipv4_hostname * is_ipv6_hostname Related tests are also moved accordingly.
1 parent 41597ad commit 53ed14e

File tree

5 files changed

+189
-195
lines changed

5 files changed

+189
-195
lines changed

httpx/_client.py

Lines changed: 103 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import datetime
44
import enum
5+
import ipaddress
56
import logging
67
import ssl
78
import time
89
import typing
10+
import urllib.request
911
import warnings
1012
from contextlib import asynccontextmanager, contextmanager
1113
from types import TracebackType
@@ -46,12 +48,7 @@
4648
TimeoutTypes,
4749
)
4850
from ._urls import URL, QueryParams
49-
from ._utils import (
50-
URLPattern,
51-
get_environment_proxies,
52-
is_https_redirect,
53-
same_origin,
54-
)
51+
from ._utils import URLPattern
5552

5653
__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
5754

@@ -61,6 +58,103 @@
6158
U = typing.TypeVar("U", bound="AsyncClient")
6259

6360

61+
def _is_ipv4_hostname(hostname: str) -> bool:
62+
try:
63+
ipaddress.IPv4Address(hostname.split("/")[0])
64+
except Exception:
65+
return False
66+
return True
67+
68+
69+
def _is_ipv6_hostname(hostname: str) -> bool:
70+
try:
71+
ipaddress.IPv6Address(hostname.split("/")[0])
72+
except Exception:
73+
return False
74+
return True
75+
76+
77+
def _is_https_redirect(url: URL, location: URL) -> bool:
78+
"""
79+
Return 'True' if 'location' is a HTTPS upgrade of 'url'
80+
"""
81+
if url.host != location.host:
82+
return False
83+
84+
return (
85+
url.scheme == "http"
86+
and _port_or_default(url) == 80
87+
and location.scheme == "https"
88+
and _port_or_default(location) == 443
89+
)
90+
91+
92+
def _port_or_default(url: URL) -> int | None:
93+
if url.port is not None:
94+
return url.port
95+
return {"http": 80, "https": 443}.get(url.scheme)
96+
97+
98+
def _same_origin(url: URL, other: URL) -> bool:
99+
"""
100+
Return 'True' if the given URLs share the same origin.
101+
"""
102+
return (
103+
url.scheme == other.scheme
104+
and url.host == other.host
105+
and _port_or_default(url) == _port_or_default(other)
106+
)
107+
108+
109+
def _get_environment_proxies() -> dict[str, str | None]:
110+
"""Gets proxy information from the environment"""
111+
112+
# urllib.request.getproxies() falls back on System
113+
# Registry and Config for proxies on Windows and macOS.
114+
# We don't want to propagate non-HTTP proxies into
115+
# our configuration such as 'TRAVIS_APT_PROXY'.
116+
proxy_info = urllib.request.getproxies()
117+
mounts: dict[str, str | None] = {}
118+
119+
for scheme in ("http", "https", "all"):
120+
if proxy_info.get(scheme):
121+
hostname = proxy_info[scheme]
122+
mounts[f"{scheme}://"] = (
123+
hostname if "://" in hostname else f"http://{hostname}"
124+
)
125+
126+
no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]
127+
for hostname in no_proxy_hosts:
128+
# See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details
129+
# on how names in `NO_PROXY` are handled.
130+
if hostname == "*":
131+
# If NO_PROXY=* is used or if "*" occurs as any one of the comma
132+
# separated hostnames, then we should just bypass any information
133+
# from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore
134+
# proxies.
135+
return {}
136+
elif hostname:
137+
# NO_PROXY=.google.com is marked as "all://*.google.com,
138+
# which disables "www.google.com" but not "google.com"
139+
# NO_PROXY=google.com is marked as "all://*google.com,
140+
# which disables "www.google.com" and "google.com".
141+
# (But not "wwwgoogle.com")
142+
# NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost"
143+
# NO_PROXY=example.com,::1,localhost,192.168.0.0/16
144+
if "://" in hostname:
145+
mounts[hostname] = None
146+
elif _is_ipv4_hostname(hostname):
147+
mounts[f"all://{hostname}"] = None
148+
elif _is_ipv6_hostname(hostname):
149+
mounts[f"all://[{hostname}]"] = None
150+
elif hostname.lower() == "localhost":
151+
mounts[f"all://{hostname}"] = None
152+
else:
153+
mounts[f"all://*{hostname}"] = None
154+
155+
return mounts
156+
157+
64158
class UseClientDefault:
65159
"""
66160
For some parameters such as `auth=...` and `timeout=...` we need to be able
@@ -213,7 +307,7 @@ def _get_proxy_map(
213307
if allow_env_proxies:
214308
return {
215309
key: None if url is None else Proxy(url=url)
216-
for key, url in get_environment_proxies().items()
310+
for key, url in _get_environment_proxies().items()
217311
}
218312
return {}
219313
else:
@@ -519,8 +613,8 @@ def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
519613
"""
520614
headers = Headers(request.headers)
521615

522-
if not same_origin(url, request.url):
523-
if not is_https_redirect(request.url, url):
616+
if not _same_origin(url, request.url):
617+
if not _is_https_redirect(request.url, url):
524618
# Strip Authorization headers when responses are redirected
525619
# away from the origin. (Except for direct HTTP to HTTPS redirects.)
526620
headers.pop("Authorization", None)

httpx/_utils.py

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
from __future__ import annotations
22

3-
import ipaddress
43
import os
54
import re
65
import typing
7-
from urllib.request import getproxies
86

97
from ._types import PrimitiveData
108

@@ -27,87 +25,6 @@ def primitive_value_to_str(value: PrimitiveData) -> str:
2725
return str(value)
2826

2927

30-
def port_or_default(url: URL) -> int | None:
31-
if url.port is not None:
32-
return url.port
33-
return {"http": 80, "https": 443}.get(url.scheme)
34-
35-
36-
def same_origin(url: URL, other: URL) -> bool:
37-
"""
38-
Return 'True' if the given URLs share the same origin.
39-
"""
40-
return (
41-
url.scheme == other.scheme
42-
and url.host == other.host
43-
and port_or_default(url) == port_or_default(other)
44-
)
45-
46-
47-
def is_https_redirect(url: URL, location: URL) -> bool:
48-
"""
49-
Return 'True' if 'location' is a HTTPS upgrade of 'url'
50-
"""
51-
if url.host != location.host:
52-
return False
53-
54-
return (
55-
url.scheme == "http"
56-
and port_or_default(url) == 80
57-
and location.scheme == "https"
58-
and port_or_default(location) == 443
59-
)
60-
61-
62-
def get_environment_proxies() -> dict[str, str | None]:
63-
"""Gets proxy information from the environment"""
64-
65-
# urllib.request.getproxies() falls back on System
66-
# Registry and Config for proxies on Windows and macOS.
67-
# We don't want to propagate non-HTTP proxies into
68-
# our configuration such as 'TRAVIS_APT_PROXY'.
69-
proxy_info = getproxies()
70-
mounts: dict[str, str | None] = {}
71-
72-
for scheme in ("http", "https", "all"):
73-
if proxy_info.get(scheme):
74-
hostname = proxy_info[scheme]
75-
mounts[f"{scheme}://"] = (
76-
hostname if "://" in hostname else f"http://{hostname}"
77-
)
78-
79-
no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]
80-
for hostname in no_proxy_hosts:
81-
# See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details
82-
# on how names in `NO_PROXY` are handled.
83-
if hostname == "*":
84-
# If NO_PROXY=* is used or if "*" occurs as any one of the comma
85-
# separated hostnames, then we should just bypass any information
86-
# from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore
87-
# proxies.
88-
return {}
89-
elif hostname:
90-
# NO_PROXY=.google.com is marked as "all://*.google.com,
91-
# which disables "www.google.com" but not "google.com"
92-
# NO_PROXY=google.com is marked as "all://*google.com,
93-
# which disables "www.google.com" and "google.com".
94-
# (But not "wwwgoogle.com")
95-
# NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost"
96-
# NO_PROXY=example.com,::1,localhost,192.168.0.0/16
97-
if "://" in hostname:
98-
mounts[hostname] = None
99-
elif is_ipv4_hostname(hostname):
100-
mounts[f"all://{hostname}"] = None
101-
elif is_ipv6_hostname(hostname):
102-
mounts[f"all://[{hostname}]"] = None
103-
elif hostname.lower() == "localhost":
104-
mounts[f"all://{hostname}"] = None
105-
else:
106-
mounts[f"all://*{hostname}"] = None
107-
108-
return mounts
109-
110-
11128
def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes:
11229
return value.encode(encoding) if isinstance(value, str) else value
11330

@@ -256,19 +173,3 @@ def __lt__(self, other: URLPattern) -> bool:
256173

257174
def __eq__(self, other: typing.Any) -> bool:
258175
return isinstance(other, URLPattern) and self.pattern == other.pattern
259-
260-
261-
def is_ipv4_hostname(hostname: str) -> bool:
262-
try:
263-
ipaddress.IPv4Address(hostname.split("/")[0])
264-
except Exception:
265-
return False
266-
return True
267-
268-
269-
def is_ipv6_hostname(hostname: str) -> bool:
270-
try:
271-
ipaddress.IPv6Address(hostname.split("/")[0])
272-
except Exception:
273-
return False
274-
return True

tests/client/test_headers.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,59 @@ def test_host_with_non_default_port_in_url():
235235
def test_request_auto_headers():
236236
request = httpx.Request("GET", "https://www.example.org/")
237237
assert "host" in request.headers
238+
239+
240+
def test_same_origin():
241+
origin = httpx.URL("https://example.com")
242+
request = httpx.Request("GET", "HTTPS://EXAMPLE.COM:443")
243+
244+
client = httpx.Client()
245+
headers = client._redirect_headers(request, origin, "GET")
246+
247+
assert headers["Host"] == request.url.netloc.decode("ascii")
248+
249+
250+
def test_not_same_origin():
251+
origin = httpx.URL("https://example.com")
252+
request = httpx.Request("GET", "HTTP://EXAMPLE.COM:80")
253+
254+
client = httpx.Client()
255+
headers = client._redirect_headers(request, origin, "GET")
256+
257+
assert headers["Host"] == origin.netloc.decode("ascii")
258+
259+
260+
def test_is_https_redirect():
261+
url = httpx.URL("https://example.com")
262+
request = httpx.Request(
263+
"GET", "http://example.com", headers={"Authorization": "empty"}
264+
)
265+
266+
client = httpx.Client()
267+
headers = client._redirect_headers(request, url, "GET")
268+
269+
assert "Authorization" in headers
270+
271+
272+
def test_is_not_https_redirect():
273+
url = httpx.URL("https://www.example.com")
274+
request = httpx.Request(
275+
"GET", "http://example.com", headers={"Authorization": "empty"}
276+
)
277+
278+
client = httpx.Client()
279+
headers = client._redirect_headers(request, url, "GET")
280+
281+
assert "Authorization" not in headers
282+
283+
284+
def test_is_not_https_redirect_if_not_default_ports():
285+
url = httpx.URL("https://example.com:1337")
286+
request = httpx.Request(
287+
"GET", "http://example.com:9999", headers={"Authorization": "empty"}
288+
)
289+
290+
client = httpx.Client()
291+
headers = client._redirect_headers(request, url, "GET")
292+
293+
assert "Authorization" not in headers

tests/client/test_proxies.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import os
2+
13
import httpcore
24
import pytest
35

46
import httpx
7+
from httpx._client import _get_environment_proxies
58

69

710
def url_to_origin(url: str) -> httpcore.URL:
@@ -263,3 +266,29 @@ def test_proxy_with_mounts():
263266

264267
transport = client._transport_for_url(httpx.URL("http://example.com"))
265268
assert transport == proxy_transport
269+
270+
271+
@pytest.mark.parametrize(
272+
["environment", "proxies"],
273+
[
274+
({}, {}),
275+
({"HTTP_PROXY": "http://127.0.0.1"}, {"http://": "http://127.0.0.1"}),
276+
(
277+
{"https_proxy": "http://127.0.0.1", "HTTP_PROXY": "https://127.0.0.1"},
278+
{"https://": "http://127.0.0.1", "http://": "https://127.0.0.1"},
279+
),
280+
({"all_proxy": "http://127.0.0.1"}, {"all://": "http://127.0.0.1"}),
281+
({"TRAVIS_APT_PROXY": "http://127.0.0.1"}, {}),
282+
({"no_proxy": "127.0.0.1"}, {"all://127.0.0.1": None}),
283+
({"no_proxy": "192.168.0.0/16"}, {"all://192.168.0.0/16": None}),
284+
({"no_proxy": "::1"}, {"all://[::1]": None}),
285+
({"no_proxy": "localhost"}, {"all://localhost": None}),
286+
({"no_proxy": "github.com"}, {"all://*github.com": None}),
287+
({"no_proxy": ".github.com"}, {"all://*.github.com": None}),
288+
({"no_proxy": "http://github.com"}, {"http://github.com": None}),
289+
],
290+
)
291+
def test_get_environment_proxies(environment, proxies):
292+
os.environ.update(environment)
293+
294+
assert _get_environment_proxies() == proxies

0 commit comments

Comments
 (0)