Skip to content

Commit

Permalink
add more TLS config options and collect HTTP/HTTPS config options in …
Browse files Browse the repository at this point in the history
…a class (#130)
  • Loading branch information
eli-darkly authored Mar 25, 2020
1 parent e754335 commit b7d081b
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 86 deletions.
102 changes: 93 additions & 9 deletions ldclient/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,71 @@
STREAM_FLAGS_PATH = '/flags'


class HTTPConfig(object):
"""Advanced HTTP configuration options for the SDK client.
This class groups together HTTP/HTTPS-related configuration properties that rarely need to be changed.
If you need to set these, construct an `HTTPConfig` instance and pass it as the `http` parameter when
you construct the main :class:`Config` for the SDK client.
For some of these properties, :class:`Config` also has properties with the same names; the latter are
deprecated and will be removed in the future, and if you specify an `HTTPConfig` instance then the
corresponding `Config` properties will be ignored.
"""
def __init__(self,
connect_timeout=10,
read_timeout=15,
http_proxy=None,
ca_certs=None,
cert_file=None,
disable_ssl_verification=False):
"""
:param float connect_timeout: The connect timeout for network connections in seconds.
:param float read_timeout: The read timeout for network connections in seconds.
: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.
:param string ca_certs: If using a custom certificate authority, set this to the file path of the
certificate bundle.
:param string cert_file: If using a custom client certificate, set this to the file path of the
certificate.
:param bool disable_ssl_verification: If true, completely disables SSL verification and certificate
verification for secure requests. This is unsafe and should not be used in a production environment;
instead, use a self-signed certificate and set `ca_certs`.
"""
self.__connect_timeout = connect_timeout
self.__read_timeout = read_timeout
self.__http_proxy = http_proxy
self.__ca_certs = ca_certs
self.__cert_file = cert_file
self.__disable_ssl_verification = disable_ssl_verification

@property
def connect_timeout(self):
return self.__connect_timeout

@property
def read_timeout(self):
return self.__read_timeout

@property
def http_proxy(self):
return self.__http_proxy

@property
def ca_certs(self):
return self.__ca_certs

@property
def cert_file(self):
return self.__cert_file

@property
def disable_ssl_verification(self):
return self.__disable_ssl_verification

class Config(object):
"""Advanced configuration options for the SDK client.
Expand Down Expand Up @@ -47,15 +112,18 @@ def __init__(self,
diagnostic_opt_out=False,
diagnostic_recording_interval=900,
wrapper_name=None,
wrapper_version=None):
wrapper_version=None,
http=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
value.
:param string events_uri: The URL for the LaunchDarkly events server. Most users should use the
default value.
:param float connect_timeout: The connect timeout for network connections in seconds.
:param float read_timeout: The read timeout for network connections in seconds.
:param float connect_timeout: Deprecated; use `http` instead and specify the `connect_timeout` as
part of :class:`HTTPConfig`.
:param float read_timeout: Deprecated; use `http` instead and specify the `read_timeout` as
part of :class:`HTTPConfig`.
:param int events_upload_max_batch_size: The maximum number of analytics events that the client will
send at once.
:param int events_max_pending: The capacity of the events buffer. The client buffers up to this many
Expand All @@ -67,6 +135,8 @@ def __init__(self,
use the default value.
:param bool stream: Whether or not the streaming API should be used to receive flag updates. By
default, it is enabled. Streaming should only be disabled on the advice of LaunchDarkly support.
:param bool verify_ssl: Deprecated; use `http` instead and specify `disable_ssl_verification` as
part of :class:`HTTPConfig` if you want to turn off SSL verification (not recommended).
:param bool send_events: Whether or not to send events back to LaunchDarkly. This differs from
`offline` in that it affects only the sending of client-side events, not streaming or polling for
events from the server. By default, events will be sent.
Expand Down Expand Up @@ -99,11 +169,8 @@ 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.
:param http_proxy: Deprecated; use `http` instead and specify the `http_proxy` as part of
:class:`HTTPConfig`.
:param bool diagnostic_opt_out: Unless this field is set to True, the client will send
some diagnostics data to the LaunchDarkly servers in order to assist in the development of future SDK
improvements. These diagnostics consist of an initial payload containing some details of SDK in use,
Expand All @@ -118,6 +185,8 @@ def __init__(self,
use. If `wrapper_name` is not set, this field will be ignored. Otherwise the version string will
be included in the HTTP headers along with the `wrapper_name` during requests to the LaunchDarkly
servers.
:param HTTPConfig http: Optional properties for customizing the client's HTTP/HTTPS behavior. See
:class:`HTTPConfig`.
"""
self.__sdk_key = sdk_key

Expand Down Expand Up @@ -154,6 +223,7 @@ def __init__(self,
self.__diagnostic_recording_interval = max(diagnostic_recording_interval, 60)
self.__wrapper_name = wrapper_name
self.__wrapper_version = wrapper_version
self.__http = http

@classmethod
def default(cls):
Expand Down Expand Up @@ -196,7 +266,8 @@ def copy_with_new_sdk_key(self, new_sdk_key):
diagnostic_opt_out=self.__diagnostic_opt_out,
diagnostic_recording_interval=self.__diagnostic_recording_interval,
wrapper_name=self.__wrapper_name,
wrapper_version=self.__wrapper_version)
wrapper_version=self.__wrapper_version,
http=self.__http)

