From 1f2268a878f095bda28cf8ee09da6c68007c90a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Mar 2023 21:40:47 -1000 Subject: [PATCH] Fix httpx client creating a new ssl context with each client (memory leak) (#90191) * Fix httpx client creating a new ssl context with each client While working on https://github.com/home-assistant/core/issues/83524 it was discovered that each new httpx client creates a new ssl context https://github.com/encode/httpx/blob/f1157dbc4102ac8e227a0a0bb12a877f592eff58/httpx/_transports/default.py#L261 If an ssl context is passed in creating a new one is avoided here https://github.com/encode/httpx/blob/f1157dbc4102ac8e227a0a0bb12a877f592eff58/httpx/_config.py#L110 This change makes httpx ssl no-verify behavior match aiohttp ssl no-verify behavior https://github.com/aio-libs/aiohttp/blob/6da04694fd87a39af9c3856048c9ff23ca815f88/aiohttp/connector.py#L892 aiohttp solved this by wrapping the code that generates the ssl context in an lru_cache * compact --- homeassistant/helpers/aiohttp_client.py | 2 +- homeassistant/helpers/httpx_client.py | 7 +++++-- homeassistant/util/ssl.py | 27 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 53c3cc1cf22..78a8051df1c 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -273,7 +273,7 @@ def _async_get_connector( if verify_ssl: ssl_context: bool | SSLContext = ssl_util.get_default_context() else: - ssl_context = False + ssl_context = ssl_util.get_default_no_verify_context() connector = aiohttp.TCPConnector( enable_cleanup_closed=True, diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py index 1e9d2e776c6..44ad81c73e9 100644 --- a/homeassistant/helpers/httpx_client.py +++ b/homeassistant/helpers/httpx_client.py @@ -11,7 +11,7 @@ from typing_extensions import Self from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.loader import bind_hass -from homeassistant.util import ssl as ssl_util +from homeassistant.util.ssl import get_default_context, get_default_no_verify_context from .frame import warn_use @@ -65,8 +65,11 @@ def create_async_httpx_client( This method must be run in the event loop. """ + ssl_context = ( + get_default_context() if verify_ssl else get_default_no_verify_context() + ) client = HassHttpXAsyncClient( - verify=ssl_util.get_default_context() if verify_ssl else False, + verify=ssl_context, headers={USER_AGENT: SERVER_SOFTWARE}, **kwargs, ) diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 9c945ef2759..5b8830e6571 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -1,10 +1,31 @@ """Helper to create SSL contexts.""" +import contextlib from os import environ import ssl import certifi +def create_no_verify_ssl_context() -> ssl.SSLContext: + """Return an SSL context that does not verify the server certificate. + + This is a copy of aiohttp's create_default_context() function, with the + ssl verify turned off. + + https://github.com/aio-libs/aiohttp/blob/33953f110e97eecc707e1402daa8d543f38a189b/aiohttp/connector.py#L911 + """ + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + sslcontext.options |= ssl.OP_NO_SSLv2 + sslcontext.options |= ssl.OP_NO_SSLv3 + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE + with contextlib.suppress(AttributeError): + # This only works for OpenSSL >= 1.0.0 + sslcontext.options |= ssl.OP_NO_COMPRESSION + sslcontext.set_default_verify_paths() + return sslcontext + + def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" @@ -18,6 +39,7 @@ def client_context() -> ssl.SSLContext: # Create this only once and reuse it _DEFAULT_SSL_CONTEXT = client_context() +_DEFAULT_NO_VERIFY_SSL_CONTEXT = create_no_verify_ssl_context() def get_default_context() -> ssl.SSLContext: @@ -25,6 +47,11 @@ def get_default_context() -> ssl.SSLContext: return _DEFAULT_SSL_CONTEXT +def get_default_no_verify_context() -> ssl.SSLContext: + """Return the default SSL context that does not verify the server certificate.""" + return _DEFAULT_NO_VERIFY_SSL_CONTEXT + + def server_context_modern() -> ssl.SSLContext: """Return an SSL context following the Mozilla recommendations.