Skip to content

Commit 92067fe

Browse files
author
Ondrej Filip
committed
Make no_proxy really match ip ranges
1 parent 3e53fe3 commit 92067fe

3 files changed

Lines changed: 86 additions & 23 deletions

File tree

httpx/_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
TimeoutTypes,
4747
)
4848
from ._urls import URL, QueryParams
49-
from ._utils import URLPattern, get_environment_proxies
49+
from ._utils import URLPattern, get_environment_proxies, build_url_pattern
5050

5151
if typing.TYPE_CHECKING:
5252
import ssl # pragma: no cover
@@ -695,7 +695,7 @@ def __init__(
695695
transport=transport,
696696
)
697697
self._mounts: dict[URLPattern, BaseTransport | None] = {
698-
URLPattern(key): None
698+
build_url_pattern(key): None
699699
if proxy is None
700700
else self._init_proxy_transport(
701701
proxy,
@@ -710,7 +710,7 @@ def __init__(
710710
}
711711
if mounts is not None:
712712
self._mounts.update(
713-
{URLPattern(key): transport for key, transport in mounts.items()}
713+
{build_url_pattern(key): transport for key, transport in mounts.items()}
714714
)
715715

716716
self._mounts = dict(sorted(self._mounts.items()))
@@ -1410,7 +1410,7 @@ def __init__(
14101410
)
14111411

14121412
self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
1413-
URLPattern(key): None
1413+
build_url_pattern(key): None
14141414
if proxy is None
14151415
else self._init_proxy_transport(
14161416
proxy,
@@ -1425,7 +1425,7 @@ def __init__(
14251425
}
14261426
if mounts is not None:
14271427
self._mounts.update(
1428-
{URLPattern(key): transport for key, transport in mounts.items()}
1428+
{build_url_pattern(key): transport for key, transport in mounts.items()}
14291429
)
14301430
self._mounts = dict(sorted(self._mounts.items()))
14311431

httpx/_utils.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import typing
77
from urllib.request import getproxies
88

9+
from abc import abstractmethod
10+
911
from ._types import PrimitiveData
1012

1113
if typing.TYPE_CHECKING: # pragma: no cover
@@ -123,24 +125,35 @@ def peek_filelike_length(stream: typing.Any) -> int | None:
123125
return length
124126

125127

126-
class URLPattern:
128+
class Pattern(typing.Protocol):
129+
@abstractmethod
130+
def matches(self, other: URL) -> bool:
131+
pass
132+
133+
@property
134+
@abstractmethod
135+
def priority(self) -> tuple[int, int, int]:
136+
pass
137+
138+
139+
class WildcardURLPattern(Pattern):
127140
"""
128141
A utility class currently used for making lookups against proxy keys...
129142
130143
# Wildcard matching...
131-
>>> pattern = URLPattern("all://")
144+
>>> pattern = WildcardURLPattern("all://")
132145
>>> pattern.matches(httpx.URL("http://example.com"))
133146
True
134147
135148
# Witch scheme matching...
136-
>>> pattern = URLPattern("https://")
149+
>>> pattern = WildcardURLPattern("https://")
137150
>>> pattern.matches(httpx.URL("https://example.com"))
138151
True
139152
>>> pattern.matches(httpx.URL("http://example.com"))
140153
False
141154
142155
# With domain matching...
143-
>>> pattern = URLPattern("https://example.com")
156+
>>> pattern = WildcardURLPattern("https://example.com")
144157
>>> pattern.matches(httpx.URL("https://example.com"))
145158
True
146159
>>> pattern.matches(httpx.URL("http://example.com"))
@@ -149,7 +162,7 @@ class URLPattern:
149162
False
150163
151164
# Wildcard scheme, with domain matching...
152-
>>> pattern = URLPattern("all://example.com")
165+
>>> pattern = WildcardURLPattern("all://example.com")
153166
>>> pattern.matches(httpx.URL("https://example.com"))
154167
True
155168
>>> pattern.matches(httpx.URL("http://example.com"))
@@ -158,7 +171,7 @@ class URLPattern:
158171
False
159172
160173
# With port matching...
161-
>>> pattern = URLPattern("https://example.com:1234")
174+
>>> pattern = WildcardURLPattern("https://example.com:1234")
162175
>>> pattern.matches(httpx.URL("https://example.com:1234"))
163176
True
164177
>>> pattern.matches(httpx.URL("https://example.com"))
@@ -229,7 +242,51 @@ def __lt__(self, other: URLPattern) -> bool:
229242
return self.priority < other.priority
230243

231244
def __eq__(self, other: typing.Any) -> bool:
232-
return isinstance(other, URLPattern) and self.pattern == other.pattern
245+
return isinstance(other, WildcardURLPattern) and self.pattern == other.pattern
246+
247+
248+
class IPNetPattern(Pattern):
249+
def __init__(self, ip_net: str) -> None:
250+
try:
251+
addr, range = ip_net.split('/', 1)
252+
if addr[0] == '[' and addr[-1] == ']':
253+
addr = addr[1:-1]
254+
ip_net = f'{addr}/{range}'
255+
except ValueError:
256+
pass # not a range
257+
self.net = ipaddress.ip_network(ip_net)
258+
259+
def matches(self, other: URL):
260+
try:
261+
return ipaddress.ip_address(other.host) in self.net
262+
except ValueError:
263+
return False
264+
265+
@property
266+
def priority(self) -> tuple[int, int, int]:
267+
return -1, 0, 0 # higher priority than URLPatterns
268+
269+
def __hash__(self) -> int:
270+
return hash(self.net)
271+
272+
def __lt__(self, other: URLPattern) -> bool:
273+
return self.priority < other.priority
274+
275+
def __eq__(self, other: typing.Any) -> bool:
276+
return isinstance(other, IPNetPattern) and self.net == other.net
277+
278+
279+
URLPattern = IPNetPattern | WildcardURLPattern
280+
281+
282+
def build_url_pattern(pattern: str) -> URLPattern:
283+
try:
284+
proto, rest = pattern.split('://', 1)
285+
if proto == 'all' and '/' in rest:
286+
return IPNetPattern(rest)
287+
except ValueError: # covers .split() and IPNetPattern
288+
pass
289+
return WildcardURLPattern(pattern)
233290

234291

235292
def is_ipv4_hostname(hostname: str) -> bool:
@@ -245,4 +302,4 @@ def is_ipv6_hostname(hostname: str) -> bool:
245302
ipaddress.IPv6Address(hostname.split("/")[0])
246303
except Exception:
247304
return False
248-
return True
305+
return True

tests/test_utils.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
import httpx
9-
from httpx._utils import URLPattern, get_environment_proxies
9+
from httpx._utils import build_url_pattern, get_environment_proxies
1010

1111

1212
@pytest.mark.parametrize(
@@ -128,24 +128,30 @@ def test_get_environment_proxies(environment, proxies):
128128
("http://", "https://example.com", False),
129129
("all://", "https://example.com:123", True),
130130
("", "https://example.com:123", True),
131+
('all://192.168.0.0/24', 'http://192.168.0.1', True),
132+
('all://192.168.0.0/24', 'https://192.168.1.1', False),
133+
('all://[2001:db8:abcd:0012::]/64', 'http://[2001:db8:abcd:12::1]', True),
134+
('all://[2001:db8:abcd:0012::]/64', 'http://[2001:db8:abcd:13::1]:8080', False),
131135
],
132136
)
133137
def test_url_matches(pattern, url, expected):
134-
pattern = URLPattern(pattern)
138+
pattern = build_url_pattern(pattern)
135139
assert pattern.matches(httpx.URL(url)) == expected
136140

137141

138142
def test_pattern_priority():
139143
matchers = [
140-
URLPattern("all://"),
141-
URLPattern("http://"),
142-
URLPattern("http://example.com"),
143-
URLPattern("http://example.com:123"),
144+
build_url_pattern("all://"),
145+
build_url_pattern("http://"),
146+
build_url_pattern("http://example.com"),
147+
build_url_pattern("http://example.com:123"),
148+
build_url_pattern("192.168.0.1/16"),
144149
]
145150
random.shuffle(matchers)
146151
assert sorted(matchers) == [
147-
URLPattern("http://example.com:123"),
148-
URLPattern("http://example.com"),
149-
URLPattern("http://"),
150-
URLPattern("all://"),
152+
build_url_pattern("192.168.0.1/16"),
153+
build_url_pattern("http://example.com:123"),
154+
build_url_pattern("http://example.com"),
155+
build_url_pattern("http://"),
156+
build_url_pattern("all://"),
151157
]

0 commit comments

Comments
 (0)