# for internal use only - probably should be part of the client logic
def get_default(self, key, default):
Expand Down Expand Up @@ -335,6 +406,19 @@ def wrapper_name(self):
def wrapper_version(self):
return self.__wrapper_version

@property
def http(self):
if self.__http is None:
return HTTPConfig(
connect_timeout=self.__connect_timeout,
read_timeout=self.__read_timeout,
http_proxy=self.__http_proxy,
ca_certs=None,
cert_file=None,
disable_ssl_verification=not self.__verify_ssl
)
return self.__http

def _validate(self):
if self.offline is False and self.sdk_key is None or self.sdk_key == '':
log.warning("Missing or blank sdk_key.")
5 changes: 2 additions & 3 deletions ldclient/event_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@

from ldclient.event_summarizer import EventSummarizer
from ldclient.fixed_thread_pool import FixedThreadPool
from ldclient.impl.http import _http_factory
from ldclient.lru_cache import SimpleLRUCache
from ldclient.user_filter import UserFilter
from ldclient.interfaces import EventProcessor
from ldclient.repeating_timer import RepeatingTimer
from ldclient.util import UnsuccessfulResponseException
from ldclient.util import _headers, _retryable_statuses
from ldclient.util import create_http_pool_manager
from ldclient.util import log
from ldclient.util import http_error_message, is_http_error_recoverable, stringify_attrs, throw_if_unsuccessful_response
from ldclient.diagnostics import create_diagnostic_init
Expand Down Expand Up @@ -255,8 +255,7 @@ class EventDispatcher(object):
def __init__(self, inbox, config, http_client, diagnostic_accumulator=None):
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, force_proxy=config.http_proxy) if http_client is None else http_client
self._http = _http_factory(config).create_pool_manager(1, config.events_uri) 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)
Expand Down
5 changes: 2 additions & 3 deletions ldclient/feature_requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
import json
import urllib3

from ldclient.impl.http import _http_factory
from ldclient.interfaces import FeatureRequester
from ldclient.util import UnsuccessfulResponseException
from ldclient.util import _headers
from ldclient.util import create_http_pool_manager
from ldclient.util import log
from ldclient.util import throw_if_unsuccessful_response
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
Expand All @@ -25,8 +25,7 @@
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, force_proxy=config.http_proxy)
self._http = _http_factory(config).create_pool_manager(1, config.base_uri)
self._config = config

def get_all_data(self):
Expand Down
63 changes: 63 additions & 0 deletions ldclient/impl/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from ldclient.version import VERSION
import certifi
from os import environ
import urllib3

def _base_headers(config):
headers = {'Authorization': config.sdk_key,
'User-Agent': 'PythonClient/' + VERSION}
if isinstance(config.wrapper_name, str) and config.wrapper_name != "":
wrapper_version = ""
if isinstance(config.wrapper_version, str) and config.wrapper_version != "":
wrapper_version = "/" + config.wrapper_version
headers.update({'X-LaunchDarkly-Wrapper': config.wrapper_name + wrapper_version})
return headers

def _http_factory(config):
return HTTPFactory(_base_headers(config), config.http)

