From 4ea6ca7f9190606ee98d0bbbc5f3335f52e4831a Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 2 Mar 2022 00:56:20 -0500 Subject: [PATCH] Prefer internal docker URL for VLC telnet when possible (#67090) --- .../components/media_player/browse_media.py | 21 ++++++++-- .../components/vlc_telnet/media_player.py | 7 +++- homeassistant/helpers/network.py | 29 ++++++++++++++ .../media_player/test_browse_media.py | 38 +++++++++++++++++- tests/helpers/test_network.py | 39 +++++++++++++++++++ 5 files changed, 127 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index fa825042817..d85dcc0a3e3 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -10,14 +10,22 @@ import yarl from homeassistant.components.http.auth import async_sign_path from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.network import get_url, is_hass_url +from homeassistant.helpers.network import ( + get_supervisor_network_url, + get_url, + is_hass_url, +) from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY @callback def async_process_play_media_url( - hass: HomeAssistant, media_content_id: str, *, allow_relative_url: bool = False + hass: HomeAssistant, + media_content_id: str, + *, + allow_relative_url: bool = False, + for_supervisor_network: bool = False, ) -> str: """Update a media URL with authentication if it points at Home Assistant.""" if media_content_id[0] != "/" and not is_hass_url(hass, media_content_id): @@ -39,7 +47,14 @@ def async_process_play_media_url( # convert relative URL to absolute URL if media_content_id[0] == "/" and not allow_relative_url: - media_content_id = f"{get_url(hass)}{media_content_id}" + base_url = None + if for_supervisor_network: + base_url = get_supervisor_network_url(hass) + + if not base_url: + base_url = get_url(hass) + + media_content_id = f"{base_url}{media_content_id}" return media_content_id diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 140c2b2c253..72beede3f2f 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -31,7 +31,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -125,6 +125,7 @@ class VlcDevice(MediaPlayerEntity): manufacturer="VideoLAN", name=name, ) + self._using_addon = config_entry.source == SOURCE_HASSIO @catch_vlc_errors async def async_update(self) -> None: @@ -316,7 +317,9 @@ class VlcDevice(MediaPlayerEntity): ) # If media ID is a relative URL, we serve it from HA. - media_id = async_process_play_media_url(self.hass, media_id) + media_id = async_process_play_media_url( + self.hass, media_id, for_supervisor_network=self._using_addon + ) await self._vlc.add(media_id) self._state = STATE_PLAYING diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index a8c4b3cf458..76c51fa29d2 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,7 @@ """Network helpers.""" from __future__ import annotations +from collections.abc import Callable from contextlib import suppress from ipaddress import ip_address from typing import cast @@ -15,6 +16,7 @@ from homeassistant.util.network import is_ip_address, is_loopback, normalize_url TYPE_URL_INTERNAL = "internal_url" TYPE_URL_EXTERNAL = "external_url" +SUPERVISOR_NETWORK_HOST = "homeassistant" class NoURLAvailableError(HomeAssistantError): @@ -33,6 +35,31 @@ def is_internal_request(hass: HomeAssistant) -> bool: return False +@bind_hass +def get_supervisor_network_url( + hass: HomeAssistant, *, allow_ssl: bool = False +) -> str | None: + """Get URL for home assistant within supervisor network.""" + if hass.config.api is None or not hass.components.hassio.is_hassio(): + return None + + scheme = "http" + if hass.config.api.use_ssl: + # Certificate won't be valid for hostname so this URL usually won't work + if not allow_ssl: + return None + + scheme = "https" + + return str( + yarl.URL.build( + scheme=scheme, + host=SUPERVISOR_NETWORK_HOST, + port=hass.config.api.port, + ) + ) + + def is_hass_url(hass: HomeAssistant, url: str) -> bool: """Return if the URL points at this Home Assistant instance.""" parsed = yarl.URL(normalize_url(url)) @@ -53,11 +80,13 @@ def is_hass_url(hass: HomeAssistant, url: str) -> bool: except NoURLAvailableError: return None + potential_base_factory: Callable[[], str | None] for potential_base_factory in ( lambda: hass.config.internal_url, lambda: hass.config.external_url, cloud_url, host_ip, + lambda: get_supervisor_network_url(hass, allow_ssl=True), ): potential_base = potential_base_factory() diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index ba7a93fc3a3..5e4bac2c635 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -8,9 +8,11 @@ from homeassistant.components.media_player.browse_media import ( ) from homeassistant.config import async_process_ha_core_config +from tests.common import mock_component -@pytest.fixture -def mock_sign_path(): + +@pytest.fixture(name="mock_sign_path") +def fixture_mock_sign_path(): """Mock sign path.""" with patch( "homeassistant.components.media_player.browse_media.async_sign_path", @@ -58,3 +60,35 @@ async def test_process_play_media_url(hass, mock_sign_path): ) == "http://192.168.123.123:8123/path?hello=world" ) + + +async def test_process_play_media_url_for_addon(hass, mock_sign_path): + """Test it uses the hostname for an addon if available.""" + await async_process_ha_core_config( + hass, + { + "internal_url": "http://example.local:8123", + "external_url": "https://example.com", + }, + ) + + # Not hassio or hassio not loaded yet, don't use supervisor network url + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "http://homeassistant:8123/path?authSig=bla" + ) + + # Is hassio and not SSL, use an supervisor network url + mock_component(hass, "hassio") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + == "http://homeassistant:8123/path?authSig=bla" + ) + + # Hassio loaded but using SSL, don't use an supervisor network url + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert ( + async_process_play_media_url(hass, "/path", for_supervisor_network=True) + != "https://homeassistant:8123/path?authSig=bla" + ) diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 0838375fd1f..0c9e8361104 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -12,6 +12,7 @@ from homeassistant.helpers.network import ( _get_external_url, _get_internal_url, _get_request_host, + get_supervisor_network_url, get_url, is_hass_url, is_internal_request, @@ -715,3 +716,41 @@ async def test_is_hass_url(hass): assert is_hass_url(hass, "https://example.nabu.casa") is True assert is_hass_url(hass, "http://example.nabu.casa:443") is False assert is_hass_url(hass, "http://example.nabu.casa") is False + + +async def test_is_hass_url_addon_url(hass): + """Test is_hass_url with a supervisor network URL.""" + assert is_hass_url(hass, "http://homeassistant:8123") is False + + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + assert is_hass_url(hass, "http://homeassistant:8123") is False + + mock_component(hass, "hassio") + assert is_hass_url(hass, "http://homeassistant:8123") + assert not is_hass_url(hass, "https://homeassistant:8123") + + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert not is_hass_url(hass, "http://homeassistant:8123") + assert is_hass_url(hass, "https://homeassistant:8123") + + +async def test_get_supervisor_network_url(hass): + """Test get_supervisor_network_url.""" + assert get_supervisor_network_url(hass) is None + + hass.config.api = Mock(use_ssl=False, port=8123, local_ip="192.168.123.123") + await async_process_ha_core_config(hass, {}) + assert get_supervisor_network_url(hass) is None + + mock_component(hass, "hassio") + assert get_supervisor_network_url(hass) == "http://homeassistant:8123" + + hass.config.api = Mock(use_ssl=True, port=8123, local_ip="192.168.123.123") + assert get_supervisor_network_url(hass) is None + assert ( + get_supervisor_network_url(hass, allow_ssl=True) == "https://homeassistant:8123" + )