From 792bdd040a460d0f77309ed1c7d9fea236542140 Mon Sep 17 00:00:00 2001 From: Brandon Hastings Date: Wed, 10 Feb 2021 23:16:48 -0800 Subject: [PATCH] Support for client to verify server with custom CA bundle --- engineio/client.py | 13 +++++-- tests/common/test_client.py | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/engineio/client.py b/engineio/client.py index 524c1a66..70044871 100644 --- a/engineio/client.py +++ b/engineio/client.py @@ -64,7 +64,7 @@ class Client(object): :param http_session: an initialized ``requests.Session`` object to be used when sending requests to the server. Use it if you need to add special client options such as proxy - servers, SSL certificates, etc. + servers, SSL certificates, custom CA bundle, etc. :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to skip SSL certificate verification, allowing connections to servers with self signed certificates. @@ -405,7 +405,12 @@ def _connect_websocket(self, url, headers, engineio_path): else None) # verify - if not self.http.verify: + if isinstance(self.http.verify, str): + if 'sslopt' in extra_options: + extra_options['sslopt']['ca_certs'] = self.http.verify + else: + extra_options['sslopt'] = {'ca_certs': self.http.verify} + elif not self.http.verify: self.ssl_verify = False if not self.ssl_verify: @@ -515,9 +520,11 @@ def _send_request( timeout=None): # pragma: no cover if self.http is None: self.http = requests.Session() + if not self.ssl_verify: + self.http.verify = False try: return self.http.request(method, url, headers=headers, data=body, - timeout=timeout, verify=self.ssl_verify) + timeout=timeout) except requests.exceptions.RequestException as exc: self.logger.info('HTTP %s request to %s failed with error %s.', method, url, exc) diff --git a/tests/common/test_client.py b/tests/common/test_client.py index 532c0783..262949a1 100644 --- a/tests/common/test_client.py +++ b/tests/common/test_client.py @@ -897,6 +897,45 @@ def test_websocket_connection_with_cert_and_key(self, create_connection): 'enable_multithread': True, } + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_verify_with_cert_and_key( + self, create_connection + ): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = ('foo.crt', 'key.pem') + http.verify = 'ca-bundle.crt' + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'sslopt': { + 'certfile': 'foo.crt', + 'keyfile': 'key.pem', + 'ca_certs': 'ca-bundle.crt' + }, + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } + @mock.patch('engineio.client.websocket.create_connection') def test_websocket_connection_with_proxies(self, create_connection): all_urls = [ @@ -996,6 +1035,39 @@ def test_websocket_connection_without_verify(self, create_connection): 'enable_multithread': True, } + @mock.patch('engineio.client.websocket.create_connection') + def test_websocket_connection_with_verify(self, create_connection): + create_connection.return_value.recv.return_value = packet.Packet( + packet.OPEN, + { + 'sid': '123', + 'upgrades': [], + 'pingInterval': 1000, + 'pingTimeout': 2000, + }, + ).encode() + http = mock.MagicMock() + http.cookies = [] + http.auth = None + http.proxies = None + http.cert = None + http.verify = 'ca-bundle.crt' + c = client.Client(http_session=http) + c._read_loop_polling = mock.MagicMock() + c._read_loop_websocket = mock.MagicMock() + c._write_loop = mock.MagicMock() + on_connect = mock.MagicMock() + c.on('connect', on_connect) + c.connect('ws://foo', transports=['websocket']) + + assert len(create_connection.call_args_list) == 1 + assert create_connection.call_args[1] == { + 'sslopt': {'ca_certs': 'ca-bundle.crt'}, + 'header': {}, + 'cookie': '', + 'enable_multithread': True, + } + @mock.patch('engineio.client.websocket.create_connection') def test_websocket_upgrade_no_pong(self, create_connection): create_connection.return_value.recv.return_value = packet.Packet(