class HTTPFactory(object):
def __init__(self, base_headers, http_config):
self.__base_headers = base_headers
self.__http_config = http_config
self.__timeout = urllib3.Timeout(connect=http_config.connect_timeout, read=http_config.read_timeout)

@property
def base_headers(self):
return self.__base_headers

@property
def timeout(self):
return self.__timeout

def create_pool_manager(self, num_pools, target_base_uri):
proxy_url = self.__http_config.http_proxy or _get_proxy_url(target_base_uri)

if self.__http_config.disable_ssl_verification:
cert_reqs = 'CERT_NONE'
ca_certs = None
else:
cert_reqs = 'CERT_REQUIRED'
ca_certs = self.__http_config.ca_certs or certifi.where()

if proxy_url is None:
return urllib3.PoolManager(
num_pools=num_pools,
cert_reqs=cert_reqs,
ca_certs=ca_certs
)
else:
return urllib3.ProxyManager(
proxy_url,
num_pools=num_pools,
cert_reqs=cert_reqs,
ca_certs = ca_certs
)

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')
33 changes: 28 additions & 5 deletions ldclient/sse_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

import urllib3

from ldclient.util import create_http_pool_manager
from ldclient.config import HTTPConfig
from ldclient.impl.http import HTTPFactory
from ldclient.util import log
from ldclient.util import throw_if_unsuccessful_response

Expand All @@ -23,24 +24,46 @@

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, http_proxy=None, **kwargs):
verify_ssl=False, http=None, http_proxy=None, http_factory=None, **kwargs):
self.url = url
self.last_id = last_id
self.retry = retry
self._connect_timeout = connect_timeout
self._read_timeout = read_timeout
self._chunk_size = chunk_size

if http_factory:
self._timeout = http_factory.timeout
base_headers = http_factory.base_headers
else:
# for backward compatibility in case anyone else is using this class
self._timeout = urllib3.Timeout(connect=self._connect_timeout, read=self._read_timeout)
base_headers = {}

# 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,
force_proxy=http_proxy)
if http:
self.http = http
else:
hf = http_factory
if hf is None: # build from individual parameters which we're only retaining for backward compatibility
hc = HTTPConfig(
connect_timeout=connect_timeout,
read_timeout=read_timeout,
disable_ssl_verification=not verify_ssl,
http_proxy=http_proxy
)
hf = HTTPFactory({}, hc)
self.http = hf.create_pool_manager(1, url)

# Any extra kwargs will be fed into the request call later.
self.requests_kwargs = kwargs

# The SSE spec requires making requests with Cache-Control: nocache
if 'headers' not in self.requests_kwargs:
self.requests_kwargs['headers'] = {}

self.requests_kwargs['headers'].update(base_headers)

self.requests_kwargs['headers']['Cache-Control'] = 'no-cache'

# The 'Accept' header is not required, but explicit > implicit
Expand All @@ -59,7 +82,7 @@ def _connect(self):
self.resp = self.http.request(
'GET',
self.url,
timeout=urllib3.Timeout(connect=self._connect_timeout, read=self._read_timeout),
timeout=self._timeout,
preload_content=False,
retries=0, # caller is responsible for implementing appropriate retry semantics, e.g. backoff
**self.requests_kwargs)
Expand Down
10 changes: 4 additions & 6 deletions ldclient/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
import logging
import time

from ldclient.impl.http import _http_factory
from ldclient.interfaces import UpdateProcessor
from ldclient.sse_client import SSEClient
from ldclient.util import _stream_headers, log, UnsuccessfulResponseException, http_error_message, is_http_error_recoverable
from ldclient.util import log, UnsuccessfulResponseException, http_error_message, is_http_error_recoverable
from ldclient.versioned_data_kind import FEATURES, SEGMENTS

# allows for up to 5 minutes to elapse without any data sent across the stream. The heartbeats sent as comments on the
Expand Down Expand Up @@ -101,11 +102,8 @@ def log_backoff_message(props):
def _connect(self):
return SSEClient(
self._uri,
headers=_stream_headers(self._config),
connect_timeout=self._config.connect_timeout,
read_timeout=stream_read_timeout,
verify_ssl=self._config.verify_ssl,
http_proxy=self._config.http_proxy)
http_factory = _http_factory(self._config)
)

def stop(self):
log.info("Stopping StreamingUpdateProcessor")
Expand Down
Loading

0 comments on commit b7d081b

Please sign in to comment.