Skip to content

Commit 5324086

Browse files
committed
[python] honor proxy environment settings
urllib3 does not read proxy environment variables, so generated clients require users to copy them into Configuration.proxy. 97e079f added no_proxy handling, but 01ed597 replaced the Python templates without carrying it forward. Resolve scheme-specific proxy and no-proxy defaults through urllib.request while preserving explicit empty values as opt-outs. Match domain, port, IPv4 CIDR, and IPv6 CIDR bypass entries without adding requests to generated clients.
1 parent 8759d96 commit 5324086

12 files changed

Lines changed: 381 additions & 5 deletions

File tree

modules/openapi-generator/src/main/resources/python/configuration.mustache

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import multiprocessing
1616
{{/async}}
1717
import sys
1818
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union
19+
{{^async}}
20+
from urllib.parse import urlparse
21+
from urllib.request import getproxies
22+
{{/async}}
1923
from typing_extensions import NotRequired, Self
2024

2125
{{^async}}
@@ -215,6 +219,9 @@ class Configuration:
215219
:param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server.
216220
:param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync.
217221
:param proxy: Proxy URL.
222+
{{^async}}
223+
:param no_proxy: Comma-separated hosts that bypass the proxy.
224+
{{/async}}
218225
:param proxy_headers: Proxy headers.
219226
:param safe_chars_for_path_param: Safe characters for path parameter encoding.
220227
:param client_side_validation: Enable client-side validation. Default True.
@@ -346,6 +353,9 @@ conf = {{{packageName}}}.Configuration(
346353
tls_server_name: Optional[str]=None,
347354
connection_pool_maxsize: Optional[int]=None,
348355
proxy: Optional[str]=None,
356+
{{^async}}
357+
no_proxy: Optional[str]=None,
358+
{{/async}}
349359
proxy_headers: Optional[Any]=None,
350360
safe_chars_for_path_param: str='',
351361
client_side_validation: bool=True,
@@ -469,9 +479,25 @@ conf = {{{packageName}}}.Configuration(
469479
"""
470480
{{/async}}
471481

482+
{{^async}}
483+
# urllib3 does not read proxy environment variables itself:
484+
# https://github.com/urllib3/urllib3/issues/1785
485+
if proxy is None or no_proxy is None:
486+
proxies = getproxies()
487+
if proxy is None:
488+
scheme = urlparse(self.host).scheme
489+
proxy = proxies.get(scheme) or proxies.get("all")
490+
if no_proxy is None:
491+
no_proxy = proxies.get("no")
492+
{{/async}}
472493
self.proxy = proxy
473494
"""Proxy URL
474495
"""
496+
{{^async}}
497+
self.no_proxy = no_proxy
498+
"""Hosts that bypass the proxy
499+
"""
500+
{{/async}}
475501
self.proxy_headers = proxy_headers
476502
"""Proxy headers
477503
"""

modules/openapi-generator/src/main/resources/python/rest.mustache

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
{{>partial_header}}
44

55

6+
import ipaddress
67
import io
78
import json
89
import re
910
import ssl
11+
from urllib.parse import urlparse
1012

1113
import urllib3
1214

@@ -26,6 +28,44 @@ def is_socks_proxy_url(url):
2628
return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES
2729

2830

31+
def should_bypass_proxies(url: str, no_proxy: str) -> bool:
32+
"""Return whether ``url`` matches the comma-separated ``no_proxy`` rules."""
33+
parsed_url = urlparse(url)
34+
if not parsed_url.hostname:
35+
return True
36+
37+
host = parsed_url.hostname.lower()
38+
host_and_port = parsed_url.netloc.lower()
39+
try:
40+
host_ip = ipaddress.ip_address(host)
41+
except ValueError:
42+
host_ip = None
43+
44+
for entry in (entry.strip().lower() for entry in no_proxy.split(',')):
45+
if not entry:
46+
continue
47+
if entry == '*':
48+
return True
49+
50+
if host_ip is not None:
51+
try:
52+
if host_ip in ipaddress.ip_network(entry, strict=False):
53+
return True
54+
except ValueError:
55+
pass
56+
57+
entry = entry.lstrip('.')
58+
if (
59+
host == entry
60+
or host.endswith('.' + entry)
61+
or host_and_port == entry
62+
or host_and_port.endswith('.' + entry)
63+
):
64+
return True
65+
66+
return False
67+
68+
2969
class RESTResponse(io.IOBase):
3070

3171
def __init__(self, resp) -> None:
@@ -95,7 +135,9 @@ class RESTClientObject:
95135
# https pool manager
96136
self.pool_manager: urllib3.PoolManager
97137

98-
if configuration.proxy:
138+
if configuration.proxy and not should_bypass_proxies(
139+
configuration.host, configuration.no_proxy or ''
140+
):
99141
if is_socks_proxy_url(configuration.proxy):
100142
from urllib3.contrib.socks import SOCKSProxyManager
101143
pool_args["proxy_url"] = configuration.proxy

samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/configuration.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import multiprocessing
1919
import sys
2020
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union
21+
from urllib.parse import urlparse
22+
from urllib.request import getproxies
2123
from typing_extensions import NotRequired, Self
2224

2325
import urllib3
@@ -171,6 +173,7 @@ class Configuration:
171173
:param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server.
172174
:param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync.
173175
:param proxy: Proxy URL.
176+
:param no_proxy: Comma-separated hosts that bypass the proxy.
174177
:param proxy_headers: Proxy headers.
175178
:param safe_chars_for_path_param: Safe characters for path parameter encoding.
176179
:param client_side_validation: Enable client-side validation. Default True.
@@ -222,6 +225,7 @@ def __init__(
222225
tls_server_name: Optional[str]=None,
223226
connection_pool_maxsize: Optional[int]=None,
224227
proxy: Optional[str]=None,
228+
no_proxy: Optional[str]=None,
225229
proxy_headers: Optional[Any]=None,
226230
safe_chars_for_path_param: str='',
227231
client_side_validation: bool=True,
@@ -328,9 +332,21 @@ def __init__(
328332
per pool. None in the constructor is coerced to cpu_count * 5.
329333
"""
330334

335+
# urllib3 does not read proxy environment variables itself:
336+
# https://github.com/urllib3/urllib3/issues/1785
337+
if proxy is None or no_proxy is None:
338+
proxies = getproxies()
339+
if proxy is None:
340+
scheme = urlparse(self.host).scheme
341+
proxy = proxies.get(scheme) or proxies.get("all")
342+
if no_proxy is None:
343+
no_proxy = proxies.get("no")
331344
self.proxy = proxy
332345
"""Proxy URL
333346
"""
347+
self.no_proxy = no_proxy
348+
"""Hosts that bypass the proxy
349+
"""
334350
self.proxy_headers = proxy_headers
335351
"""Proxy headers
336352
"""

samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
""" # noqa: E501
1414

1515

16+
import ipaddress
1617
import io
1718
import json
1819
import re
1920
import ssl
21+
from urllib.parse import urlparse
2022

2123
import urllib3
2224

@@ -36,6 +38,44 @@ def is_socks_proxy_url(url):
3638
return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES
3739

3840

41+
def should_bypass_proxies(url: str, no_proxy: str) -> bool:
42+
"""Return whether ``url`` matches the comma-separated ``no_proxy`` rules."""
43+
parsed_url = urlparse(url)
44+
if not parsed_url.hostname:
45+
return True
46+
47+
host = parsed_url.hostname.lower()
48+
host_and_port = parsed_url.netloc.lower()
49+
try:
50+
host_ip = ipaddress.ip_address(host)
51+
except ValueError:
52+
host_ip = None
53+
54+
for entry in (entry.strip().lower() for entry in no_proxy.split(',')):
55+
if not entry:
56+
continue
57+
if entry == '*':
58+
return True
59+
60+
if host_ip is not None:
61+
try:
62+
if host_ip in ipaddress.ip_network(entry, strict=False):
63+
return True
64+
except ValueError:
65+
pass
66+
67+
entry = entry.lstrip('.')
68+
if (
69+
host == entry
70+
or host.endswith('.' + entry)
71+
or host_and_port == entry
72+
or host_and_port.endswith('.' + entry)
73+
):
74+
return True
75+
76+
return False
77+
78+
3979
class RESTResponse(io.IOBase):
4080

4181
def __init__(self, resp) -> None:
@@ -105,7 +145,9 @@ def __init__(self, configuration) -> None:
105145
# https pool manager
106146
self.pool_manager: urllib3.PoolManager
107147

108-
if configuration.proxy:
148+
if configuration.proxy and not should_bypass_proxies(
149+
configuration.host, configuration.no_proxy or ''
150+
):
109151
if is_socks_proxy_url(configuration.proxy):
110152
from urllib3.contrib.socks import SOCKSProxyManager
111153
pool_args["proxy_url"] = configuration.proxy

samples/client/echo_api/python/openapi_client/configuration.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import multiprocessing
1919
import sys
2020
from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union
21+
from urllib.parse import urlparse
22+
from urllib.request import getproxies
2123
from typing_extensions import NotRequired, Self
2224

2325
import urllib3
@@ -171,6 +173,7 @@ class Configuration:
171173
:param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server.
172174
:param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync.
173175
:param proxy: Proxy URL.
176+
:param no_proxy: Comma-separated hosts that bypass the proxy.
174177
:param proxy_headers: Proxy headers.
175178
:param safe_chars_for_path_param: Safe characters for path parameter encoding.
176179
:param client_side_validation: Enable client-side validation. Default True.
@@ -222,6 +225,7 @@ def __init__(
222225
tls_server_name: Optional[str]=None,
223226
connection_pool_maxsize: Optional[int]=None,
224227
proxy: Optional[str]=None,
228+
no_proxy: Optional[str]=None,
225229
proxy_headers: Optional[Any]=None,
226230
safe_chars_for_path_param: str='',
227231
client_side_validation: bool=True,
@@ -328,9 +332,21 @@ def __init__(
328332
per pool. None in the constructor is coerced to cpu_count * 5.
329333
"""
330334

335+
# urllib3 does not read proxy environment variables itself:
336+
# https://github.com/urllib3/urllib3/issues/1785
337+
if proxy is None or no_proxy is None:
338+
proxies = getproxies()
339+
if proxy is None:
340+
scheme = urlparse(self.host).scheme
341+
proxy = proxies.get(scheme) or proxies.get("all")
342+
if no_proxy is None:
343+
no_proxy = proxies.get("no")
331344
self.proxy = proxy
332345
"""Proxy URL
333346
"""
347+
self.no_proxy = no_proxy
348+
"""Hosts that bypass the proxy
349+
"""
334350
self.proxy_headers = proxy_headers
335351
"""Proxy headers
336352
"""

samples/client/echo_api/python/openapi_client/rest.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
""" # noqa: E501
1414

1515

16+
import ipaddress
1617
import io
1718
import json
1819
import re
1920
import ssl
21+
from urllib.parse import urlparse
2022

2123
import urllib3
2224

@@ -36,6 +38,44 @@ def is_socks_proxy_url(url):
3638
return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES
3739

3840

41+
def should_bypass_proxies(url: str, no_proxy: str) -> bool:
42+
"""Return whether ``url`` matches the comma-separated ``no_proxy`` rules."""
43+
parsed_url = urlparse(url)
44+
if not parsed_url.hostname:
45+
return True
46+
47+
host = parsed_url.hostname.lower()
48+
host_and_port = parsed_url.netloc.lower()
49+
try:
50+
host_ip = ipaddress.ip_address(host)
51+
except ValueError:
52+
host_ip = None
53+
54+
for entry in (entry.strip().lower() for entry in no_proxy.split(',')):
55+
if not entry:
56+
continue
57+
if entry == '*':
58+
return True
59+
60+
if host_ip is not None:
61+
try:
62+
if host_ip in ipaddress.ip_network(entry, strict=False):
63+
return True
64+
except ValueError:
65+
pass
66+
67+
entry = entry.lstrip('.')
68+
if (
69+
host == entry
70+
or host.endswith('.' + entry)
71+
or host_and_port == entry
72+
or host_and_port.endswith('.' + entry)
73+
):
74+
return True
75+
76+
return False
77+
78+
3979
class RESTResponse(io.IOBase):
4080

4181
def __init__(self, resp) -> None:
@@ -105,7 +145,9 @@ def __init__(self, configuration) -> None:
105145
# https pool manager
106146
self.pool_manager: urllib3.PoolManager
107147

108-
if configuration.proxy:
148+
if configuration.proxy and not should_bypass_proxies(
149+
configuration.host, configuration.no_proxy or ''
150+
):
109151
if is_socks_proxy_url(configuration.proxy):
110152
from urllib3.contrib.socks import SOCKSProxyManager
111153
pool_args["proxy_url"] = configuration.proxy

0 commit comments

Comments
 (0)