From 2c72cd94f28167fc6680e3cc9f5cb8352e802a9d Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Tue, 2 Dec 2025 13:35:39 +0100 Subject: [PATCH] Create the go2rtc unix socket inside a temporary folder (#157742) --- homeassistant/components/go2rtc/__init__.py | 8 ++++-- homeassistant/components/go2rtc/const.py | 1 - homeassistant/components/go2rtc/server.py | 22 +++++++++++----- homeassistant/components/go2rtc/util.py | 12 +++++++++ tests/components/go2rtc/conftest.py | 13 ++++++++++ .../go2rtc/snapshots/test_server.ambr | 4 +-- tests/components/go2rtc/test_init.py | 13 ++++++---- tests/components/go2rtc/test_server.py | 25 ++++++++++++------- 8 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/go2rtc/util.py diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index f93d35a86ae..cde9b5c8367 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -6,6 +6,7 @@ from dataclasses import dataclass import logging from secrets import token_hex import shutil +from tempfile import mkdtemp from aiohttp import BasicAuth, ClientSession, UnixConnector from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError @@ -62,11 +63,11 @@ from .const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, - HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL, RECOMMENDED_VERSION, ) from .server import Server +from .util import get_go2rtc_unix_socket_path _LOGGER = logging.getLogger(__name__) @@ -154,10 +155,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: auth = BasicAuth(username, password) # HA will manage the binary + temp_dir = mkdtemp(prefix="go2rtc-") # Manually created session (not using the helper) needs to be closed manually # See on_stop listener below session = ClientSession( - connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth + connector=UnixConnector(path=get_go2rtc_unix_socket_path(temp_dir)), + auth=auth, ) server = Server( hass, @@ -166,6 +169,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: enable_ui=domain_config.get(CONF_DEBUG_UI, False), username=username, password=password, + working_dir=temp_dir, ) try: await server.start() diff --git a/homeassistant/components/go2rtc/const.py b/homeassistant/components/go2rtc/const.py index b827c59fe6d..35c0c6fb70e 100644 --- a/homeassistant/components/go2rtc/const.py +++ b/homeassistant/components/go2rtc/const.py @@ -6,7 +6,6 @@ CONF_DEBUG_UI = "debug_ui" DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time." HA_MANAGED_API_PORT = 11984 HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/" -HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock" # When changing this version, also update the corresponding SHA hash (_GO2RTC_SHA) # in script/hassfest/docker.py. RECOMMENDED_VERSION = "1.9.12" diff --git a/homeassistant/components/go2rtc/server.py b/homeassistant/components/go2rtc/server.py index f1b375c9111..08af88f0929 100644 --- a/homeassistant/components/go2rtc/server.py +++ b/homeassistant/components/go2rtc/server.py @@ -12,13 +12,13 @@ from go2rtc_client import Go2RtcRestClient from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL +from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL +from .util import get_go2rtc_unix_socket_path _LOGGER = logging.getLogger(__name__) _TERMINATE_TIMEOUT = 5 _SETUP_TIMEOUT = 30 _SUCCESSFUL_BOOT_MESSAGE = "INF [api] listen addr=" -_LOCALHOST_IP = "127.0.0.1" _LOG_BUFFER_SIZE = 512 _RESPAWN_COOLDOWN = 1 @@ -122,7 +122,9 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str: return f"[{formatted_items}]" -def _create_temp_file(enable_ui: bool, username: str, password: str) -> str: +def _create_temp_file( + enable_ui: bool, username: str, password: str, working_dir: str +) -> str: """Create temporary config file.""" app_modules: tuple[str, ...] = _APP_MODULES api_paths: tuple[str, ...] = _API_ALLOW_PATHS @@ -139,11 +141,13 @@ def _create_temp_file(enable_ui: bool, username: str, password: str) -> str: # Set delete=False to prevent the file from being deleted when the file is closed # Linux is clearing tmp folder on reboot, so no need to delete it manually - with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file: + with NamedTemporaryFile( + prefix="go2rtc_", suffix=".yaml", dir=working_dir, delete=False + ) as file: file.write( _GO2RTC_CONFIG_FORMAT.format( listen_config=listen_config, - unix_socket=HA_MANAGED_UNIX_SOCKET, + unix_socket=get_go2rtc_unix_socket_path(working_dir), app_modules=_format_list_for_yaml(app_modules), api_allow_paths=_format_list_for_yaml(api_paths), username=username, @@ -165,6 +169,7 @@ class Server: enable_ui: bool = False, username: str, password: str, + working_dir: str, ) -> None: """Initialize the server.""" self._hass = hass @@ -173,6 +178,7 @@ class Server: self._enable_ui = enable_ui self._username = username self._password = password + self._working_dir = working_dir self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE) self._process: asyncio.subprocess.Process | None = None self._startup_complete = asyncio.Event() @@ -190,7 +196,11 @@ class Server: """Start the server.""" _LOGGER.debug("Starting go2rtc server") config_file = await self._hass.async_add_executor_job( - _create_temp_file, self._enable_ui, self._username, self._password + _create_temp_file, + self._enable_ui, + self._username, + self._password, + self._working_dir, ) self._startup_complete.clear() diff --git a/homeassistant/components/go2rtc/util.py b/homeassistant/components/go2rtc/util.py new file mode 100644 index 00000000000..6e47075dbf9 --- /dev/null +++ b/homeassistant/components/go2rtc/util.py @@ -0,0 +1,12 @@ +"""Go2rtc utility functions.""" + +from pathlib import Path + +_HA_MANAGED_UNIX_SOCKET_FILE = "go2rtc.sock" + + +def get_go2rtc_unix_socket_path(path: str | Path) -> str: + """Get the Go2rtc unix socket path.""" + if not isinstance(path, Path): + path = Path(path) + return str(path / _HA_MANAGED_UNIX_SOCKET_FILE) diff --git a/tests/components/go2rtc/conftest.py b/tests/components/go2rtc/conftest.py index ff5748bffb1..3186a0536f4 100644 --- a/tests/components/go2rtc/conftest.py +++ b/tests/components/go2rtc/conftest.py @@ -1,6 +1,7 @@ """Go2rtc test configuration.""" from collections.abc import Generator +from pathlib import Path from unittest.mock import AsyncMock, Mock, patch from awesomeversion import AwesomeVersion @@ -228,3 +229,15 @@ async def init_test_integration( await hass.async_block_till_done() return test_camera + + +@pytest.fixture +def server_dir(tmp_path: Path) -> Generator[Path]: + """Fixture to provide a temporary directory for the server.""" + server_dir = tmp_path / "go2rtc" + server_dir.mkdir() + with patch( + "homeassistant.components.go2rtc.mkdtemp", + return_value=str(server_dir), + ): + yield server_dir diff --git a/tests/components/go2rtc/snapshots/test_server.ambr b/tests/components/go2rtc/snapshots/test_server.ambr index b4efd4ada1a..61f5115e8ea 100644 --- a/tests/components/go2rtc/snapshots/test_server.ambr +++ b/tests/components/go2rtc/snapshots/test_server.ambr @@ -3,7 +3,7 @@ _CallList([ _Call( tuple( - b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n local_auth: true\n username: d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1\n password: bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', + b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws"]\n\napi:\n listen: ""\n unix_listen: "/test/path/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\n local_auth: true\n username: d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1\n password: bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', ), dict({ }), @@ -14,7 +14,7 @@ _CallList([ _Call( tuple( - b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/run/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n local_auth: true\n username: user\n password: pass\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', + b'# This file is managed by Home Assistant\n# Do not edit it manually\n\napp:\n modules: ["api","exec","ffmpeg","http","mjpeg","onvif","rtmp","rtsp","srtp","webrtc","ws","debug"]\n\napi:\n listen: ":11984"\n unix_listen: "/test/path/go2rtc.sock"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\n local_auth: true\n username: user\n password: pass\n\n# ffmpeg needs the exec module\n# Restrict execution to only ffmpeg binary\nexec:\n allow_paths:\n - ffmpeg\n\nrtsp:\n listen: "127.0.0.1:18554"\n\nwebrtc:\n listen: ":18555/tcp"\n ice_servers: []\n', ), dict({ }), diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index de291f4f37e..426cfc5f01e 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable import logging +from pathlib import Path from typing import NamedTuple from unittest.mock import ANY, AsyncMock, Mock, patch @@ -39,9 +40,9 @@ from homeassistant.components.go2rtc.const import ( CONF_DEBUG_UI, DEBUG_UI_URL_MESSAGE, DOMAIN, - HA_MANAGED_UNIX_SOCKET, RECOMMENDED_VERSION, ) +from homeassistant.components.go2rtc.util import get_go2rtc_unix_socket_path from homeassistant.components.stream import Orientation from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -239,6 +240,7 @@ async def test_setup_go_binary( server_stop: Mock, init_test_integration: MockCamera, has_go2rtc_entry: bool, + server_dir: Path, config: ConfigType, ui_enabled: bool, expected_username: str, @@ -255,6 +257,7 @@ async def test_setup_go_binary( enable_ui=ui_enabled, username=expected_username, password=expected_password, + working_dir=str(server_dir), ) call_kwargs = server.call_args[1] assert call_kwargs["username"] == expected_username @@ -1034,7 +1037,7 @@ async def test_stream_orientation_with_generic_camera( "rest_client", "server", ) -async def test_unix_socket_connection(hass: HomeAssistant) -> None: +async def test_unix_socket_connection(hass: HomeAssistant, server_dir: Path) -> None: """Test Unix socket is used for HA-managed go2rtc instances.""" config = {DOMAIN: {}} @@ -1056,7 +1059,7 @@ async def test_unix_socket_connection(hass: HomeAssistant) -> None: assert "connector" in call_kwargs connector = call_kwargs["connector"] assert isinstance(connector, UnixConnector) - assert connector.path == HA_MANAGED_UNIX_SOCKET + assert connector.path == get_go2rtc_unix_socket_path(server_dir) # Auth should be auto-generated when credentials are not explicitly configured assert "auth" in call_kwargs auth = call_kwargs["auth"] @@ -1120,7 +1123,7 @@ async def test_basic_auth_with_custom_url(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("rest_client") -async def test_basic_auth_with_debug_ui(hass: HomeAssistant) -> None: +async def test_basic_auth_with_debug_ui(hass: HomeAssistant, server_dir: Path) -> None: """Test BasicAuth session is created when username and password are provided with debug_ui.""" config = { DOMAIN: { @@ -1158,7 +1161,7 @@ async def test_basic_auth_with_debug_ui(hass: HomeAssistant) -> None: assert "connector" in call_kwargs connector = call_kwargs["connector"] assert isinstance(connector, UnixConnector) - assert connector.path == HA_MANAGED_UNIX_SOCKET + assert connector.path == get_go2rtc_unix_socket_path(server_dir) assert "auth" in call_kwargs auth = call_kwargs["auth"] assert isinstance(auth, BasicAuth) diff --git a/tests/components/go2rtc/test_server.py b/tests/components/go2rtc/test_server.py index e84749e240e..e51e05b75f1 100644 --- a/tests/components/go2rtc/test_server.py +++ b/tests/components/go2rtc/test_server.py @@ -3,6 +3,7 @@ import asyncio from collections.abc import Generator import logging +from pathlib import Path import subprocess from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -47,16 +48,22 @@ def server( enable_ui: bool, username: str, password: str, -) -> Server: + server_dir: Path, +) -> Generator[Server]: """Fixture to initialize the Server.""" - return Server( - hass, - binary=TEST_BINARY, - session=mock_session, - enable_ui=enable_ui, - username=username, - password=password, - ) + with patch( + "homeassistant.components.go2rtc.server.get_go2rtc_unix_socket_path", + return_value="/test/path/go2rtc.sock", + ): + yield Server( + hass, + binary=TEST_BINARY, + session=mock_session, + enable_ui=enable_ui, + username=username, + password=password, + working_dir=str(server_dir), + ) @pytest.fixture