diff --git a/ldclient/config.py b/ldclient/config.py index b0283d95..44da64f0 100644 --- a/ldclient/config.py +++ b/ldclient/config.py @@ -43,7 +43,8 @@ def __init__(self, offline=False, user_keys_capacity=1000, user_keys_flush_interval=300, - inline_users_in_events=False): + inline_users_in_events=False, + http_proxy=None): """ :param string sdk_key: The SDK key for your LaunchDarkly account. :param string base_uri: The base URL for the LaunchDarkly server. Most users should use the default @@ -95,6 +96,11 @@ def __init__(self, :type event_processor_class: (ldclient.config.Config) -> EventProcessor :param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key, config, and FeatureStore implementation + :param http_proxy: Use a proxy when connecting to LaunchDarkly. This is the full URI of the + proxy; for example: http://my-proxy.com:1234. Note that unlike the standard `http_proxy` environment + variable, this is used regardless of whether the target URI is HTTP or HTTPS (the actual LaunchDarkly + service uses HTTPS, but a Relay Proxy instance could use HTTP). Setting this Config parameter will + override any proxy specified by an environment variable, but only for LaunchDarkly SDK connections. """ self.__sdk_key = sdk_key @@ -126,6 +132,7 @@ def __init__(self, self.__user_keys_capacity = user_keys_capacity self.__user_keys_flush_interval = user_keys_flush_interval self.__inline_users_in_events = inline_users_in_events + self.__http_proxy = http_proxy @classmethod def default(cls): @@ -278,6 +285,10 @@ def user_keys_flush_interval(self): def inline_users_in_events(self): return self.__inline_users_in_events + @property + def http_proxy(self): + return self.__http_proxy + def _validate(self): if self.offline is False and self.sdk_key is None or self.sdk_key is '': log.warning("Missing or blank sdk_key.") diff --git a/ldclient/event_processor.py b/ldclient/event_processor.py index 1f9c5649..93680c13 100644 --- a/ldclient/event_processor.py +++ b/ldclient/event_processor.py @@ -212,7 +212,7 @@ def __init__(self, inbox, config, http_client): self._inbox = inbox self._config = config self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl, - target_base_uri=config.events_uri) if http_client is None else http_client + target_base_uri=config.events_uri, force_proxy=config.http_proxy) if http_client is None else http_client self._close_http = (http_client is None) # so we know whether to close it later self._disabled = False self._outbox = EventBuffer(config.events_max_pending) diff --git a/ldclient/feature_requester.py b/ldclient/feature_requester.py index e14ebfe5..6af810a5 100644 --- a/ldclient/feature_requester.py +++ b/ldclient/feature_requester.py @@ -25,7 +25,8 @@ class FeatureRequesterImpl(FeatureRequester): def __init__(self, config): self._cache = dict() - self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl, target_base_uri=config.base_uri) + self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl, + target_base_uri=config.base_uri, force_proxy=config.http_proxy) self._config = config def get_all_data(self): diff --git a/ldclient/sse_client.py b/ldclient/sse_client.py index 4aeee9f2..23a0209e 100644 --- a/ldclient/sse_client.py +++ b/ldclient/sse_client.py @@ -23,7 +23,7 @@ class SSEClient(object): def __init__(self, url, last_id=None, retry=3000, connect_timeout=10, read_timeout=300, chunk_size=10000, - verify_ssl=False, http=None, **kwargs): + verify_ssl=False, http=None, http_proxy=None, **kwargs): self.url = url self.last_id = last_id self.retry = retry @@ -32,7 +32,8 @@ def __init__(self, url, last_id=None, retry=3000, connect_timeout=10, read_timeo self._chunk_size = chunk_size # Optional support for passing in an HTTP client - self.http = create_http_pool_manager(num_pools=1, verify_ssl=verify_ssl, target_base_uri=url) + self.http = create_http_pool_manager(num_pools=1, verify_ssl=verify_ssl, target_base_uri=url, + force_proxy=http_proxy) # Any extra kwargs will be fed into the request call later. self.requests_kwargs = kwargs diff --git a/ldclient/streaming.py b/ldclient/streaming.py index 75a56840..b3638621 100644 --- a/ldclient/streaming.py +++ b/ldclient/streaming.py @@ -89,7 +89,8 @@ def _connect(self): headers=_stream_headers(self._config.sdk_key), connect_timeout=self._config.connect_timeout, read_timeout=stream_read_timeout, - verify_ssl=self._config.verify_ssl) + verify_ssl=self._config.verify_ssl, + http_proxy=self._config.http_proxy) def stop(self): log.info("Stopping StreamingUpdateProcessor") diff --git a/ldclient/util.py b/ldclient/util.py index 98ad4357..1d059798 100644 --- a/ldclient/util.py +++ b/ldclient/util.py @@ -85,8 +85,8 @@ def status(self): return self._status -def create_http_pool_manager(num_pools=1, verify_ssl=False, target_base_uri=None): - proxy_url = _get_proxy_url(target_base_uri) +def create_http_pool_manager(num_pools=1, verify_ssl=False, target_base_uri=None, force_proxy=None): + proxy_url = force_proxy or _get_proxy_url(target_base_uri) if not verify_ssl: if proxy_url is None: diff --git a/testing/test_event_processor.py b/testing/test_event_processor.py index 44ed3609..75093a3d 100644 --- a/testing/test_event_processor.py +++ b/testing/test_event_processor.py @@ -469,41 +469,51 @@ def start_consuming_events(): assert had_no_more def test_can_use_http_proxy_via_environment_var(monkeypatch): - fake_events_uri = 'http://not-real' - with start_server() as server: monkeypatch.setenv('http_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', events_uri = fake_events_uri) - server.setup_response(fake_events_uri + '/bulk', 200, None) - - with DefaultEventProcessor(config) as ep: - ep.send_event({ 'kind': 'identify', 'user': user }) - ep.flush() - ep._wait_until_inactive() - - # 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. - req = server.require_request() - assert req.method == 'POST' + config = Config(sdk_key = 'sdk-key', events_uri = 'http://not-real') + _verify_http_proxy_is_used(server, config) def test_can_use_https_proxy_via_environment_var(monkeypatch): - fake_events_uri = 'https://not-real' - with start_server() as server: monkeypatch.setenv('https_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', events_uri = fake_events_uri) - server.setup_response(fake_events_uri + '/bulk', 200, None) - - with DefaultEventProcessor(config) as ep: - ep.send_event({ 'kind': 'identify', 'user': user }) - ep.flush() - ep._wait_until_inactive() - - # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but - # it can still record that it *got* the request, which proves that the request went to the proxy. - req = server.require_request() - assert req.method == 'CONNECT' + config = Config(sdk_key = 'sdk-key', events_uri = 'https://not-real') + _verify_https_proxy_is_used(server, config) + +def test_can_use_http_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', events_uri = 'http://not-real', http_proxy=server.uri) + _verify_http_proxy_is_used(server, config) + +def test_can_use_https_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', events_uri = 'https://not-real', http_proxy=server.uri) + _verify_https_proxy_is_used(server, config) + +def _verify_http_proxy_is_used(server, config): + server.setup_response(config.events_uri + '/bulk', 200, None) + with DefaultEventProcessor(config) as ep: + ep.send_event({ 'kind': 'identify', 'user': user }) + ep.flush() + ep._wait_until_inactive() + + # 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. + req = server.require_request() + assert req.method == 'POST' + +def _verify_https_proxy_is_used(server, config): + server.setup_response(config.events_uri + '/bulk', 200, None) + with DefaultEventProcessor(config) as ep: + ep.send_event({ 'kind': 'identify', 'user': user }) + ep.flush() + ep._wait_until_inactive() + + # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but + # it can still record that it *got* the request, which proves that the request went to the proxy. + req = server.require_request() + assert req.method == 'CONNECT' def verify_unrecoverable_http_error(status): with DefaultEventProcessor(Config(sdk_key = 'SDK_KEY'), mock_http) as ep: diff --git a/testing/test_feature_requester.py b/testing/test_feature_requester.py index 45239567..658c8157 100644 --- a/testing/test_feature_requester.py +++ b/testing/test_feature_requester.py @@ -127,39 +127,53 @@ def test_get_one_flag_does_not_use_etags(): assert 'If-None-Match' not in req.headers.keys() # did not send etag from previous request def test_can_use_http_proxy_via_environment_var(monkeypatch): - fake_base_uri = 'http://not-real' with start_server() as server: monkeypatch.setenv('http_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', base_uri = fake_base_uri) - fr = FeatureRequesterImpl(config) - - resp_data = { 'flags': {}, 'segments': {} } - expected_data = { FEATURES: {}, SEGMENTS: {} } - server.setup_json_response(fake_base_uri + '/sdk/latest-all', resp_data) - - # 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. - result = fr.get_all_data() - assert result == expected_data - req = server.require_request() - assert req.method == 'GET' + config = Config(sdk_key = 'sdk-key', base_uri = 'http://not-real') + _verify_http_proxy_is_used(server, config) def test_can_use_https_proxy_via_environment_var(monkeypatch): - fake_base_uri = 'https://not-real' with start_server() as server: monkeypatch.setenv('https_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', base_uri = fake_base_uri) - fr = FeatureRequesterImpl(config) + config = Config(sdk_key = 'sdk-key', base_uri = 'https://not-real') + _verify_https_proxy_is_used(server, config) - resp_data = { 'flags': {}, 'segments': {} } - server.setup_json_response(fake_base_uri + '/sdk/latest-all', resp_data) - - # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but - # it can still record that it *got* the request, which proves that the request went to the proxy. - try: - fr.get_all_data() - except: - pass - req = server.require_request() - assert req.method == 'CONNECT' +def test_can_use_http_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', base_uri = 'http://not-real', http_proxy = server.uri) + _verify_http_proxy_is_used(server, config) + +def test_can_use_https_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', base_uri = 'https://not-real', http_proxy = server.uri) + _verify_https_proxy_is_used(server, config) + +def _verify_http_proxy_is_used(server, config): + fr = FeatureRequesterImpl(config) + + resp_data = { 'flags': {}, 'segments': {} } + expected_data = { FEATURES: {}, SEGMENTS: {} } + server.setup_json_response(config.base_uri + '/sdk/latest-all', resp_data) + + # 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. + result = fr.get_all_data() + assert result == expected_data + req = server.require_request() + assert req.method == 'GET' + +def _verify_https_proxy_is_used(server, config): + fr = FeatureRequesterImpl(config) + + resp_data = { 'flags': {}, 'segments': {} } + server.setup_json_response(config.base_uri + '/sdk/latest-all', resp_data) + + # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but + # it can still record that it *got* the request, which proves that the request went to the proxy. + try: + fr.get_all_data() + except: + pass + req = server.require_request() + assert req.method == 'CONNECT' \ No newline at end of file diff --git a/testing/test_streaming.py b/testing/test_streaming.py index ba2899c0..65ba0542 100644 --- a/testing/test_streaming.py +++ b/testing/test_streaming.py @@ -44,38 +44,48 @@ def test_sends_headers(): assert req.headers['User-Agent'] == 'PythonClient/' + VERSION def test_can_use_http_proxy_via_environment_var(monkeypatch): - store = InMemoryFeatureStore() - ready = Event() - fake_stream_uri = 'http://not-real' - with start_server() as server: + config = Config(sdk_key = 'sdk-key', stream_uri = 'http://not-real') monkeypatch.setenv('http_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', stream_uri = fake_stream_uri) - server.setup_response(fake_stream_uri + '/all', 200, fake_event, { 'Content-Type': 'text/event-stream' }) - - with StreamingUpdateProcessor(config, None, store, ready) as sp: - sp.start() - # 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. - req = server.await_request() - assert req.method == 'GET' - ready.wait(1) - assert sp.initialized() + _verify_http_proxy_is_used(server, config) def test_can_use_https_proxy_via_environment_var(monkeypatch): - store = InMemoryFeatureStore() - ready = Event() - fake_stream_uri = 'https://not-real' - with start_server() as server: + config = Config(sdk_key = 'sdk-key', stream_uri = 'https://not-real') monkeypatch.setenv('https_proxy', server.uri) - config = Config(sdk_key = 'sdk-key', stream_uri = fake_stream_uri) - server.setup_response(fake_stream_uri + '/all', 200, fake_event, { 'Content-Type': 'text/event-stream' }) + _verify_https_proxy_is_used(server, config) - with StreamingUpdateProcessor(config, None, store, ready) as sp: - sp.start() - # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but - # it can still record that it *got* the request, which proves that the request went to the proxy. - req = server.await_request() - assert req.method == 'CONNECT' +def test_can_use_http_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', stream_uri = 'http://not-real', http_proxy=server.uri) + _verify_http_proxy_is_used(server, config) + +def test_can_use_https_proxy_via_config(): + with start_server() as server: + config = Config(sdk_key = 'sdk-key', stream_uri = 'https://not-real', http_proxy=server.uri) + _verify_https_proxy_is_used(server, config) + +def _verify_http_proxy_is_used(server, config): + store = InMemoryFeatureStore() + ready = Event() + server.setup_response(config.stream_base_uri + '/all', 200, fake_event, { 'Content-Type': 'text/event-stream' }) + with StreamingUpdateProcessor(config, None, store, ready) as sp: + sp.start() + # 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. + req = server.await_request() + assert req.method == 'GET' + ready.wait(1) + assert sp.initialized() + +def _verify_https_proxy_is_used(server, config): + store = InMemoryFeatureStore() + ready = Event() + server.setup_response(config.stream_base_uri + '/all', 200, fake_event, { 'Content-Type': 'text/event-stream' }) + with StreamingUpdateProcessor(config, None, store, ready) as sp: + sp.start() + # Our simple stub server implementation can't really do HTTPS proxying, so the request will fail, but + # it can still record that it *got* the request, which proves that the request went to the proxy. + req = server.await_request() + assert req.method == 'CONNECT'