From db64a9ebfa5fe90ab98ae3067ca38a3a9bd16d24 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 11 Sep 2020 13:00:00 +0200 Subject: [PATCH] Accept known hosts for get_url for OAuth (#39936) --- homeassistant/helpers/network.py | 32 +++++++++++++++++++ tests/helpers/test_network.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index d40fd9fad2b..8bdfc286c1a 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -75,6 +75,38 @@ def get_url( except NoURLAvailableError: pass + # For current request, we accept loopback interfaces (e.g., 127.0.0.1), + # the Supervisor hostname and localhost transparently + request_host = _get_request_host() + if ( + require_current_request + and request_host is not None + and hass.config.api is not None + ): + scheme = "https" if hass.config.api.use_ssl else "http" + current_url = yarl.URL.build( + scheme=scheme, host=request_host, port=hass.config.api.port + ) + + known_hostname = None + if hass.components.hassio.is_hassio(): + host_info = hass.components.hassio.get_host_info() + known_hostname = f"{host_info['hostname']}.local" + + if ( + ( + ( + allow_ip + and is_ip_address(request_host) + and is_loopback(ip_address(request_host)) + ) + or request_host in ["localhost", known_hostname] + ) + and (not require_ssl or current_url.scheme == "https") + and (not require_standard_port or current_url.is_default_port()) + ): + return normalize_url(str(current_url)) + # We have to be honest now, we have no viable option available raise NoURLAvailableError diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index f51ee2090dc..ed97b3e3757 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -15,6 +15,7 @@ from homeassistant.helpers.network import ( ) from tests.async_mock import Mock, patch +from tests.common import mock_component async def test_get_url_internal(hass: HomeAssistant): @@ -799,3 +800,55 @@ async def test_get_external_url_with_base_url_fallback(hass: HomeAssistant): assert _get_external_url(hass, allow_ip=False) == "https://example.com" assert _get_external_url(hass, require_standard_port=True) == "https://example.com" assert _get_external_url(hass, require_ssl=True) == "https://example.com" + + +async def test_get_current_request_url_with_known_host( + hass: HomeAssistant, current_request +): + """Test getting current request URL with known hosts addresses.""" + hass.config.api = Mock( + use_ssl=False, port=8123, local_ip="127.0.0.1", deprecated_base_url=None + ) + assert hass.config.internal_url is None + + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True) + + # Ensure we accept localhost + with patch( + "homeassistant.helpers.network._get_request_host", return_value="localhost" + ): + assert get_url(hass, require_current_request=True) == "http://localhost:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_ssl=True) + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, require_standard_port=True) + + # Ensure we accept local loopback ip (e.g., 127.0.0.1) + with patch( + "homeassistant.helpers.network._get_request_host", return_value="127.0.0.8" + ): + assert get_url(hass, require_current_request=True) == "http://127.0.0.8:8123" + with pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True, allow_ip=False) + + # Ensure hostname from Supervisor is accepted transparently + mock_component(hass, "hassio") + hass.components.hassio.is_hassio = Mock(return_value=True) + hass.components.hassio.get_host_info = Mock( + return_value={"hostname": "homeassistant"} + ) + + with patch( + "homeassistant.helpers.network._get_request_host", + return_value="homeassistant.local", + ): + assert ( + get_url(hass, require_current_request=True) + == "http://homeassistant.local:8123" + ) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="unknown.local" + ), pytest.raises(NoURLAvailableError): + get_url(hass, require_current_request=True)