Skip to content

Commit 053eef4

Browse files
committed
feat: NO_PROXY environment variable can be used to override HTTP(S)_PROXY values
When determining if a proxy should be used, the SDK would: 1. Check the `config.http_config.http_proxy` value. If that is set, use that value without further consideration. 2. If the target URI is `https`, use the value from the `HTTPS_PROXY` environment variable. 3. If the target is `http`, use `HTTP_PROXY` instead. The SDK will now support another environment variable -- `NO_PROXY`. This variable can be set to a comma-separated list of hosts to exclude from proxy support, or the special case '*' meaning to ignore all hosts. The `NO_PROXY` variable will only take affect if the SDK isn't explicitly configured to use a proxy as specified in #1 above.
1 parent bd3b2f8 commit 053eef4

File tree

3 files changed

+83
-17
lines changed

3 files changed

+83
-17
lines changed

ldclient/impl/http.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import certifi
33
from os import environ
44
import urllib3
5+
from urllib.parse import urlparse
6+
57

68
def _application_header_value(application: dict) -> str:
79
parts = []
@@ -34,9 +36,11 @@ def _base_headers(config):
3436

3537
return headers
3638

39+
3740
def _http_factory(config):
3841
return HTTPFactory(_base_headers(config), config.http)
3942

43+
4044
class HTTPFactory:
4145
def __init__(self, base_headers, http_config, override_read_timeout=None):
4246
self.__base_headers = base_headers
@@ -73,26 +77,50 @@ def create_pool_manager(self, num_pools, target_base_uri):
7377
num_pools=num_pools,
7478
cert_reqs=cert_reqs,
7579
ca_certs=ca_certs
76-
)
80+
)
7781
else:
7882
# Get proxy authentication, if provided
7983
url = urllib3.util.parse_url(proxy_url)
8084
proxy_headers = None
81-
if url.auth != None:
85+
if url.auth is not None:
8286
proxy_headers = urllib3.util.make_headers(proxy_basic_auth=url.auth)
8387
# Create a proxied connection
8488
return urllib3.ProxyManager(
8589
proxy_url,
8690
num_pools=num_pools,
8791
cert_reqs=cert_reqs,
88-
ca_certs = ca_certs,
92+
ca_certs=ca_certs,
8993
proxy_headers=proxy_headers
9094
)
9195

96+
9297
def _get_proxy_url(target_base_uri):
9398
if target_base_uri is None:
9499
return None
95-
is_https = target_base_uri.startswith('https:')
96-
if is_https:
97-
return environ.get('https_proxy')
98-
return environ.get('http_proxy')
100+
101+
parse = urlparse(target_base_uri)
102+
is_https = parse.scheme == 'https'
103+
104+
target_port = parse.port
105+
if target_port is None:
106+
target_port = 443 if is_https else 80
107+
108+
proxy_url = environ.get('https_proxy') if is_https else environ.get('http_proxy')
109+
no_proxy = environ.get('no_proxy')
110+
111+
if proxy_url is None or no_proxy == '*':
112+
return None
113+
elif no_proxy is None:
114+
return proxy_url
115+
116+
for no_proxy_entry in no_proxy.split(','):
117+
parts = no_proxy_entry.strip().split(':')
118+
if len(parts) == 1:
119+
if parse.hostname.endswith(no_proxy_entry):
120+
return None
121+
continue
122+
123+
if parse.hostname.endswith(parts[0]) and target_port == int(parts[1]):
124+
return None
125+
126+
return proxy_url

ldclient/testing/impl/test_http.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
from typing import Optional
4+
from ldclient.impl.http import _get_proxy_url
5+
6+
7+
@pytest.mark.parametrize(
8+
'target_uri, no_proxy, expected',
9+
[
10+
('https://secure.example.com', 'secure.example.com', None),
11+
('https://secure.example.com', 'secure.example.com:443', None),
12+
('https://secure.example.com', 'secure.example.com:80', 'https://secure.proxy:1234'),
13+
('https://secure.example.com', 'wrong.example.com', 'https://secure.proxy:1234'),
14+
15+
('https://secure.example.com', 'example.com', None),
16+
('https://secure.example.com', 'example.com:443', None),
17+
('https://secure.example.com', 'example.com:80', 'https://secure.proxy:1234'),
18+
19+
('http://insecure.example.com', 'insecure.example.com', None),
20+
('http://insecure.example.com', 'insecure.example.com:443', 'http://insecure.proxy:6789'),
21+
('http://insecure.example.com', 'insecure.example.com:80', None),
22+
('http://insecure.example.com', 'wrong.example.com', 'http://insecure.proxy:6789'),
23+
24+
('http://insecure.example.com', 'example.com', None),
25+
('http://insecure.example.com', 'example.com:443', 'http://insecure.proxy:6789'),
26+
('http://insecure.example.com', 'example.com:80', None),
27+
]
28+
)
29+
def test_honors_no_proxy(target_uri: str, no_proxy: str, expected: Optional[str], monkeypatch):
30+
monkeypatch.setenv('https_proxy', 'https://secure.proxy:1234')
31+
monkeypatch.setenv('http_proxy', 'http://insecure.proxy:6789')
32+
monkeypatch.setenv('no_proxy', no_proxy)
33+
34+
proxy_url = _get_proxy_url(target_uri)
35+
36+
assert proxy_url == expected

ldclient/testing/proxy_test_util.py

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ldclient.config import Config, HTTPConfig
2-
from ldclient.testing.http_util import start_server, BasicResponse, JsonResponse
2+
from ldclient.testing.http_util import start_server
3+
34

45
# Runs tests of all of our supported proxy server configurations: secure or insecure, configured
56
# 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):
1617
(False, True, False),
1718
(True, False, False),
1819
(True, False, True),
19-
(True, True, False)]:
20+
(True, True, False)
21+
]:
2022
test_desc = "%s, %s, %s" % (
2123
"using env vars" if use_env_vars else "using Config",
2224
"secure" if secure else "insecure",
@@ -27,23 +29,23 @@ def do_proxy_tests(action, action_method, monkeypatch):
2729
if use_env_vars:
2830
monkeypatch.setenv('https_proxy' if secure else 'http_proxy', proxy_uri)
2931
config = Config(
30-
sdk_key = 'sdk_key',
31-
base_uri = target_uri,
32-
events_uri = target_uri,
33-
stream_uri = target_uri,
34-
http = HTTPConfig(http_proxy=proxy_uri),
35-
diagnostic_opt_out = True)
32+
sdk_key='sdk_key',
33+
base_uri=target_uri,
34+
events_uri=target_uri,
35+
stream_uri=target_uri,
36+
http=HTTPConfig(http_proxy=proxy_uri),
37+
diagnostic_opt_out=True)
3638
try:
3739
action(server, config, secure)
38-
except:
40+
except Exception:
3941
print("test action failed (%s)" % test_desc)
4042
raise
4143
# For an insecure proxy request, our stub server behaves enough like the real thing to satisfy the
4244
# HTTP client, so we should be able to see the request go through. Note that the URI path will
4345
# actually be an absolute URI for a proxy request.
4446
try:
4547
req = server.require_request()
46-
except:
48+
except Exception:
4749
print("server did not receive a request (%s)" % test_desc)
4850
raise
4951
expected_method = 'CONNECT' if secure else action_method

0 commit comments

Comments
 (0)