From 2fa711378799efadcaeb2dd0a687aa8003e6f530 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 24 Sep 2024 15:35:10 +0200 Subject: [PATCH] Raise issue if SSL is set but no external URL configured (#121768) * Raise issue if SSL is set but no external URL configured * Add cloud * Add cloud * Fix strings * Attempt * Fix * Fix * Move strings * Fixes * fix * Fix * Fix * Fix * Break tests * Fix tests --- homeassistant/components/http/__init__.py | 34 +++- homeassistant/components/http/strings.json | 8 + tests/components/http/test_init.py | 151 +++++++++++++++++- .../components/logbook/test_websocket_api.py | 4 + 4 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/http/strings.json diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5b68f91e494..a8721720dfb 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -30,10 +30,14 @@ import voluptuous as vol from yarl import URL from homeassistant.components.network import async_get_source_ip -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, SERVER_PORT +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + SERVER_PORT, +) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import frame, storage +from homeassistant.helpers import frame, issue_registry as ir, storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.http import ( KEY_ALLOW_CONFIGURED_CORS, @@ -264,6 +268,32 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: local_ip, host, server_port, ssl_certificate is not None ) + @callback + def _async_check_ssl_issue(_: Event) -> None: + if ( + ssl_certificate is not None + and (hass.config.external_url or hass.config.internal_url) is None + ): + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.cloud import ( + CloudNotAvailable, + async_remote_ui_url, + ) + + try: + async_remote_ui_url(hass) + except CloudNotAvailable: + ir.async_create_issue( + hass, + DOMAIN, + "ssl_configured_without_configured_urls", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="ssl_configured_without_configured_urls", + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_check_ssl_issue) + return True diff --git a/homeassistant/components/http/strings.json b/homeassistant/components/http/strings.json new file mode 100644 index 00000000000..5dbd8faec20 --- /dev/null +++ b/homeassistant/components/http/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "ssl_configured_without_configured_urls": { + "title": "SSL is configured without an external URL or internal URL", + "description": "Home Assistant detected that SSL has been set up on your instance, however, no custom external internet URL has been set.\n\nThis may result in unexpected behavior. Text-to-speech may fail, and integrations may not be able to connect back to your instance correctly.\n\nTo address this issue, go to Settings > System > Network; under the \"Home Assistant URL\" section, configure your new \"Internet\" and \"Local network\" addresses that match your new SSL configuration." + } + } +} diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2895209b5f9..4d96f2267fa 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -12,8 +12,10 @@ from unittest.mock import Mock, patch import pytest from homeassistant.auth.providers.homeassistant import HassAuthProvider -from homeassistant.components import http +from homeassistant.components import cloud, http +from homeassistant.components.cloud import CloudNotAvailable from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.http import KEY_HASS from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component @@ -545,3 +547,150 @@ async def test_register_static_paths( "event loop, instead call " "`await hass.http.async_register_static_paths" ) in caplog.text + + +async def test_ssl_issue_if_no_urls_configured( + hass: HomeAssistant, + tmp_path: Path, + issue_registry: ir.IssueRegistry, +) -> None: + """Test raising SSL issue if no external or internal URL is configured.""" + + assert hass.config.external_url is None + assert hass.config.internal_url is None + + 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, + ), + ): + assert await async_setup_component( + hass, + "http", + {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}}, + ) + await hass.async_start() + await hass.async_block_till_done() + + assert ("http", "ssl_configured_without_configured_urls") in issue_registry.issues + + +async def test_ssl_issue_if_using_cloud( + hass: HomeAssistant, + tmp_path: Path, + issue_registry: ir.IssueRegistry, +) -> None: + """Test raising no SSL issue if not right configured but using cloud.""" + assert hass.config.external_url is None + assert hass.config.internal_url is None + + 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.object(cloud, "async_remote_ui_url", return_value="https://example.com"), + patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ), + ): + assert await async_setup_component( + hass, + "http", + {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}}, + ) + await hass.async_start() + await hass.async_block_till_done() + + assert ( + "http", + "ssl_configured_without_configured_urls", + ) not in issue_registry.issues + + +async def test_ssl_issue_if_not_connected_to_cloud( + hass: HomeAssistant, + tmp_path: Path, + issue_registry: ir.IssueRegistry, +) -> None: + """Test raising no SSL issue if not right configured and not connected to cloud.""" + assert hass.config.external_url is None + assert hass.config.internal_url is None + + 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, + ), + patch( + "homeassistant.components.cloud.async_remote_ui_url", + side_effect=CloudNotAvailable, + ), + ): + assert await async_setup_component( + hass, + "http", + {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}}, + ) + await hass.async_start() + await hass.async_block_till_done() + + assert ("http", "ssl_configured_without_configured_urls") in issue_registry.issues + + +@pytest.mark.parametrize( + ("external_url", "internal_url"), + [ + ("https://example.com", "https://example.local"), + (None, "http://example.local"), + ("https://example.com", None), + ], +) +async def test_ssl_issue_urls_configured( + hass: HomeAssistant, + tmp_path: Path, + issue_registry: ir.IssueRegistry, + external_url: str | None, + internal_url: str | None, +) -> None: + """Test raising SSL issue if no external or internal URL is configured.""" + + cert_path, key_path, _ = await hass.async_add_executor_job( + _setup_empty_ssl_pem_files, tmp_path + ) + + hass.config.external_url = external_url + hass.config.internal_url = internal_url + + with ( + patch("ssl.SSLContext.load_cert_chain"), + patch( + "homeassistant.util.ssl.server_context_modern", + side_effect=server_context_modern, + ), + ): + assert await async_setup_component( + hass, + "http", + {"http": {"ssl_certificate": cert_path, "ssl_key": key_path}}, + ) + await hass.async_start() + await hass.async_block_till_done() + + assert ( + "http", + "ssl_configured_without_configured_urls", + ) not in issue_registry.issues diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index e5649564f94..2a97556f5ad 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1181,6 +1181,10 @@ async def test_subscribe_unsubscribe_logbook_stream( await async_wait_recording_done(hass) websocket_client = await hass_ws_client() init_listeners = hass.bus.async_listeners() + init_listeners = { + **init_listeners, + EVENT_HOMEASSISTANT_START: init_listeners[EVENT_HOMEASSISTANT_START] - 1, + } await websocket_client.send_json( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} )