diff --git a/splitio/api/client.py b/splitio/api/client.py index 02eff8c2..c7a37194 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -19,7 +19,7 @@ TELEMETRY_URL = 'https://telemetry.split.io/api' _LOGGER = logging.getLogger(__name__) - +_EXC_MSG = '{source} library is throwing exceptions' HttpResponse = namedtuple('HttpResponse', ['status_code', 'body', 'headers']) @@ -122,7 +122,7 @@ def _get_headers(self, extra_headers, sdk_key): class HttpClient(HttpClientBase): """HttpClient wrapper.""" - def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): + def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None): """ Class constructor. @@ -140,8 +140,6 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t _LOGGER.debug("Initializing httpclient") self._timeout = timeout/1000 if timeout else None # Convert ms to seconds. self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url) - self._authentication_scheme = authentication_scheme - self._authentication_params = authentication_params self._lock = threading.RLock() def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments @@ -162,22 +160,19 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: :return: Tuple of status_code & response text :rtype: HttpResponse """ - with self._lock: - start = get_current_epoch_time_ms() - with requests.Session() as session: - self._set_authentication(session) - try: - response = session.get( - _build_url(server, path, self._urls), - params=query, - headers=self._get_headers(extra_headers, sdk_key), - timeout=self._timeout - ) - self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) - return HttpResponse(response.status_code, response.text, response.headers) + start = get_current_epoch_time_ms() + try: + response = requests.get( + _build_url(server, path, self._urls), + params=query, + headers=self._get_headers(extra_headers, sdk_key), + timeout=self._timeout + ) + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) - except Exception as exc: # pylint: disable=broad-except - raise HttpClientException('requests library is throwing exceptions') from exc + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -199,37 +194,19 @@ def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # :return: Tuple of status_code & response text :rtype: HttpResponse """ - with self._lock: - start = get_current_epoch_time_ms() - with requests.Session() as session: - self._set_authentication(session) - try: - response = session.post( - _build_url(server, path, self._urls), - json=body, - params=query, - headers=self._get_headers(extra_headers, sdk_key), - timeout=self._timeout, - ) - self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) - return HttpResponse(response.status_code, response.text, response.headers) - except Exception as exc: # pylint: disable=broad-except - raise HttpClientException('requests library is throwing exceptions') from exc - - def _set_authentication(self, session): - if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO: - _LOGGER.debug("Using Kerberos Spnego Authentication") - if self._authentication_params != [None, None]: - session.auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL) - else: - session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) - elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY: - _LOGGER.debug("Using Kerberos Proxy Authentication") - if self._authentication_params != [None, None]: - session.mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1])) - else: - session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) - + start = get_current_epoch_time_ms() + try: + response = requests.post( + _build_url(server, path, self._urls), + json=body, + params=query, + headers=self._get_headers(extra_headers, sdk_key), + timeout=self._timeout, + ) + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc def _record_telemetry(self, status_code, elapsed): """ @@ -306,7 +283,7 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py return HttpResponse(response.status, body, response.headers) except aiohttp.ClientError as exc: # pylint: disable=broad-except - raise HttpClientException('aiohttp library is throwing exceptions') from exc + raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc async def post(self, server, path, apikey, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ @@ -350,7 +327,7 @@ async def post(self, server, path, apikey, body, query=None, extra_headers=None) return HttpResponse(response.status, body, response.headers) except aiohttp.ClientError as exc: # pylint: disable=broad-except - raise HttpClientException('aiohttp library is throwing exceptions') from exc + raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc async def _record_telemetry(self, status_code, elapsed): """ @@ -372,3 +349,111 @@ async def _record_telemetry(self, status_code, elapsed): async def close_session(self): if not self._session.closed: await self._session.close() + +class HttpClientKerberos(HttpClient): + """HttpClient wrapper.""" + + def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None, authentication_scheme=None, authentication_params=None): + """ + Class constructor. + + :param timeout: How many milliseconds to wait until the server responds. + :type timeout: int + :param sdk_url: Optional alternative sdk URL. + :type sdk_url: str + :param events_url: Optional alternative events URL. + :type events_url: str + :param auth_url: Optional alternative auth URL. + :type auth_url: str + :param telemetry_url: Optional alternative telemetry URL. + :type telemetry_url: str + """ + _LOGGER.debug("Initializing httpclient for Kerberos auth") + HttpClient.__init__(self, timeout=timeout, sdk_url=sdk_url, events_url=events_url, auth_url=auth_url, telemetry_url=telemetry_url) + self._authentication_scheme = authentication_scheme + self._authentication_params = authentication_params + + def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments + """ + Issue a get request. + :param server: Whether the request is for SDK server, Events server or Auth server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + with self._lock: + start = get_current_epoch_time_ms() + with requests.Session() as session: + self._set_authentication(session) + try: + response = session.get( + _build_url(server, path, self._urls), + headers=self._get_headers(extra_headers, sdk_key), + params=query, + timeout=self._timeout + ) + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) + + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc + + def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments + """ + Issue a POST request. + + :param server: Whether the request is for SDK server or Events server. + :typee server: str + :param path: path to append to the host url. + :type path: str + :param sdk_key: sdk key. + :type sdk_key: str + :param body: body sent in the request. + :type body: str + :param query: Query string passed as dictionary. + :type query: dict + :param extra_headers: key/value pairs of possible extra headers. + :type extra_headers: dict + + :return: Tuple of status_code & response text + :rtype: HttpResponse + """ + with self._lock: + start = get_current_epoch_time_ms() + with requests.Session() as session: + self._set_authentication(session) + try: + response = session.post( + _build_url(server, path, self._urls), + params=query, + headers=self._get_headers(extra_headers, sdk_key), + json=body, + timeout=self._timeout, + ) + self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start) + return HttpResponse(response.status_code, response.text, response.headers) + except Exception as exc: # pylint: disable=broad-except + raise HttpClientException(_EXC_MSG.format(source='request')) from exc + + def _set_authentication(self, session): + if self._authentication_scheme == AuthenticateScheme.KERBEROS_SPNEGO: + _LOGGER.debug("Using Kerberos Spnego Authentication") + if self._authentication_params != [None, None]: + session.auth = HTTPKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1], mutual_authentication=OPTIONAL) + else: + session.auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL) + elif self._authentication_scheme == AuthenticateScheme.KERBEROS_PROXY: + _LOGGER.debug("Using Kerberos Proxy Authentication") + if self._authentication_params != [None, None]: + session.mount('https://', HTTPAdapterWithProxyKerberosAuth(principal=self._authentication_params[0], password=self._authentication_params[1])) + else: + session.mount('https://', HTTPAdapterWithProxyKerberosAuth()) diff --git a/splitio/client/factory.py b/splitio/client/factory.py index fffb0212..8c3b7572 100644 --- a/splitio/client/factory.py +++ b/splitio/client/factory.py @@ -33,7 +33,7 @@ PluggableImpressionsStorageAsync, PluggableSegmentStorageAsync, PluggableSplitStorageAsync # APIs -from splitio.api.client import HttpClient, HttpClientAsync +from splitio.api.client import HttpClient, HttpClientAsync, HttpClientKerberos from splitio.api.splits import SplitsAPI, SplitsAPIAsync from splitio.api.segments import SegmentsAPI, SegmentsAPIAsync from splitio.api.impressions import ImpressionsAPI, ImpressionsAPIAsync @@ -512,16 +512,23 @@ def _build_in_memory_factory(api_key, cfg, sdk_url=None, events_url=None, # pyl if cfg.get("httpAuthenticateScheme") in [AuthenticateScheme.KERBEROS_SPNEGO, AuthenticateScheme.KERBEROS_PROXY]: authentication_params = [cfg.get("kerberosPrincipalUser"), cfg.get("kerberosPrincipalPassword")] - - http_client = HttpClient( - sdk_url=sdk_url, - events_url=events_url, - auth_url=auth_api_base_url, - telemetry_url=telemetry_api_base_url, - timeout=cfg.get('connectionTimeout'), - authentication_scheme = cfg.get("httpAuthenticateScheme"), - authentication_params = authentication_params - ) + http_client = HttpClientKerberos( + sdk_url=sdk_url, + events_url=events_url, + auth_url=auth_api_base_url, + telemetry_url=telemetry_api_base_url, + timeout=cfg.get('connectionTimeout'), + authentication_scheme = cfg.get("httpAuthenticateScheme"), + authentication_params = authentication_params + ) + else: + http_client = HttpClient( + sdk_url=sdk_url, + events_url=events_url, + auth_url=auth_api_base_url, + telemetry_url=telemetry_api_base_url, + timeout=cfg.get('connectionTimeout'), + ) sdk_metadata = util.get_metadata(cfg) apis = { diff --git a/tests/api/test_httpclient.py b/tests/api/test_httpclient.py index d95dcb5f..621e696a 100644 --- a/tests/api/test_httpclient.py +++ b/tests/api/test_httpclient.py @@ -20,7 +20,7 @@ def test_get(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -54,7 +54,7 @@ def test_get_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', 'test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) @@ -89,7 +89,7 @@ def test_post(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient() httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -125,7 +125,7 @@ def test_post_custom_urls(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.post('sdk', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) @@ -160,7 +160,7 @@ def test_authentication_scheme(self, mocker): get_mock = mocker.Mock() get_mock.return_value = response_mock mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -168,14 +168,13 @@ def test_authentication_scheme(self, mocker): headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -183,29 +182,38 @@ def test_authentication_scheme(self, mocker): headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(principal='bilal', password='split', mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + response_mock = mocker.Mock() + response_mock.status_code = 200 + response_mock.headers = {} + response_mock.text = 'ok' + get_mock = mocker.Mock() + get_mock.return_value = response_mock + mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', events_url='https://events.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) httpclient.set_telemetry_data("metric", mocker.Mock()) - response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) + + response = httpclient.post('events', 'test1', 'some_api_key', {'p1': 'a'}, {'param1': 123}, {'h1': 'abc'}) call = mocker.call( - 'https://sdk.com/test1', + 'https://events.com/test1', + json={'p1': 'a'}, headers={'Authorization': 'Bearer some_api_key', 'h1': 'abc', 'Content-Type': 'application/json'}, params={'param1': 123}, timeout=None -# auth=HTTPKerberosAuth(mutual_authentication=OPTIONAL) ) assert response.status_code == 200 assert response.body == 'ok' assert get_mock.mock_calls == [call] get_mock.reset_mock() - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) httpclient.set_telemetry_data("metric", mocker.Mock()) response = httpclient.get('sdk', '/test1', 'some_api_key', {'param1': 123}, {'h1': 'abc'}) call = mocker.call( @@ -220,28 +228,28 @@ def test_authentication_scheme(self, mocker): get_mock.reset_mock() # test auth settings - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=['bilal', 'split']) my_session = requests.Session() httpclient._set_authentication(my_session) assert(my_session.auth.principal == 'bilal') assert(my_session.auth.password == 'split') assert(isinstance(my_session.auth, HTTPKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_SPNEGO, authentication_params=[None, None]) my_session2 = requests.Session() httpclient._set_authentication(my_session2) assert(my_session2.auth.principal == None) assert(my_session2.auth.password == None) assert(isinstance(my_session2.auth, HTTPKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=['bilal', 'split']) my_session = requests.Session() httpclient._set_authentication(my_session) assert(my_session.adapters['https://']._principal == 'bilal') assert(my_session.adapters['https://']._password == 'split') assert(isinstance(my_session.adapters['https://'], client.HTTPAdapterWithProxyKerberosAuth)) - httpclient = client.HttpClient(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) + httpclient = client.HttpClientKerberos(sdk_url='https://sdk.com', authentication_scheme=AuthenticateScheme.KERBEROS_PROXY, authentication_params=[None, None]) my_session2 = requests.Session() httpclient._set_authentication(my_session2) assert(my_session2.adapters['https://']._principal == None) @@ -259,7 +267,7 @@ def test_telemetry(self, mocker): response_mock.text = 'ok' get_mock = mocker.Mock() get_mock.return_value = response_mock - mocker.patch('splitio.api.client.requests.Session.post', new=get_mock) + mocker.patch('splitio.api.client.requests.post', new=get_mock) httpclient = client.HttpClient(sdk_url='https://sdk.com', events_url='https://events.com') httpclient.set_telemetry_data("metric", telemetry_runtime_producer) @@ -297,7 +305,7 @@ def record_sync_error(metric_name, elapsed): assert (self.status == 400) # testing get call - mocker.patch('splitio.api.client.requests.Session.get', new=get_mock) + mocker.patch('splitio.api.client.requests.get', new=get_mock) self.metric1 = None self.cur_time = 0 self.metric2 = None