diff --git a/modules/openapi-generator/src/main/resources/python/configuration.mustache b/modules/openapi-generator/src/main/resources/python/configuration.mustache index 11b924f173dc..6ee3283dd72a 100644 --- a/modules/openapi-generator/src/main/resources/python/configuration.mustache +++ b/modules/openapi-generator/src/main/resources/python/configuration.mustache @@ -16,6 +16,10 @@ import multiprocessing {{/async}} import sys from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +{{^async}} +from urllib.parse import urlparse +from urllib.request import getproxies +{{/async}} from typing_extensions import NotRequired, Self {{^async}} @@ -215,6 +219,9 @@ class Configuration: :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. :param proxy: Proxy URL. +{{^async}} + :param no_proxy: Comma-separated hosts that bypass the proxy. +{{/async}} :param proxy_headers: Proxy headers. :param safe_chars_for_path_param: Safe characters for path parameter encoding. :param client_side_validation: Enable client-side validation. Default True. @@ -346,6 +353,9 @@ conf = {{{packageName}}}.Configuration( tls_server_name: Optional[str]=None, connection_pool_maxsize: Optional[int]=None, proxy: Optional[str]=None, +{{^async}} + no_proxy: Optional[str]=None, +{{/async}} proxy_headers: Optional[Any]=None, safe_chars_for_path_param: str='', client_side_validation: bool=True, @@ -469,9 +479,25 @@ conf = {{{packageName}}}.Configuration( """ {{/async}} +{{^async}} + # urllib3 does not read proxy environment variables itself: + # https://github.com/urllib3/urllib3/issues/1785 + if proxy is None or no_proxy is None: + proxies = getproxies() + if proxy is None: + scheme = urlparse(self.host).scheme + proxy = proxies.get(scheme) or proxies.get("all") + if no_proxy is None: + no_proxy = proxies.get("no") +{{/async}} self.proxy = proxy """Proxy URL """ +{{^async}} + self.no_proxy = no_proxy + """Hosts that bypass the proxy + """ +{{/async}} self.proxy_headers = proxy_headers """Proxy headers """ diff --git a/modules/openapi-generator/src/main/resources/python/rest.mustache b/modules/openapi-generator/src/main/resources/python/rest.mustache index 5a22da9b3df8..5d852b28d92b 100644 --- a/modules/openapi-generator/src/main/resources/python/rest.mustache +++ b/modules/openapi-generator/src/main/resources/python/rest.mustache @@ -3,10 +3,12 @@ {{>partial_header}} +import ipaddress import io import json import re import ssl +from urllib.parse import urlparse import urllib3 @@ -26,6 +28,44 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def should_bypass_proxies(url: str, no_proxy: str) -> bool: + """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" + parsed_url = urlparse(url) + if not parsed_url.hostname: + return True + + host = parsed_url.hostname.lower() + host_and_port = parsed_url.netloc.lower() + try: + host_ip = ipaddress.ip_address(host) + except ValueError: + host_ip = None + + for entry in (entry.strip().lower() for entry in no_proxy.split(',')): + if not entry: + continue + if entry == '*': + return True + + if host_ip is not None: + try: + if host_ip in ipaddress.ip_network(entry, strict=False): + return True + except ValueError: + pass + + entry = entry.lstrip('.') + if ( + host == entry + or host.endswith('.' + entry) + or host_and_port == entry + or host_and_port.endswith('.' + entry) + ): + return True + + return False + + class RESTResponse(io.IOBase): def __init__(self, resp) -> None: @@ -95,7 +135,9 @@ class RESTClientObject: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: + if configuration.proxy and not should_bypass_proxies( + configuration.host, configuration.no_proxy or '' + ): if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager pool_args["proxy_url"] = configuration.proxy diff --git a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/configuration.py b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/configuration.py index ec8b847d3202..9aad2e28787b 100644 --- a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/configuration.py +++ b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/configuration.py @@ -18,6 +18,8 @@ import multiprocessing import sys from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from urllib.parse import urlparse +from urllib.request import getproxies from typing_extensions import NotRequired, Self import urllib3 @@ -171,6 +173,7 @@ class Configuration: :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. :param proxy: Proxy URL. + :param no_proxy: Comma-separated hosts that bypass the proxy. :param proxy_headers: Proxy headers. :param safe_chars_for_path_param: Safe characters for path parameter encoding. :param client_side_validation: Enable client-side validation. Default True. @@ -222,6 +225,7 @@ def __init__( tls_server_name: Optional[str]=None, connection_pool_maxsize: Optional[int]=None, proxy: Optional[str]=None, + no_proxy: Optional[str]=None, proxy_headers: Optional[Any]=None, safe_chars_for_path_param: str='', client_side_validation: bool=True, @@ -328,9 +332,21 @@ def __init__( per pool. None in the constructor is coerced to cpu_count * 5. """ + # urllib3 does not read proxy environment variables itself: + # https://github.com/urllib3/urllib3/issues/1785 + if proxy is None or no_proxy is None: + proxies = getproxies() + if proxy is None: + scheme = urlparse(self.host).scheme + proxy = proxies.get(scheme) or proxies.get("all") + if no_proxy is None: + no_proxy = proxies.get("no") self.proxy = proxy """Proxy URL """ + self.no_proxy = no_proxy + """Hosts that bypass the proxy + """ self.proxy_headers = proxy_headers """Proxy headers """ diff --git a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py index 70a82f5a5bac..934eb3f8b794 100644 --- a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py +++ b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/rest.py @@ -13,10 +13,12 @@ """ # noqa: E501 +import ipaddress import io import json import re import ssl +from urllib.parse import urlparse import urllib3 @@ -36,6 +38,44 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def should_bypass_proxies(url: str, no_proxy: str) -> bool: + """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" + parsed_url = urlparse(url) + if not parsed_url.hostname: + return True + + host = parsed_url.hostname.lower() + host_and_port = parsed_url.netloc.lower() + try: + host_ip = ipaddress.ip_address(host) + except ValueError: + host_ip = None + + for entry in (entry.strip().lower() for entry in no_proxy.split(',')): + if not entry: + continue + if entry == '*': + return True + + if host_ip is not None: + try: + if host_ip in ipaddress.ip_network(entry, strict=False): + return True + except ValueError: + pass + + entry = entry.lstrip('.') + if ( + host == entry + or host.endswith('.' + entry) + or host_and_port == entry + or host_and_port.endswith('.' + entry) + ): + return True + + return False + + class RESTResponse(io.IOBase): def __init__(self, resp) -> None: @@ -105,7 +145,9 @@ def __init__(self, configuration) -> None: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: + if configuration.proxy and not should_bypass_proxies( + configuration.host, configuration.no_proxy or '' + ): if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager pool_args["proxy_url"] = configuration.proxy diff --git a/samples/client/echo_api/python/openapi_client/configuration.py b/samples/client/echo_api/python/openapi_client/configuration.py index ec8b847d3202..9aad2e28787b 100644 --- a/samples/client/echo_api/python/openapi_client/configuration.py +++ b/samples/client/echo_api/python/openapi_client/configuration.py @@ -18,6 +18,8 @@ import multiprocessing import sys from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from urllib.parse import urlparse +from urllib.request import getproxies from typing_extensions import NotRequired, Self import urllib3 @@ -171,6 +173,7 @@ class Configuration: :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. :param proxy: Proxy URL. + :param no_proxy: Comma-separated hosts that bypass the proxy. :param proxy_headers: Proxy headers. :param safe_chars_for_path_param: Safe characters for path parameter encoding. :param client_side_validation: Enable client-side validation. Default True. @@ -222,6 +225,7 @@ def __init__( tls_server_name: Optional[str]=None, connection_pool_maxsize: Optional[int]=None, proxy: Optional[str]=None, + no_proxy: Optional[str]=None, proxy_headers: Optional[Any]=None, safe_chars_for_path_param: str='', client_side_validation: bool=True, @@ -328,9 +332,21 @@ def __init__( per pool. None in the constructor is coerced to cpu_count * 5. """ + # urllib3 does not read proxy environment variables itself: + # https://github.com/urllib3/urllib3/issues/1785 + if proxy is None or no_proxy is None: + proxies = getproxies() + if proxy is None: + scheme = urlparse(self.host).scheme + proxy = proxies.get(scheme) or proxies.get("all") + if no_proxy is None: + no_proxy = proxies.get("no") self.proxy = proxy """Proxy URL """ + self.no_proxy = no_proxy + """Hosts that bypass the proxy + """ self.proxy_headers = proxy_headers """Proxy headers """ diff --git a/samples/client/echo_api/python/openapi_client/rest.py b/samples/client/echo_api/python/openapi_client/rest.py index 87c20b4c419c..b5a114226b79 100644 --- a/samples/client/echo_api/python/openapi_client/rest.py +++ b/samples/client/echo_api/python/openapi_client/rest.py @@ -13,10 +13,12 @@ """ # noqa: E501 +import ipaddress import io import json import re import ssl +from urllib.parse import urlparse import urllib3 @@ -36,6 +38,44 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def should_bypass_proxies(url: str, no_proxy: str) -> bool: + """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" + parsed_url = urlparse(url) + if not parsed_url.hostname: + return True + + host = parsed_url.hostname.lower() + host_and_port = parsed_url.netloc.lower() + try: + host_ip = ipaddress.ip_address(host) + except ValueError: + host_ip = None + + for entry in (entry.strip().lower() for entry in no_proxy.split(',')): + if not entry: + continue + if entry == '*': + return True + + if host_ip is not None: + try: + if host_ip in ipaddress.ip_network(entry, strict=False): + return True + except ValueError: + pass + + entry = entry.lstrip('.') + if ( + host == entry + or host.endswith('.' + entry) + or host_and_port == entry + or host_and_port.endswith('.' + entry) + ): + return True + + return False + + class RESTResponse(io.IOBase): def __init__(self, resp) -> None: @@ -105,7 +145,9 @@ def __init__(self, configuration) -> None: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: + if configuration.proxy and not should_bypass_proxies( + configuration.host, configuration.no_proxy or '' + ): if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager pool_args["proxy_url"] = configuration.proxy diff --git a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/configuration.py b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/configuration.py index ede113d38519..74e89e0946f4 100644 --- a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/configuration.py +++ b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/configuration.py @@ -17,6 +17,8 @@ import multiprocessing import sys from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from urllib.parse import urlparse +from urllib.request import getproxies from typing_extensions import NotRequired, Self import urllib3 @@ -177,6 +179,7 @@ class Configuration: :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. :param proxy: Proxy URL. + :param no_proxy: Comma-separated hosts that bypass the proxy. :param proxy_headers: Proxy headers. :param safe_chars_for_path_param: Safe characters for path parameter encoding. :param client_side_validation: Enable client-side validation. Default True. @@ -287,6 +290,7 @@ def __init__( tls_server_name: Optional[str]=None, connection_pool_maxsize: Optional[int]=None, proxy: Optional[str]=None, + no_proxy: Optional[str]=None, proxy_headers: Optional[Any]=None, safe_chars_for_path_param: str='', client_side_validation: bool=True, @@ -398,9 +402,21 @@ def __init__( per pool. None in the constructor is coerced to cpu_count * 5. """ + # urllib3 does not read proxy environment variables itself: + # https://github.com/urllib3/urllib3/issues/1785 + if proxy is None or no_proxy is None: + proxies = getproxies() + if proxy is None: + scheme = urlparse(self.host).scheme + proxy = proxies.get(scheme) or proxies.get("all") + if no_proxy is None: + no_proxy = proxies.get("no") self.proxy = proxy """Proxy URL """ + self.no_proxy = no_proxy + """Hosts that bypass the proxy + """ self.proxy_headers = proxy_headers """Proxy headers """ diff --git a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py index 5d95d94f8191..5df0581723cc 100644 --- a/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py +++ b/samples/openapi3/client/petstore/python-lazyImports/petstore_api/rest.py @@ -12,10 +12,12 @@ """ # noqa: E501 +import ipaddress import io import json import re import ssl +from urllib.parse import urlparse import urllib3 @@ -35,6 +37,44 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def should_bypass_proxies(url: str, no_proxy: str) -> bool: + """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" + parsed_url = urlparse(url) + if not parsed_url.hostname: + return True + + host = parsed_url.hostname.lower() + host_and_port = parsed_url.netloc.lower() + try: + host_ip = ipaddress.ip_address(host) + except ValueError: + host_ip = None + + for entry in (entry.strip().lower() for entry in no_proxy.split(',')): + if not entry: + continue + if entry == '*': + return True + + if host_ip is not None: + try: + if host_ip in ipaddress.ip_network(entry, strict=False): + return True + except ValueError: + pass + + entry = entry.lstrip('.') + if ( + host == entry + or host.endswith('.' + entry) + or host_and_port == entry + or host_and_port.endswith('.' + entry) + ): + return True + + return False + + class RESTResponse(io.IOBase): def __init__(self, resp) -> None: @@ -104,7 +144,9 @@ def __init__(self, configuration) -> None: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: + if configuration.proxy and not should_bypass_proxies( + configuration.host, configuration.no_proxy or '' + ): if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager pool_args["proxy_url"] = configuration.proxy diff --git a/samples/openapi3/client/petstore/python/petstore_api/configuration.py b/samples/openapi3/client/petstore/python/petstore_api/configuration.py index ede113d38519..74e89e0946f4 100755 --- a/samples/openapi3/client/petstore/python/petstore_api/configuration.py +++ b/samples/openapi3/client/petstore/python/petstore_api/configuration.py @@ -17,6 +17,8 @@ import multiprocessing import sys from typing import Any, ClassVar, Dict, List, Literal, Optional, TypedDict, Union +from urllib.parse import urlparse +from urllib.request import getproxies from typing_extensions import NotRequired, Self import urllib3 @@ -177,6 +179,7 @@ class Configuration: :param tls_server_name: SSL/TLS Server Name Indication (SNI). Set this to the SNI value expected by the server. :param connection_pool_maxsize: Connection pool max size. None in the constructor is coerced to 100 for async and cpu_count * 5 for sync. :param proxy: Proxy URL. + :param no_proxy: Comma-separated hosts that bypass the proxy. :param proxy_headers: Proxy headers. :param safe_chars_for_path_param: Safe characters for path parameter encoding. :param client_side_validation: Enable client-side validation. Default True. @@ -287,6 +290,7 @@ def __init__( tls_server_name: Optional[str]=None, connection_pool_maxsize: Optional[int]=None, proxy: Optional[str]=None, + no_proxy: Optional[str]=None, proxy_headers: Optional[Any]=None, safe_chars_for_path_param: str='', client_side_validation: bool=True, @@ -398,9 +402,21 @@ def __init__( per pool. None in the constructor is coerced to cpu_count * 5. """ + # urllib3 does not read proxy environment variables itself: + # https://github.com/urllib3/urllib3/issues/1785 + if proxy is None or no_proxy is None: + proxies = getproxies() + if proxy is None: + scheme = urlparse(self.host).scheme + proxy = proxies.get(scheme) or proxies.get("all") + if no_proxy is None: + no_proxy = proxies.get("no") self.proxy = proxy """Proxy URL """ + self.no_proxy = no_proxy + """Hosts that bypass the proxy + """ self.proxy_headers = proxy_headers """Proxy headers """ diff --git a/samples/openapi3/client/petstore/python/petstore_api/rest.py b/samples/openapi3/client/petstore/python/petstore_api/rest.py index 5d95d94f8191..5df0581723cc 100755 --- a/samples/openapi3/client/petstore/python/petstore_api/rest.py +++ b/samples/openapi3/client/petstore/python/petstore_api/rest.py @@ -12,10 +12,12 @@ """ # noqa: E501 +import ipaddress import io import json import re import ssl +from urllib.parse import urlparse import urllib3 @@ -35,6 +37,44 @@ def is_socks_proxy_url(url): return split_section[0].lower() in SUPPORTED_SOCKS_PROXIES +def should_bypass_proxies(url: str, no_proxy: str) -> bool: + """Return whether ``url`` matches the comma-separated ``no_proxy`` rules.""" + parsed_url = urlparse(url) + if not parsed_url.hostname: + return True + + host = parsed_url.hostname.lower() + host_and_port = parsed_url.netloc.lower() + try: + host_ip = ipaddress.ip_address(host) + except ValueError: + host_ip = None + + for entry in (entry.strip().lower() for entry in no_proxy.split(',')): + if not entry: + continue + if entry == '*': + return True + + if host_ip is not None: + try: + if host_ip in ipaddress.ip_network(entry, strict=False): + return True + except ValueError: + pass + + entry = entry.lstrip('.') + if ( + host == entry + or host.endswith('.' + entry) + or host_and_port == entry + or host_and_port.endswith('.' + entry) + ): + return True + + return False + + class RESTResponse(io.IOBase): def __init__(self, resp) -> None: @@ -104,7 +144,9 @@ def __init__(self, configuration) -> None: # https pool manager self.pool_manager: urllib3.PoolManager - if configuration.proxy: + if configuration.proxy and not should_bypass_proxies( + configuration.host, configuration.no_proxy or '' + ): if is_socks_proxy_url(configuration.proxy): from urllib3.contrib.socks import SOCKSProxyManager pool_args["proxy_url"] = configuration.proxy diff --git a/samples/openapi3/client/petstore/python/tests/test_configuration.py b/samples/openapi3/client/petstore/python/tests/test_configuration.py index 4e14b14576dc..3dfa32ae4f99 100644 --- a/samples/openapi3/client/petstore/python/tests/test_configuration.py +++ b/samples/openapi3/client/petstore/python/tests/test_configuration.py @@ -11,6 +11,7 @@ from __future__ import absolute_import import unittest +from unittest.mock import patch import petstore_api @@ -58,6 +59,48 @@ def testAccessTokenWhenConstructingConfiguration(self): c1 = petstore_api.Configuration(access_token="12345") self.assertEqual(c1.access_token, "12345") + def test_proxy_settings_default_from_environment(self): + environment_proxies = { + "http": "http://plain-proxy.example", + "https": "http://secure-proxy.example", + "no": "internal.example", + } + with patch( + "petstore_api.configuration.getproxies", + return_value=environment_proxies, + ): + config = petstore_api.Configuration(host="https://api.example") + + self.assertEqual(config.proxy, "http://secure-proxy.example") + self.assertEqual(config.no_proxy, "internal.example") + + def test_explicit_proxy_settings_override_environment(self): + with patch( + "petstore_api.configuration.getproxies", + return_value={"https": "http://proxy.example", "no": "example"}, + ) as getproxies: + config = petstore_api.Configuration( + host="https://api.example", + proxy="", + no_proxy="", + ) + + getproxies.assert_not_called() + self.assertEqual(config.proxy, "") + self.assertEqual(config.no_proxy, "") + + def test_explicit_proxy_does_not_resolve_generated_host(self): + with patch( + "petstore_api.configuration.getproxies", + return_value={}, + ): + config = petstore_api.Configuration( + server_index=999, + proxy="http://proxy.example", + ) + + self.assertEqual(config.proxy, "http://proxy.example") + def test_ignore_operation_servers(self): self.config.ignore_operation_servers = True self.assertTrue(self.config.ignore_operation_servers) diff --git a/samples/openapi3/client/petstore/python/tests/test_rest.py b/samples/openapi3/client/petstore/python/tests/test_rest.py index bf8f7fde3c7c..faa292c98613 100644 --- a/samples/openapi3/client/petstore/python/tests/test_rest.py +++ b/samples/openapi3/client/petstore/python/tests/test_rest.py @@ -1,4 +1,5 @@ import json +import json import os import unittest from unittest.mock import Mock, patch @@ -7,6 +8,38 @@ from petstore_api.rest import RESTClientObject +class TestProxyConnection(unittest.TestCase): + def test_no_proxy_selects_direct_connection(self): + cases = [ + ("https://api.example.com", "example.com", True), + ("https://api.example.com:8443", "example.com:8443", True), + ("https://example.com:443", "example.com:8443", False), + ("https://10.2.3.4", "10.0.0.0/8", True), + ("https://[2001:db8::1]", "2001:db8::/32", True), + ("https://api.example.net", "*", True), + ("https://api.example.net", "example.com", False), + ] + for host, no_proxy, bypasses_proxy in cases: + with self.subTest(host=host, no_proxy=no_proxy): + config = petstore_api.Configuration( + host=host, + proxy="http://proxy.example", + no_proxy=no_proxy, + ) + with ( + patch("petstore_api.rest.urllib3.PoolManager") as direct, + patch("petstore_api.rest.urllib3.ProxyManager") as proxied, + ): + RESTClientObject(config) + + if bypasses_proxy: + direct.assert_called_once() + proxied.assert_not_called() + else: + direct.assert_not_called() + proxied.assert_called_once() + + class TestYamlRequestBodies(unittest.TestCase): def setUp(self): self.rest_client = RESTClientObject(