diff --git a/ldclient/impl/http.py b/ldclient/impl/http.py index 5fea123..91efdc1 100644 --- a/ldclient/impl/http.py +++ b/ldclient/impl/http.py @@ -2,6 +2,8 @@ import certifi from os import environ import urllib3 +from urllib.parse import urlparse + def _application_header_value(application: dict) -> str: parts = [] @@ -34,9 +36,11 @@ def _base_headers(config): return headers + def _http_factory(config): return HTTPFactory(_base_headers(config), config.http) + class HTTPFactory: def __init__(self, base_headers, http_config, override_read_timeout=None): self.__base_headers = base_headers @@ -73,26 +77,50 @@ def create_pool_manager(self, num_pools, target_base_uri): num_pools=num_pools, cert_reqs=cert_reqs, ca_certs=ca_certs - ) + ) else: # Get proxy authentication, if provided url = urllib3.util.parse_url(proxy_url) proxy_headers = None - if url.auth != None: + if url.auth is not None: proxy_headers = urllib3.util.make_headers(proxy_basic_auth=url.auth) # Create a proxied connection return urllib3.ProxyManager( proxy_url, num_pools=num_pools, cert_reqs=cert_reqs, - ca_certs = ca_certs, + ca_certs=ca_certs, proxy_headers=proxy_headers ) + def _get_proxy_url(target_base_uri): if target_base_uri is None: return None - is_https = target_base_uri.startswith('https:') - if is_https: - return environ.get('https_proxy') - return environ.get('http_proxy') + + parse = urlparse(target_base_uri) + is_https = parse.scheme == 'https' + + target_port = parse.port + if target_port is None: + target_port = 443 if is_https else 80 + + proxy_url = environ.get('https_proxy') if is_https else environ.get('http_proxy') + no_proxy = environ.get('no_proxy') + + if proxy_url is None or no_proxy == '*': + return None + elif no_proxy is None: + return proxy_url + + for no_proxy_entry in no_proxy.split(','): + parts = no_proxy_entry.strip().split(':') + if len(parts) == 1: + if parse.hostname.endswith(no_proxy_entry): + return None + continue + + if parse.hostname.endswith(parts[0]) and target_port == int(parts[1]): + return None + + return proxy_url diff --git a/ldclient/testing/impl/test_http.py b/ldclient/testing/impl/test_http.py new file mode 100644 index 0000000..d2ebca8 --- /dev/null +++ b/ldclient/testing/impl/test_http.py @@ -0,0 +1,36 @@ +import pytest + +from typing import Optional +from ldclient.impl.http import _get_proxy_url + + +@pytest.mark.parametrize( + 'target_uri, no_proxy, expected', + [ + ('https://secure.example.com', 'secure.example.com', None), + ('https://secure.example.com', 'secure.example.com:443', None), + ('https://secure.example.com', 'secure.example.com:80', 'https://secure.proxy:1234'), + ('https://secure.example.com', 'wrong.example.com', 'https://secure.proxy:1234'), + + ('https://secure.example.com', 'example.com', None), + ('https://secure.example.com', 'example.com:443', None), + ('https://secure.example.com', 'example.com:80', 'https://secure.proxy:1234'), + + ('http://insecure.example.com', 'insecure.example.com', None), + ('http://insecure.example.com', 'insecure.example.com:443', 'http://insecure.proxy:6789'), + ('http://insecure.example.com', 'insecure.example.com:80', None), + ('http://insecure.example.com', 'wrong.example.com', 'http://insecure.proxy:6789'), + + ('http://insecure.example.com', 'example.com', None), + ('http://insecure.example.com', 'example.com:443', 'http://insecure.proxy:6789'), + ('http://insecure.example.com', 'example.com:80', None), + ] +) +def test_honors_no_proxy(target_uri: str, no_proxy: str, expected: Optional[str], monkeypatch): + monkeypatch.setenv('https_proxy', 'https://secure.proxy:1234') + monkeypatch.setenv('http_proxy', 'http://insecure.proxy:6789') + monkeypatch.setenv('no_proxy', no_proxy) + + proxy_url = _get_proxy_url(target_uri) + + assert proxy_url == expected diff --git a/ldclient/testing/proxy_test_util.py b/ldclient/testing/proxy_test_util.py index dab075d..b9d1e85 100644 --- a/ldclient/testing/proxy_test_util.py +++ b/ldclient/testing/proxy_test_util.py @@ -1,5 +1,6 @@ from ldclient.config import Config, HTTPConfig -from ldclient.testing.http_util import start_server, BasicResponse, JsonResponse +from ldclient.testing.http_util import start_server + # Runs tests of all of our supported proxy server configurations: secure or insecure, configured # by Config.http_proxy or by an environment variable, with or without authentication. The action @@ -16,7 +17,8 @@ def do_proxy_tests(action, action_method, monkeypatch): (False, True, False), (True, False, False), (True, False, True), - (True, True, False)]: + (True, True, False) + ]: test_desc = "%s, %s, %s" % ( "using env vars" if use_env_vars else "using Config", "secure" if secure else "insecure", @@ -27,15 +29,15 @@ def do_proxy_tests(action, action_method, monkeypatch): if use_env_vars: monkeypatch.setenv('https_proxy' if secure else 'http_proxy', proxy_uri) config = Config( - sdk_key = 'sdk_key', - base_uri = target_uri, - events_uri = target_uri, - stream_uri = target_uri, - http = HTTPConfig(http_proxy=proxy_uri), - diagnostic_opt_out = True) + sdk_key='sdk_key', + base_uri=target_uri, + events_uri=target_uri, + stream_uri=target_uri, + http=HTTPConfig(http_proxy=proxy_uri), + diagnostic_opt_out=True) try: action(server, config, secure) - except: + except Exception: print("test action failed (%s)" % test_desc) raise # For an insecure proxy request, our stub server behaves enough like the real thing to satisfy the @@ -43,7 +45,7 @@ def do_proxy_tests(action, action_method, monkeypatch): # actually be an absolute URI for a proxy request. try: req = server.require_request() - except: + except Exception: print("server did not receive a request (%s)" % test_desc) raise expected_method = 'CONNECT' if secure else action_method