From d8fa06de67339dd364c6679ff12cfdf91da502cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:01:34 -0500 Subject: [PATCH 01/26] Support multiple sites on the webserver aiohttp already supports multiple TCPSites for a single server This allows Home Assistant to listen on multiple ports with different ssl configuration per port so that devices that need a less secure configuration can be isolated to a single port. The motivation for this is that I did not want to downgrade my entire ssl configuration for a single device that needs a less secure configuration, or to support an ONVIF camera that does not do SSL --- homeassistant/components/http/__init__.py | 250 +++++++++++++--------- 1 file changed, 152 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2d306ba5ee5..76ea244147b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass import datetime from ipaddress import IPv4Network, IPv6Network, ip_network import logging @@ -87,18 +88,38 @@ STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 SAVE_DELAY: Final = 180 +CONF_SERVERS = "servers" + +SERVER_SCHEMA = { + vol.Optional(CONF_SERVER_HOST): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, + vol.Optional(CONF_SSL_KEY): cv.isfile, + vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( + [SSL_INTERMEDIATE, SSL_MODERN] + ), +} + + +def _has_all_unique_ports(servers: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Validate that each http service has a unique port.""" + ports = [list[CONF_SERVER_PORT] for list in servers] + vol.Schema(vol.Unique())(ports) + return servers + + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { - vol.Optional(CONF_SERVER_HOST): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ), - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, + **SERVER_SCHEMA, vol.Optional(CONF_BASE_URL): cv.string, - vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, - vol.Optional(CONF_SSL_KEY): cv.isfile, + vol.Optional(CONF_SERVERS): vol.All( + cv.ensure_list, [vol.Schema(SERVER_SCHEMA)], _has_all_unique_ports + ), vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All( cv.ensure_list, [cv.string] ), @@ -110,13 +131,11 @@ HTTP_SCHEMA: Final = vol.All( CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean, - vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN): vol.In( - [SSL_INTERMEDIATE, SSL_MODERN] - ), } ), ) + CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: HTTP_SCHEMA}, extra=vol.ALLOW_EXTRA) @@ -161,6 +180,33 @@ class ApiConfig: self.use_ssl = use_ssl +@dataclass +class SiteServerConfig: + """Configuration for a single TCPSite.""" + + server_host: list[str] + server_port: int + ssl_certificate: str | None + ssl_peer_certificate: str | None + ssl_key: str | None + ssl_profile: str + ssl_context: ssl.SSLContext | None = None + + +def _create_site_server_config_from_dict( + conf: ConfData | dict[str, Any] +) -> SiteServerConfig: + """Create a SiteServerConfig from a dict.""" + return SiteServerConfig( + server_host=conf[CONF_SERVER_HOST], + server_port=conf[CONF_SERVER_PORT], + ssl_certificate=conf.get(CONF_SSL_CERTIFICATE), + ssl_peer_certificate=conf.get(CONF_SSL_PEER_CERTIFICATE), + ssl_key=conf.get(CONF_SSL_KEY), + ssl_profile=conf[CONF_SSL_PROFILE], + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HTTP API and debug interface.""" conf: ConfData | None = config.get(DOMAIN) @@ -168,27 +214,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if conf is None: conf = cast(ConfData, HTTP_SCHEMA({})) - server_host = conf.get(CONF_SERVER_HOST) - server_port = conf[CONF_SERVER_PORT] - ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) - ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE) - ssl_key = conf.get(CONF_SSL_KEY) + # configuration options that affect all TCPSites cors_origins = conf[CONF_CORS_ORIGINS] use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False) trusted_proxies = conf.get(CONF_TRUSTED_PROXIES) or [] is_ban_enabled = conf[CONF_IP_BAN_ENABLED] login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] - ssl_profile = conf[CONF_SSL_PROFILE] + + site_configs: list[SiteServerConfig] = [] + + if CONF_SERVERS in config[DOMAIN]: + site_configs = [ + _create_site_server_config_from_dict(conf) + for conf in config[DOMAIN][CONF_SERVERS] + ] + else: + site_configs = [_create_site_server_config_from_dict(conf)] server = HomeAssistantHTTP( hass, - server_host=server_host, - server_port=server_port, - ssl_certificate=ssl_certificate, - ssl_peer_certificate=ssl_peer_certificate, - ssl_key=ssl_key, + site_configs=site_configs, trusted_proxies=trusted_proxies, - ssl_profile=ssl_profile, ) await server.async_initialize( cors_origins=cors_origins, @@ -212,16 +258,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_when_setup_or_start(hass, "frontend", start_server) hass.http = server - + primary_server_conf = site_configs[0] local_ip = await async_get_source_ip(hass) - host = local_ip - if server_host is not None: - # Assume the first server host name provided as API host - host = server_host[0] + if primary_server_host := primary_server_conf.server_host: + primary_host = primary_server_host[0] + else: + primary_host = local_ip hass.config.api = ApiConfig( - local_ip, host, server_port, ssl_certificate is not None + local_ip, + primary_host, + primary_server_conf.server_port, + primary_server_conf.ssl_certificate is not None, ) return True @@ -286,13 +335,8 @@ class HomeAssistantHTTP: def __init__( self, hass: HomeAssistant, - ssl_certificate: str | None, - ssl_peer_certificate: str | None, - ssl_key: str | None, - server_host: list[str] | None, - server_port: int, + site_configs: list[SiteServerConfig], trusted_proxies: list[IPv4Network | IPv6Network], - ssl_profile: str, ) -> None: """Initialize the HTTP Home Assistant server.""" self.app = HomeAssistantApplication( @@ -304,16 +348,10 @@ class HomeAssistantHTTP: }, ) self.hass = hass - self.ssl_certificate = ssl_certificate - self.ssl_peer_certificate = ssl_peer_certificate - self.ssl_key = ssl_key - self.server_host = server_host - self.server_port = server_port + self.site_configs = site_configs self.trusted_proxies = trusted_proxies - self.ssl_profile = ssl_profile self.runner: web.AppRunner | None = None - self.site: HomeAssistantTCPSite | None = None - self.context: ssl.SSLContext | None = None + self.sites: list[HomeAssistantTCPSite] = [] async def async_initialize( self, @@ -341,10 +379,8 @@ class HomeAssistantHTTP: setup_cors(self.app, cors_origins) - if self.ssl_certificate: - self.context = await self.hass.async_add_executor_job( - self._create_ssl_context - ) + if any(site.ssl_certificate for site in self.site_configs): + await self.hass.async_add_executor_job(self._create_ssl_contexts) def register_view(self, view: HomeAssistantView | type[HomeAssistantView]) -> None: """Register a view with the WSGI server. @@ -417,53 +453,55 @@ class HomeAssistantHTTP: self.app.router.add_route("GET", url_path, serve_file) ) - def _create_ssl_context(self) -> ssl.SSLContext | None: - context: ssl.SSLContext | None = None - assert self.ssl_certificate is not None - try: - if self.ssl_profile == SSL_INTERMEDIATE: - context = ssl_util.server_context_intermediate() - else: - context = ssl_util.server_context_modern() - context.load_cert_chain(self.ssl_certificate, self.ssl_key) - except OSError as error: - if not self.hass.config.safe_mode: - raise HomeAssistantError( - f"Could not use SSL certificate from {self.ssl_certificate}:" - f" {error}" - ) from error - _LOGGER.error( - "Could not read SSL certificate from %s: %s", - self.ssl_certificate, - error, - ) + def _create_ssl_contexts(self) -> None: + for site in self.site_configs: + context: ssl.SSLContext | None = None + assert site.ssl_certificate is not None try: - context = self._create_emergency_ssl_context() - except OSError as error2: + if site.ssl_profile == SSL_INTERMEDIATE: + context = ssl_util.server_context_intermediate() + else: + context = ssl_util.server_context_modern() + context.load_cert_chain(site.ssl_certificate, site.ssl_key) + except OSError as error: + if not self.hass.config.safe_mode: + raise HomeAssistantError( + f"Could not use SSL certificate from {site.ssl_certificate}:" + f" {error}" + ) from error _LOGGER.error( - "Could not create an emergency self signed ssl certificate: %s", - error2, + "Could not read SSL certificate from %s: %s", + site.ssl_certificate, + error, ) - context = None - else: - _LOGGER.critical( - "Home Assistant is running in safe mode with an emergency self" - " signed ssl certificate because the configured SSL certificate was" - " not usable" - ) - return context + try: + context = self._create_emergency_ssl_context() + except OSError as os_error: + _LOGGER.error( + "Could not create an emergency self signed ssl certificate: %s", + os_error, + ) + continue + else: + _LOGGER.critical( + "Home Assistant is running in safe mode with an emergency self" + " signed ssl certificate because the configured SSL certificate was" + " not usable" + ) + site.ssl_context = context + continue - if self.ssl_peer_certificate: - if context is None: - raise HomeAssistantError( - "Failed to create ssl context, no fallback available because a peer" - " certificate is required." - ) + if site.ssl_peer_certificate: + if context is None: + raise HomeAssistantError( + "Failed to create ssl context, no fallback available because a peer" + " certificate is required." + ) - context.verify_mode = ssl.CERT_REQUIRED - context.load_verify_locations(self.ssl_peer_certificate) + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(site.ssl_peer_certificate) - return context + site.ssl_context = context def _create_emergency_ssl_context(self) -> ssl.SSLContext: """Create an emergency ssl certificate so we can still startup.""" @@ -527,22 +565,38 @@ class HomeAssistantHTTP: ) await self.runner.setup() - self.site = HomeAssistantTCPSite( - self.runner, self.server_host, self.server_port, ssl_context=self.context - ) - try: - await self.site.start() - except OSError as error: - _LOGGER.error( - "Failed to create HTTP server at port %d: %s", self.server_port, error + sites = [ + HomeAssistantTCPSite( + self.runner, + site_config.server_host, + site_config.server_port, + ssl_context=site_config.ssl_context, ) + for site_config in self.site_configs + ] - _LOGGER.info("Now listening on port %d", self.server_port) + results = await asyncio.gather( + *(site.start() for site in sites), return_exceptions=True + ) + + for idx, result in enumerate(results): + site_config = self.site_configs[idx] + + if isinstance(result, Exception): + _LOGGER.error( + "Failed to create HTTP server at port %d: %s", + site_config.server_port, + result, + ) + continue + + self.sites.append(sites[idx]) + _LOGGER.info("Now listening on port %d", site_config.server_port) async def stop(self) -> None: """Stop the aiohttp server.""" - if self.site is not None: - await self.site.stop() + if self.sites: + await asyncio.gather(*[site.stop() for site in self.sites]) if self.runner is not None: await self.runner.cleanup() From cb06b5af786e59ef2bcc32e9de65308d3da0d12c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:06:10 -0500 Subject: [PATCH 02/26] Support multiple sites on the webserver aiohttp already supports multiple TCPSites for a single server This allows Home Assistant to listen on multiple ports with different ssl configuration per port so that devices that need a less secure configuration can be isolated to a single port --- homeassistant/components/http/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 76ea244147b..2d323fa9ec3 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -193,12 +193,10 @@ class SiteServerConfig: ssl_context: ssl.SSLContext | None = None -def _create_site_server_config_from_dict( - conf: ConfData | dict[str, Any] -) -> SiteServerConfig: +def _create_site_server_config_from_dict(conf: ConfData) -> SiteServerConfig: """Create a SiteServerConfig from a dict.""" return SiteServerConfig( - server_host=conf[CONF_SERVER_HOST], + server_host=conf.get(CONF_SERVER_HOST) or [], server_port=conf[CONF_SERVER_PORT], ssl_certificate=conf.get(CONF_SSL_CERTIFICATE), ssl_peer_certificate=conf.get(CONF_SSL_PEER_CERTIFICATE), @@ -223,11 +221,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: site_configs: list[SiteServerConfig] = [] - if CONF_SERVERS in config[DOMAIN]: - site_configs = [ - _create_site_server_config_from_dict(conf) - for conf in config[DOMAIN][CONF_SERVERS] - ] + if server_cfg := conf.get(CONF_SERVERS): + servers = cast(list[ConfData], server_cfg) + site_configs = [_create_site_server_config_from_dict(cfg) for cfg in servers] else: site_configs = [_create_site_server_config_from_dict(conf)] From 5c0614eb1768e9dc4e4e4765597be13b96b25f23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:10:37 -0500 Subject: [PATCH 03/26] fix tests --- tests/components/http/test_init.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 8f9fff79580..92aa3b675f5 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -306,7 +306,7 @@ async def test_emergency_ssl_certificate_when_invalid( in caplog.text ) - assert hass.http.site is not None + assert len(hass.http.sites) == 1 async def test_emergency_ssl_certificate_not_used_when_not_safe_mode( @@ -360,7 +360,7 @@ async def test_emergency_ssl_certificate_when_invalid_get_url_fails( in caplog.text ) - assert hass.http.site is not None + assert len(hass.http.sites) == 1 async def test_invalid_ssl_and_cannot_create_emergency_cert( @@ -391,7 +391,7 @@ async def test_invalid_ssl_and_cannot_create_emergency_cert( assert "Could not create an emergency self signed ssl certificate" in caplog.text assert len(mock_builder.mock_calls) == 1 - assert hass.http.site is not None + assert len(hass.http.sites) == 1 async def test_invalid_ssl_and_cannot_create_emergency_cert_with_ssl_peer_cert( @@ -425,11 +425,12 @@ async def test_invalid_ssl_and_cannot_create_emergency_cert_with_ssl_peer_cert( }, }, ) - is False + is True ) await hass.async_start() await hass.async_block_till_done() assert "Could not create an emergency self signed ssl certificate" in caplog.text + # We should fallback to non-ssl so they can still access the system assert len(mock_builder.mock_calls) == 1 From 00e784d0c45096feb7533e3516a3f9844711bc76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:24:21 -0500 Subject: [PATCH 04/26] adjust --- homeassistant/components/http/__init__.py | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 2d323fa9ec3..4cd9796bdb9 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -91,9 +91,6 @@ SAVE_DELAY: Final = 180 CONF_SERVERS = "servers" SERVER_SCHEMA = { - vol.Optional(CONF_SERVER_HOST): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ), vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, @@ -111,14 +108,34 @@ def _has_all_unique_ports(servers: list[dict[str, Any]]) -> list[dict[str, Any]] return servers +SERVERS_EXCLUSIVE_MESSAGE = ( + 'Configure one server at top level or configure multiple servers under "servers"' +) + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { + vol.Exclusive( + CONF_SERVER_HOST, "servers", msg=SERVERS_EXCLUSIVE_MESSAGE + ): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), **SERVER_SCHEMA, vol.Optional(CONF_BASE_URL): cv.string, - vol.Optional(CONF_SERVERS): vol.All( - cv.ensure_list, [vol.Schema(SERVER_SCHEMA)], _has_all_unique_ports + vol.Exclusive( + CONF_SERVERS, "servers", msg=SERVERS_EXCLUSIVE_MESSAGE + ): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_SERVER_HOST): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ) + } + | SERVER_SCHEMA + ) + ], + _has_all_unique_ports, ), vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All( cv.ensure_list, [cv.string] From 5c3a76053d0ec0ec1973833012a1354ee35aaa56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:28:34 -0500 Subject: [PATCH 05/26] tweak --- homeassistant/components/http/__init__.py | 31 +++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4cd9796bdb9..a312f4409f2 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -90,8 +90,10 @@ SAVE_DELAY: Final = 180 CONF_SERVERS = "servers" -SERVER_SCHEMA = { - vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, +SERVER_SCHEMA_WITHOUT_PORT = { + vol.Optional(CONF_SERVER_HOST): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile, vol.Optional(CONF_SSL_KEY): cv.isfile, @@ -100,6 +102,13 @@ SERVER_SCHEMA = { ), } +OPTIONAL_PORT = {vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port} + +_EXCLUSIVE_PORT_KEY = vol.Exclusive(CONF_SERVER_PORT, "server") +_EXCLUSIVE_PORT_KEY.default = vol.default_factory(SERVER_PORT) + +EXCLUSIVE_PORT = {_EXCLUSIVE_PORT_KEY: cv.port} + def _has_all_unique_ports(servers: list[dict[str, Any]]) -> list[dict[str, Any]]: """Validate that each http service has a unique port.""" @@ -112,29 +121,19 @@ SERVERS_EXCLUSIVE_MESSAGE = ( 'Configure one server at top level or configure multiple servers under "servers"' ) + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), vol.Schema( { - vol.Exclusive( - CONF_SERVER_HOST, "servers", msg=SERVERS_EXCLUSIVE_MESSAGE - ): vol.All(cv.ensure_list, vol.Length(min=1), [cv.string]), - **SERVER_SCHEMA, + **SERVER_SCHEMA_WITHOUT_PORT, + **EXCLUSIVE_PORT, vol.Optional(CONF_BASE_URL): cv.string, vol.Exclusive( CONF_SERVERS, "servers", msg=SERVERS_EXCLUSIVE_MESSAGE ): vol.All( cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_SERVER_HOST): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ) - } - | SERVER_SCHEMA - ) - ], + [vol.Schema({**SERVER_SCHEMA_WITHOUT_PORT, **OPTIONAL_PORT})], _has_all_unique_ports, ), vol.Optional(CONF_CORS_ORIGINS, default=DEFAULT_CORS): vol.All( From 0652dda10694a40ad201856034dcb8792c44e2cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:34:04 -0500 Subject: [PATCH 06/26] coverage --- homeassistant/components/http/__init__.py | 3 +- tests/components/http/test_init.py | 86 +++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a312f4409f2..eccd52219fe 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -468,7 +468,8 @@ class HomeAssistantHTTP: def _create_ssl_contexts(self) -> None: for site in self.site_configs: context: ssl.SSLContext | None = None - assert site.ssl_certificate is not None + if site.ssl_certificate is None: + continue try: if site.ssl_profile == SSL_INTERMEDIATE: context = ssl_util.server_context_intermediate() diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 92aa3b675f5..a689c4ed25d 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -519,3 +519,89 @@ async def test_hass_access_logger_at_info_level( test_logger.setLevel(logging.WARNING) logger.log(mock_request, response, time.time()) assert "42" not in caplog.text + + +async def test_multiple_ssl_profiles(hass: HomeAssistant, tmp_path: Path) -> None: + """Test different ssl profiles on different ports.""" + + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmp_path + ) + + with patch("ssl.SSLContext.load_cert_chain"), patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_server_context_modern, patch( + "homeassistant.util.ssl.server_context_intermediate", + side_effect=server_context_intermediate, + ) as mock_server_context_intermediate: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "servers": [ + { + "ssl_certificate": cert_path, + "ssl_key": key_path, + }, + { + "ssl_profile": "intermediate", + "ssl_certificate": cert_path, + "ssl_key": key_path, + "server_port": "8124", + }, + ] + } + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_server_context_modern.mock_calls) == 1 + assert len(mock_server_context_intermediate.mock_calls) == 1 + assert len(hass.http.sites) == 2 + + +async def test_ssl_for_external_no_ssl_internal( + hass: HomeAssistant, tmp_path: Path +) -> None: + """Test ssl for external and no ssl for internal.""" + + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmp_path + ) + + with patch("ssl.SSLContext.load_cert_chain"), patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ) as mock_server_context_modern: + assert ( + await async_setup_component( + hass, + "http", + { + "http": { + "servers": [ + { + "ssl_certificate": cert_path, + "ssl_key": key_path, + }, + { + "server_port": "8124", + }, + ] + } + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + + assert len(mock_server_context_modern.mock_calls) == 1 + + assert len(hass.http.sites) == 2 From 64424f672c83cc748d40969f8e22e6b852305115 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:43:41 -0500 Subject: [PATCH 07/26] back compat --- homeassistant/components/http/__init__.py | 8 ++++++++ homeassistant/components/zeroconf/__init__.py | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index eccd52219fe..b17dd182f15 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -364,6 +364,14 @@ class HomeAssistantHTTP: self.trusted_proxies = trusted_proxies self.runner: web.AppRunner | None = None self.sites: list[HomeAssistantTCPSite] = [] + self.ssl_certificate: str | None = None + self.server_port: int | None = None + # For backwards compat + for site in site_configs: + if self.ssl_certificate is None and site.ssl_certificate: + self.ssl_certificate = site.ssl_certificate + if self.server_port is None and site.server_port: + self.server_port = site.server_port async def async_initialize( self, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 9d7dd73be01..f595c136772 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -15,6 +15,7 @@ import sys from typing import Any, Final, cast import voluptuous as vol +from yarl import URL from zeroconf import ( BadTypeInNameException, InterfaceChoice, @@ -267,14 +268,16 @@ async def _async_register_hass_zc_service( } # Get instance URL's + external_url: str | None = None with suppress(NoURLAvailableError): - params["external_url"] = get_url(hass, allow_internal=False) + params["external_url"] = external_url = get_url(hass, allow_internal=False) + internal_url: str | None = None with suppress(NoURLAvailableError): - params["internal_url"] = get_url(hass, allow_external=False) + params["internal_url"] = internal_url = get_url(hass, allow_external=False) # Set old base URL based on external or internal - params["base_url"] = params["external_url"] or params["internal_url"] + params["base_url"] = external_url or internal_url adapters = await network.async_get_adapters(hass) @@ -289,12 +292,19 @@ async def _async_register_hass_zc_service( _suppress_invalid_properties(params) + server_port: int | None = None + with contextlib.suppress(ValueError): + if (preferred_url := (internal_url or external_url)) and ( + url_host := URL(preferred_url).host + ): + server_port = int(url_host.partition(":")[1]) + info = AsyncServiceInfo( ZEROCONF_TYPE, name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", addresses=address_list, - port=hass.http.server_port, + port=server_port or hass.http.server_port, properties=params, ) From 85dd053be8284484a379848a5bd191f30fc71022 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:44:04 -0500 Subject: [PATCH 08/26] back compat --- homeassistant/components/zeroconf/__init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index f595c136772..9d7dd73be01 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -15,7 +15,6 @@ import sys from typing import Any, Final, cast import voluptuous as vol -from yarl import URL from zeroconf import ( BadTypeInNameException, InterfaceChoice, @@ -268,16 +267,14 @@ async def _async_register_hass_zc_service( } # Get instance URL's - external_url: str | None = None with suppress(NoURLAvailableError): - params["external_url"] = external_url = get_url(hass, allow_internal=False) + params["external_url"] = get_url(hass, allow_internal=False) - internal_url: str | None = None with suppress(NoURLAvailableError): - params["internal_url"] = internal_url = get_url(hass, allow_external=False) + params["internal_url"] = get_url(hass, allow_external=False) # Set old base URL based on external or internal - params["base_url"] = external_url or internal_url + params["base_url"] = params["external_url"] or params["internal_url"] adapters = await network.async_get_adapters(hass) @@ -292,19 +289,12 @@ async def _async_register_hass_zc_service( _suppress_invalid_properties(params) - server_port: int | None = None - with contextlib.suppress(ValueError): - if (preferred_url := (internal_url or external_url)) and ( - url_host := URL(preferred_url).host - ): - server_port = int(url_host.partition(":")[1]) - info = AsyncServiceInfo( ZEROCONF_TYPE, name=f"{valid_location_name}.{ZEROCONF_TYPE}", server=f"{uuid}.local.", addresses=address_list, - port=server_port or hass.http.server_port, + port=hass.http.server_port, properties=params, ) From 11a2e30d224991e7d36b6d49f2cff3d27cc5936e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:58:40 -0500 Subject: [PATCH 09/26] debug --- homeassistant/components/http/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index b17dd182f15..d39735e932e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -605,14 +605,19 @@ class HomeAssistantHTTP: if isinstance(result, Exception): _LOGGER.error( - "Failed to create HTTP server at port %d: %s", + "Failed to create HTTP server at port %s:%d: %s", + site_config.server_host, site_config.server_port, result, ) continue self.sites.append(sites[idx]) - _LOGGER.info("Now listening on port %d", site_config.server_port) + _LOGGER.info( + "Now listening on %s:%d", + site_config.server_host, + site_config.server_port, + ) async def stop(self) -> None: """Stop the aiohttp server.""" From 2820a63f9f77b5192beeacc4e9014c80be4d87ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 13:59:05 -0500 Subject: [PATCH 10/26] debug --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index d39735e932e..1ce31602bd7 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -622,7 +622,7 @@ class HomeAssistantHTTP: async def stop(self) -> None: """Stop the aiohttp server.""" if self.sites: - await asyncio.gather(*[site.stop() for site in self.sites]) + await asyncio.gather(*(site.stop() for site in self.sites)) if self.runner is not None: await self.runner.cleanup() From c34aa0e23ba912a57b1a5ffd3435ca1154c9c7c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 14:12:42 -0500 Subject: [PATCH 11/26] fix test --- homeassistant/components/http/__init__.py | 4 +-- tests/components/http/test_init.py | 44 ++++++++++++----------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1ce31602bd7..97337642339 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -200,7 +200,7 @@ class ApiConfig: class SiteServerConfig: """Configuration for a single TCPSite.""" - server_host: list[str] + server_host: list[str] | None server_port: int ssl_certificate: str | None ssl_peer_certificate: str | None @@ -212,7 +212,7 @@ class SiteServerConfig: def _create_site_server_config_from_dict(conf: ConfData) -> SiteServerConfig: """Create a SiteServerConfig from a dict.""" return SiteServerConfig( - server_host=conf.get(CONF_SERVER_HOST) or [], + server_host=conf.get(CONF_SERVER_HOST), server_port=conf[CONF_SERVER_PORT], ssl_certificate=conf.get(CONF_SSL_CERTIFICATE), ssl_peer_certificate=conf.get(CONF_SSL_PEER_CERTIFICATE), diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index a689c4ed25d..aca7d0015fc 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -288,23 +288,23 @@ async def test_emergency_ssl_certificate_when_invalid( ) hass.config.safe_mode = True - assert ( - await async_setup_component( - hass, - "http", - { - "http": {"ssl_certificate": cert_path, "ssl_key": key_path}, - }, + with patch.object(hass.loop, "create_server"): + assert ( + await async_setup_component( + hass, + "http", + { + "http": {"ssl_certificate": cert_path, "ssl_key": key_path}, + }, + ) + is True + ) + await hass.async_start() + await hass.async_block_till_done() + assert ( + "Home Assistant is running in safe mode with an emergency self signed ssl certificate because the configured SSL certificate was not usable" + in caplog.text ) - is True - ) - - await hass.async_start() - await hass.async_block_till_done() - assert ( - "Home Assistant is running in safe mode with an emergency self signed ssl certificate because the configured SSL certificate was not usable" - in caplog.text - ) assert len(hass.http.sites) == 1 @@ -340,7 +340,7 @@ async def test_emergency_ssl_certificate_when_invalid_get_url_fails( with patch( "homeassistant.components.http.get_url", side_effect=NoURLAvailableError - ) as mock_get_url: + ) as mock_get_url, patch.object(hass.loop, "create_server"): assert ( await async_setup_component( hass, @@ -375,7 +375,7 @@ async def test_invalid_ssl_and_cannot_create_emergency_cert( with patch( "homeassistant.components.http.x509.CertificateBuilder", side_effect=OSError - ) as mock_builder: + ) as mock_builder, patch.object(hass.loop, "create_server"): assert ( await async_setup_component( hass, @@ -412,7 +412,7 @@ async def test_invalid_ssl_and_cannot_create_emergency_cert_with_ssl_peer_cert( with patch( "homeassistant.components.http.x509.CertificateBuilder", side_effect=OSError - ) as mock_builder: + ) as mock_builder, patch.object(hass.loop, "create_server"): assert ( await async_setup_component( hass, @@ -534,7 +534,9 @@ async def test_multiple_ssl_profiles(hass: HomeAssistant, tmp_path: Path) -> Non ) as mock_server_context_modern, patch( "homeassistant.util.ssl.server_context_intermediate", side_effect=server_context_intermediate, - ) as mock_server_context_intermediate: + ) as mock_server_context_intermediate, patch.object( + hass.loop, "create_server" + ): assert ( await async_setup_component( hass, @@ -578,7 +580,7 @@ async def test_ssl_for_external_no_ssl_internal( with patch("ssl.SSLContext.load_cert_chain"), patch( "homeassistant.util.ssl.server_context_modern", side_effect=server_context_modern, - ) as mock_server_context_modern: + ) as mock_server_context_modern, patch.object(hass.loop, "create_server"): assert ( await async_setup_component( hass, From 18ef390dcaddb09eada244c47c9979933dfe40f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 14:14:03 -0500 Subject: [PATCH 12/26] make sure we actually get multiple servers --- tests/components/http/test_init.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index aca7d0015fc..65b0ef074a2 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -536,7 +536,7 @@ async def test_multiple_ssl_profiles(hass: HomeAssistant, tmp_path: Path) -> Non side_effect=server_context_intermediate, ) as mock_server_context_intermediate, patch.object( hass.loop, "create_server" - ): + ) as mock_create_server: assert ( await async_setup_component( hass, @@ -566,6 +566,7 @@ async def test_multiple_ssl_profiles(hass: HomeAssistant, tmp_path: Path) -> Non assert len(mock_server_context_modern.mock_calls) == 1 assert len(mock_server_context_intermediate.mock_calls) == 1 assert len(hass.http.sites) == 2 + assert len(mock_create_server.mock_calls) == 2 async def test_ssl_for_external_no_ssl_internal( @@ -580,7 +581,9 @@ async def test_ssl_for_external_no_ssl_internal( with patch("ssl.SSLContext.load_cert_chain"), patch( "homeassistant.util.ssl.server_context_modern", side_effect=server_context_modern, - ) as mock_server_context_modern, patch.object(hass.loop, "create_server"): + ) as mock_server_context_modern, patch.object( + hass.loop, "create_server" + ) as mock_create_server: assert ( await async_setup_component( hass, @@ -605,5 +608,5 @@ async def test_ssl_for_external_no_ssl_internal( await hass.async_block_till_done() assert len(mock_server_context_modern.mock_calls) == 1 - + assert len(mock_create_server.mock_calls) == 2 assert len(hass.http.sites) == 2 From e7c95ee088652e184d5e1e3af9310ecf272d7712 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 15:24:55 -0500 Subject: [PATCH 13/26] fixes --- homeassistant/components/analytics/analytics.py | 4 +++- homeassistant/components/http/__init__.py | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 9419f00e41e..a32043b4412 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -247,7 +247,9 @@ class Analytics: ) if self.preferences.get(ATTR_USAGE, False): - payload[ATTR_CERTIFICATE] = self.hass.http.ssl_certificate is not None + payload[ATTR_CERTIFICATE] = ( + self.hass.config.api and self.hass.config.api.use_ssl + ) payload[ATTR_INTEGRATIONS] = integrations payload[ATTR_CUSTOM_INTEGRATIONS] = custom_integrations if supervisor_info is not None: diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 97337642339..56dcd68b9b1 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -282,7 +282,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, primary_host, primary_server_conf.server_port, - primary_server_conf.ssl_certificate is not None, + all(site_config.ssl_certificate is not None for site_config in site_configs), ) return True @@ -364,14 +364,8 @@ class HomeAssistantHTTP: self.trusted_proxies = trusted_proxies self.runner: web.AppRunner | None = None self.sites: list[HomeAssistantTCPSite] = [] - self.ssl_certificate: str | None = None - self.server_port: int | None = None # For backwards compat - for site in site_configs: - if self.ssl_certificate is None and site.ssl_certificate: - self.ssl_certificate = site.ssl_certificate - if self.server_port is None and site.server_port: - self.server_port = site.server_port + self.server_port: int = site_configs[0].server_port async def async_initialize( self, From 484c9d4d1f1d57eada0a0fa4ade1b1ed8fdb842f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 15:26:59 -0500 Subject: [PATCH 14/26] fix --- tests/components/analytics/test_analytics.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index 412553e81cc..36ca469be96 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -199,7 +199,7 @@ async def test_send_usage( """Test send usage preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate=None) + hass.config.api = Mock(use_ssl=False) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] @@ -222,7 +222,7 @@ async def test_send_usage_with_supervisor( """Test send usage with supervisor preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate=None) + hass.config.api = Mock(use_ssl=False) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] assert analytics.preferences[ATTR_USAGE] @@ -417,7 +417,7 @@ async def test_custom_integrations( """Test sending custom integrations.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate=None) + hass.config.api = Mock(use_ssl=False) assert await async_setup_component(hass, "test_package", {"test_package": {}}) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -491,7 +491,7 @@ async def test_send_with_no_energy( """Test send base preferences are defined.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate=None) + hass.config.api = Mock(use_ssl=False) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) @@ -565,7 +565,8 @@ async def test_send_usage_with_certificate( """Test send usage preferences with certificate.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate="/some/path/to/cert.pem") + + hass.config.api = Mock(use_ssl=True) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) assert analytics.preferences[ATTR_BASE] @@ -586,7 +587,7 @@ async def test_send_with_recorder( """Test recorder information.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) - hass.http = Mock(ssl_certificate="/some/path/to/cert.pem") + hass.config.api = Mock(use_ssl=True) await analytics.save_preferences({ATTR_BASE: True, ATTR_USAGE: True}) From 10bf6db3a2bbe1d8497797744de7650499a26fd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 15:29:21 -0500 Subject: [PATCH 15/26] back compat --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 56dcd68b9b1..1385437b466 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -282,7 +282,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, primary_host, primary_server_conf.server_port, - all(site_config.ssl_certificate is not None for site_config in site_configs), + primary_server_conf.ssl_certificate is not None, ) return True From 775dcebeb9f9b3d7e281ae489a0f9ab090e3eca6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 15:41:45 -0500 Subject: [PATCH 16/26] Revert "back compat" This reverts commit 10bf6db3a2bbe1d8497797744de7650499a26fd6. --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1385437b466..56dcd68b9b1 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -282,7 +282,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, primary_host, primary_server_conf.server_port, - primary_server_conf.ssl_certificate is not None, + all(site_config.ssl_certificate is not None for site_config in site_configs), ) return True From 65ddb464c3def77767bbc39e0932318f2a02d3e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 15:47:00 -0500 Subject: [PATCH 17/26] back compat --- tests/components/http/test_init.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 65b0ef074a2..5f9f3a047df 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -567,6 +567,9 @@ async def test_multiple_ssl_profiles(hass: HomeAssistant, tmp_path: Path) -> Non assert len(mock_server_context_intermediate.mock_calls) == 1 assert len(hass.http.sites) == 2 assert len(mock_create_server.mock_calls) == 2 + # If all sites are using ssl use_ssl should be True + # for backwards compatibility + assert hass.config.api.use_ssl is True async def test_ssl_for_external_no_ssl_internal( @@ -610,3 +613,6 @@ async def test_ssl_for_external_no_ssl_internal( assert len(mock_server_context_modern.mock_calls) == 1 assert len(mock_create_server.mock_calls) == 2 assert len(hass.http.sites) == 2 + # If there is at least one site not using ssl, use_ssl + # should be False for backwards compatibility + assert hass.config.api.use_ssl is False From b5220c41494ccbdea62b64238d9158b405bbf3e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 16:33:43 -0500 Subject: [PATCH 18/26] dep --- homeassistant/components/http/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 56dcd68b9b1..f193ebcf4da 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -124,6 +124,12 @@ SERVERS_EXCLUSIVE_MESSAGE = ( HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), + cv.deprecated(CONF_SERVER_HOST), + cv.deprecated(CONF_SERVER_PORT), + cv.deprecated(CONF_SSL_CERTIFICATE), + cv.deprecated(CONF_SSL_PEER_CERTIFICATE), + cv.deprecated(CONF_SSL_KEY), + cv.deprecated(CONF_SSL_PROFILE), vol.Schema( { **SERVER_SCHEMA_WITHOUT_PORT, From 82a4036965bad527742d5aac632858ce12e427e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 16:40:18 -0500 Subject: [PATCH 19/26] moved --- homeassistant/components/http/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index f193ebcf4da..ab6ced4b870 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -124,12 +124,14 @@ SERVERS_EXCLUSIVE_MESSAGE = ( HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), - cv.deprecated(CONF_SERVER_HOST), - cv.deprecated(CONF_SERVER_PORT), - cv.deprecated(CONF_SSL_CERTIFICATE), - cv.deprecated(CONF_SSL_PEER_CERTIFICATE), - cv.deprecated(CONF_SSL_KEY), - cv.deprecated(CONF_SSL_PROFILE), + cv.deprecated(CONF_SERVER_HOST, replacement_key="servers[0].server_host"), + cv.deprecated(CONF_SERVER_PORT, replacement_key="servers[0].server_port"), + cv.deprecated(CONF_SSL_CERTIFICATE, replacement_key="servers[0].ssl_certificate"), + cv.deprecated( + CONF_SSL_PEER_CERTIFICATE, replacement_key="servers[0].ssl_peer_certificate" + ), + cv.deprecated(CONF_SSL_KEY, replacement_key="servers[0].ssl_key"), + cv.deprecated(CONF_SSL_PROFILE, replacement_key="servers[0].ssl_profile"), vol.Schema( { **SERVER_SCHEMA_WITHOUT_PORT, From 240f0cc4909c466a272ab5e9d15e4f2118b31662 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 16:43:36 -0500 Subject: [PATCH 20/26] moved --- homeassistant/components/http/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index ab6ced4b870..4deeee476ee 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -89,6 +89,7 @@ STORAGE_VERSION: Final = 1 SAVE_DELAY: Final = 180 CONF_SERVERS = "servers" +SERVERS_GROUP = "servers" SERVER_SCHEMA_WITHOUT_PORT = { vol.Optional(CONF_SERVER_HOST): vol.All( @@ -104,7 +105,7 @@ SERVER_SCHEMA_WITHOUT_PORT = { OPTIONAL_PORT = {vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port} -_EXCLUSIVE_PORT_KEY = vol.Exclusive(CONF_SERVER_PORT, "server") +_EXCLUSIVE_PORT_KEY = vol.Exclusive(CONF_SERVER_PORT, SERVERS_GROUP) _EXCLUSIVE_PORT_KEY.default = vol.default_factory(SERVER_PORT) EXCLUSIVE_PORT = {_EXCLUSIVE_PORT_KEY: cv.port} @@ -138,7 +139,7 @@ HTTP_SCHEMA: Final = vol.All( **EXCLUSIVE_PORT, vol.Optional(CONF_BASE_URL): cv.string, vol.Exclusive( - CONF_SERVERS, "servers", msg=SERVERS_EXCLUSIVE_MESSAGE + CONF_SERVERS, SERVERS_GROUP, msg=SERVERS_EXCLUSIVE_MESSAGE ): vol.All( cv.ensure_list, [vol.Schema({**SERVER_SCHEMA_WITHOUT_PORT, **OPTIONAL_PORT})], From 2a8a1bdc62496fc44127f22af7a25dcbe2a9cc0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 16:53:36 -0500 Subject: [PATCH 21/26] nicer message --- homeassistant/components/http/__init__.py | 41 +++++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4deeee476ee..1577edee50d 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Callable +import contextlib from dataclasses import dataclass import datetime from ipaddress import IPv4Network, IPv6Network, ip_network @@ -123,16 +125,41 @@ SERVERS_EXCLUSIVE_MESSAGE = ( ) +def _relocated_with_message(key: str, new_location: str) -> Callable[[dict], dict]: + """Log key as relocated with a message.""" + + def validator(config: dict) -> dict: + """Check if key is in config and log the new location.""" + near = "" + with contextlib.suppress(AttributeError): + near = ( + f"near {config.__config_file__}" # type: ignore[attr-defined] + f":{config.__line__}" + ) + if key in config: + _LOGGER.warning( + "The '%s' option %s has moved to '%s', please update your configuration.", + key, + near, + new_location, + ) + return config + + return validator + + HTTP_SCHEMA: Final = vol.All( cv.deprecated(CONF_BASE_URL), - cv.deprecated(CONF_SERVER_HOST, replacement_key="servers[0].server_host"), - cv.deprecated(CONF_SERVER_PORT, replacement_key="servers[0].server_port"), - cv.deprecated(CONF_SSL_CERTIFICATE, replacement_key="servers[0].ssl_certificate"), - cv.deprecated( - CONF_SSL_PEER_CERTIFICATE, replacement_key="servers[0].ssl_peer_certificate" + _relocated_with_message(CONF_SERVER_HOST, new_location="servers[0].server_host"), + _relocated_with_message(CONF_SERVER_PORT, new_location="servers[0].server_port"), + _relocated_with_message( + CONF_SSL_CERTIFICATE, new_location="servers[0].ssl_certificate" ), - cv.deprecated(CONF_SSL_KEY, replacement_key="servers[0].ssl_key"), - cv.deprecated(CONF_SSL_PROFILE, replacement_key="servers[0].ssl_profile"), + _relocated_with_message( + CONF_SSL_PEER_CERTIFICATE, new_location="servers[0].ssl_peer_certificate" + ), + _relocated_with_message(CONF_SSL_KEY, new_location="servers[0].ssl_key"), + _relocated_with_message(CONF_SSL_PROFILE, new_location="servers[0].ssl_profile"), vol.Schema( { **SERVER_SCHEMA_WITHOUT_PORT, From 84aeaf79600ed223ffdcfc1f582b17331851a0f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 May 2023 17:08:25 -0500 Subject: [PATCH 22/26] lint --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1577edee50d..99b2e2792b8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -138,7 +138,7 @@ def _relocated_with_message(key: str, new_location: str) -> Callable[[dict], dic ) if key in config: _LOGGER.warning( - "The '%s' option %s has moved to '%s', please update your configuration.", + "The '%s' option %s has moved to '%s', please update your configuration", key, near, new_location, From a3cf66c98c8cde2bb81c0875743e43d27c4ed0f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 May 2023 10:22:16 -0500 Subject: [PATCH 23/26] Update homeassistant/components/http/__init__.py Co-authored-by: Jan Bouwhuis --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 99b2e2792b8..1c0ae834c61 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -128,7 +128,7 @@ SERVERS_EXCLUSIVE_MESSAGE = ( def _relocated_with_message(key: str, new_location: str) -> Callable[[dict], dict]: """Log key as relocated with a message.""" - def validator(config: dict) -> dict: + def validator(config: ConfigType) -> ConfigType: """Check if key is in config and log the new location.""" near = "" with contextlib.suppress(AttributeError): From bf18897b0f18e537c759e2f76666a71aa4520dab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 May 2023 10:22:22 -0500 Subject: [PATCH 24/26] Update homeassistant/components/http/__init__.py Co-authored-by: Jan Bouwhuis --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1c0ae834c61..c53db3aded8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -125,7 +125,7 @@ SERVERS_EXCLUSIVE_MESSAGE = ( ) -def _relocated_with_message(key: str, new_location: str) -> Callable[[dict], dict]: +def _relocated_with_message(key: str, new_location: str) -> Callable[[ConfigType], ConfigType]: """Log key as relocated with a message.""" def validator(config: ConfigType) -> ConfigType: From d5c88dd4f52a944643cb7848fcf753eec141e21f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 May 2023 10:22:27 -0500 Subject: [PATCH 25/26] Update homeassistant/components/http/__init__.py Co-authored-by: Jan Bouwhuis --- homeassistant/components/http/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c53db3aded8..c425b680260 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -113,7 +113,7 @@ _EXCLUSIVE_PORT_KEY.default = vol.default_factory(SERVER_PORT) EXCLUSIVE_PORT = {_EXCLUSIVE_PORT_KEY: cv.port} -def _has_all_unique_ports(servers: list[dict[str, Any]]) -> list[dict[str, Any]]: +def _has_all_unique_ports(servers: list[ConfigType]) -> list[ConfigType]: """Validate that each http service has a unique port.""" ports = [list[CONF_SERVER_PORT] for list in servers] vol.Schema(vol.Unique())(ports) From 74c518ce79d0711b507056bf0b8f487e236f4500 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 May 2023 14:16:55 -0500 Subject: [PATCH 26/26] lint --- homeassistant/components/http/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index c425b680260..72f4400f11c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -125,7 +125,9 @@ SERVERS_EXCLUSIVE_MESSAGE = ( ) -def _relocated_with_message(key: str, new_location: str) -> Callable[[ConfigType], ConfigType]: +def _relocated_with_message( + key: str, new_location: str +) -> Callable[[ConfigType], ConfigType]: """Log key as relocated with a message.""" def validator(config: ConfigType) -> ConfigType: