From: Tomás Farías Date: Sun, 1 Sep 2019 13:01:11 +0000 (-0300) Subject: Add support for SSLKEYLOGFILE (#301) X-Git-Tag: 0.7.3~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=db7f2d0df365daafbcc4e60fd7e96e8abc1a466e;p=thirdparty%2Fhttpx.git Add support for SSLKEYLOGFILE (#301) * Skip test if OpenSSL version is lower than 1.1.1 * Use bionic dist for Python 3.8 job * Pass trust_env to SSLConfig use monkeypatch in testing * Don't raise KeyError if SSLKEYLOGFILE is not set * Move trust_env after verify and cert --- diff --git a/.travis.yml b/.travis.yml index 07207f99..2a8da531 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ matrix: env: NOX_SESSION=test-3.7 - python: 3.8-dev env: NOX_SESSION=test-3.8 + dist: bionic # Required to get OpenSSL 1.1.1+ install: - pip install --upgrade nox diff --git a/docs/environment_variables.md b/docs/environment_variables.md index be1b5810..d8f3960d 100644 --- a/docs/environment_variables.md +++ b/docs/environment_variables.md @@ -44,3 +44,38 @@ user@host:~$ HTTPX_DEBUG=1 python test_script.py 20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=0 event= 20:54:17.743 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443)) ``` + +`SSLKEYLOGFILE` +----------- + +Valid values: a filename + +If this environment variable is set, TLS keys will be appended to the specified file, creating it if it doesn't exist, whenever key material is generated or received. The keylog file is designed for debugging purposes only. + +Support for `SSLKEYLOGFILE` requires Python 3.8 and OpenSSL 1.1.1 or newer. + +Example: + +```python +# test_script.py + +import httpx +client = httpx.Client() +client.get("https://google.com") +``` + +```console +SSLKEYLOGFILE=test.log python test_script.py +cat test.log +# TLS secrets log file, generated by OpenSSL / Python +SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX +EXPORTER_SECRET XXXX +SERVER_TRAFFIC_SECRET_0 XXXX +CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX +CLIENT_TRAFFIC_SECRET_0 XXXX +SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX +EXPORTER_SECRET XXXX +SERVER_TRAFFIC_SECRET_0 XXXX +CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX +CLIENT_TRAFFIC_SECRET_0 XXXX +``` diff --git a/httpx/client.py b/httpx/client.py index 44a09219..3c135e29 100644 --- a/httpx/client.py +++ b/httpx/client.py @@ -82,6 +82,8 @@ class BaseClient: else: dispatch = ASGIDispatch(app=app) + self.trust_env = True if trust_env is None else trust_env + if dispatch is None: async_dispatch: AsyncDispatcher = ConnectionPool( verify=verify, @@ -90,6 +92,7 @@ class BaseClient: http_versions=http_versions, pool_limits=pool_limits, backend=backend, + trust_env=self.trust_env, ) elif isinstance(dispatch, Dispatcher): async_dispatch = ThreadedDispatcher(dispatch, backend) @@ -107,7 +110,6 @@ class BaseClient: self.max_redirects = max_redirects self.dispatch = async_dispatch self.concurrency_backend = backend - self.trust_env = True if trust_env is None else trust_env @property def headers(self) -> Headers: diff --git a/httpx/config.py b/httpx/config.py index 4082a9f7..99e9240f 100644 --- a/httpx/config.py +++ b/httpx/config.py @@ -1,3 +1,4 @@ +import os import ssl import typing from pathlib import Path @@ -43,7 +44,13 @@ class SSLConfig: SSL Configuration. """ - def __init__(self, *, cert: CertTypes = None, verify: VerifyTypes = True): + def __init__( + self, + *, + cert: CertTypes = None, + verify: VerifyTypes = True, + trust_env: bool = None, + ): self.cert = cert # Allow passing in our own SSLContext object that's pre-configured. @@ -56,6 +63,7 @@ class SSLConfig: self.ssl_context: typing.Optional[ssl.SSLContext] = ssl_context self.verify: typing.Union[str, bool] = verify + self.trust_env = trust_env def __eq__(self, other: typing.Any) -> bool: return ( @@ -166,6 +174,11 @@ class SSLConfig: if ssl.HAS_NPN: # pragma: no cover context.set_npn_protocols(http_versions.alpn_identifiers) + if hasattr(context, "keylog_filename"): + keylogfile = os.environ.get("SSLKEYLOGFILE") + if keylogfile and self.trust_env: + context.keylog_filename = keylogfile # type: ignore + return context def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None: diff --git a/httpx/dispatch/connection.py b/httpx/dispatch/connection.py index b843c2e5..529d373a 100644 --- a/httpx/dispatch/connection.py +++ b/httpx/dispatch/connection.py @@ -33,13 +33,14 @@ class HTTPConnection(AsyncDispatcher): origin: typing.Union[str, Origin], verify: VerifyTypes = True, cert: CertTypes = None, + trust_env: bool = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, http_versions: HTTPVersionTypes = None, backend: ConcurrencyBackend = None, release_func: typing.Optional[ReleaseCallback] = None, ): self.origin = Origin(origin) if isinstance(origin, str) else origin - self.ssl = SSLConfig(cert=cert, verify=verify) + self.ssl = SSLConfig(cert=cert, verify=verify, trust_env=trust_env) self.timeout = TimeoutConfig(timeout) self.http_versions = HTTPVersionConfig(http_versions) self.backend = AsyncioBackend() if backend is None else backend diff --git a/httpx/dispatch/connection_pool.py b/httpx/dispatch/connection_pool.py index 36992091..befa88cb 100644 --- a/httpx/dispatch/connection_pool.py +++ b/httpx/dispatch/connection_pool.py @@ -84,6 +84,7 @@ class ConnectionPool(AsyncDispatcher): *, verify: VerifyTypes = True, cert: CertTypes = None, + trust_env: bool = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, pool_limits: PoolLimits = DEFAULT_POOL_LIMITS, http_versions: HTTPVersionTypes = None, @@ -95,6 +96,7 @@ class ConnectionPool(AsyncDispatcher): self.pool_limits = pool_limits self.http_versions = http_versions self.is_closed = False + self.trust_env = trust_env self.keepalive_connections = ConnectionStore() self.active_connections = ConnectionStore() @@ -145,6 +147,7 @@ class ConnectionPool(AsyncDispatcher): http_versions=self.http_versions, backend=self.backend, release_func=self.release_connection, + trust_env=self.trust_env, ) logger.debug(f"new_connection connection={connection!r}") else: diff --git a/tests/test_config.py b/tests/test_config.py index 0df1029b..fefc06d3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,4 +1,5 @@ import ssl +import sys import pytest @@ -171,3 +172,33 @@ def test_timeout_from_tuple(): def test_timeout_from_config_instance(): timeout = httpx.TimeoutConfig(timeout=5.0) assert httpx.TimeoutConfig(timeout) == httpx.TimeoutConfig(timeout=5.0) + + +@pytest.mark.skipif( + not hasattr(ssl.SSLContext, "keylog_filename"), + reason="requires OpenSSL 1.1.1 or higher", +) +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python3.8 or higher") +def test_ssl_config_support_for_keylog_file(tmpdir, monkeypatch): + with monkeypatch.context() as m: + m.delenv("SSLKEYLOGFILE", raising=False) + + ssl_config = httpx.SSLConfig(trust_env=True) + ssl_config.load_ssl_context() + + assert ssl_config.ssl_context.keylog_filename is None + + filename = str(tmpdir.join("test.log")) + + with monkeypatch.context() as m: + m.setenv("SSLKEYLOGFILE", filename) + + ssl_config = httpx.SSLConfig(trust_env=True) + ssl_config.load_ssl_context() + + assert ssl_config.ssl_context.keylog_filename == filename + + ssl_config = httpx.SSLConfig(trust_env=False) + ssl_config.load_ssl_context() + + assert ssl_config.ssl_context.keylog_filename is None