diff --git a/CHANGELOG.md b/CHANGELOG.md index 11613126..9f7eac44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ release (0.8.0), unrecognized arguments/keywords for these methods of creating a instead of being passed as ClickHouse server settings. This is in conjunction with some refactoring in Client construction. The supported method of passing ClickHouse server settings is to prefix such arguments/query parameters with`ch_`. +## 0.7.15, 2024-07-01 +### Bug Fix +- If the ClickHouse server was behind an https proxy that required mutual TLS authentication, the client would incorrectly +attempt to use ClickHouse mutual TLS instead and authentication would fail. It should now be possible to authenticate +correctly in this situation by settings the `verify` parameter to `proxy`. This should close https://github.com/ClickHouse/clickhouse-connect/issues/370 + ## 0.7.14, 2024-06-24 ### Bug Fix - Fix insert of UUID strings including dashes. Closes #368 diff --git a/clickhouse_connect/__version__.py b/clickhouse_connect/__version__.py index 3c2c07c7..8dec87cb 100644 --- a/clickhouse_connect/__version__.py +++ b/clickhouse_connect/__version__.py @@ -1 +1 @@ -version = '0.7.14' +version = '0.7.15' diff --git a/clickhouse_connect/driver/common.py b/clickhouse_connect/driver/common.py index 84a91c94..dca0dc93 100644 --- a/clickhouse_connect/driver/common.py +++ b/clickhouse_connect/driver/common.py @@ -112,6 +112,12 @@ def dict_copy(source: Dict = None, update: Optional[Dict] = None) -> Dict: return copy +def dict_add(source: Dict, key: str, value: any) -> Dict: + if value is not None: + source[key] = value + return source + + def empty_gen(): yield from () diff --git a/clickhouse_connect/driver/httpclient.py b/clickhouse_connect/driver/httpclient.py index 0af2612f..03b5bbe3 100644 --- a/clickhouse_connect/driver/httpclient.py +++ b/clickhouse_connect/driver/httpclient.py @@ -16,7 +16,7 @@ from clickhouse_connect.datatypes import registry from clickhouse_connect.datatypes.base import ClickHouseType from clickhouse_connect.driver.client import Client -from clickhouse_connect.driver.common import dict_copy, coerce_bool, coerce_int +from clickhouse_connect.driver.common import dict_copy, coerce_bool, coerce_int, dict_add from clickhouse_connect.driver.compression import available_compression from clickhouse_connect.driver.ctypes import RespBuffCls from clickhouse_connect.driver.exceptions import DatabaseError, OperationalError, ProgrammingError @@ -58,7 +58,7 @@ def __init__(self, connect_timeout: int = 10, send_receive_timeout: int = 300, client_name: Optional[str] = None, - verify: bool = True, + verify: Union[bool, str] = True, ca_cert: Optional[str] = None, client_cert: Optional[str] = None, client_cert_key: Optional[str] = None, @@ -81,22 +81,23 @@ def __init__(self, if interface == 'https': if not https_proxy: https_proxy = check_env_proxy('https', host, port) - if client_cert: + if https_proxy and isinstance(verify, str) and verify.lower() == 'proxy': + verify = 'proxy' + else: + verify = coerce_bool(verify) + if client_cert and verify != 'proxy': if not username: raise ProgrammingError('username parameter is required for Mutual TLS authentication') self.headers['X-ClickHouse-User'] = username self.headers['X-ClickHouse-SSL-Certificate-Auth'] = 'on' - verify = coerce_bool(verify) # pylint: disable=too-many-boolean-expressions if not self.http and (server_host_name or ca_cert or client_cert or not verify or https_proxy): - options = { - 'ca_cert': ca_cert, - 'client_cert': client_cert, - 'verify': verify, - 'client_cert_key': client_cert_key - } + options = {'verify': verify is not False} + dict_add(options,'ca_cert', ca_cert) + dict_add(options, 'client_cert', client_cert) + dict_add(options, 'client_cert_key', client_cert_key) if server_host_name: - if verify: + if options['verify']: options['assert_hostname'] = server_host_name options['server_hostname'] = server_host_name self.http = get_pool_manager(https_proxy=https_proxy, **options) @@ -109,7 +110,7 @@ def __init__(self, else: self.http = default_pool_manager() - if not client_cert and username: + if (not client_cert or verify == 'proxy') and username: self.headers['Authorization'] = 'Basic ' + b64encode(f'{username}:{password}'.encode()).decode() self.headers['User-Agent'] = common.build_client_name(client_name) self._read_format = self._write_format = 'Native'