Skip to content

Commit

Permalink
feat: NO_PROXY environment variable can be used to override `HTTP(S…
Browse files Browse the repository at this point in the history
…)_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.
  • Loading branch information
keelerm84 committed Aug 7, 2024
1 parent bd3b2f8 commit 053eef4
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 17 deletions.
42 changes: 35 additions & 7 deletions ldclient/impl/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
36 changes: 36 additions & 0 deletions ldclient/testing/impl/test_http.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 12 additions & 10 deletions ldclient/testing/proxy_test_util.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
Expand All @@ -27,23 +29,23 @@ 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
# HTTP client, so we should be able to see the request go through. Note that the URI path will
# 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
Expand Down

0 comments on commit 053eef4

Please sign in to comment.