Skip to content

Commit d01843e

Browse files
authored
Use unix socket for HA managed go2rtc instance (home-assistant#156968)
1 parent 9964cb5 commit d01843e

File tree

6 files changed

+115
-23
lines changed

6 files changed

+115
-23
lines changed

homeassistant/components/go2rtc/__init__.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from __future__ import annotations
44

5+
from dataclasses import dataclass
56
import logging
67
import shutil
78

8-
from aiohttp import ClientSession
9+
from aiohttp import ClientSession, UnixConnector
910
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
1011
from awesomeversion import AwesomeVersion
1112
from go2rtc_client import Go2RtcRestClient
@@ -52,6 +53,7 @@
5253
CONF_DEBUG_UI,
5354
DEBUG_UI_URL_MESSAGE,
5455
DOMAIN,
56+
HA_MANAGED_UNIX_SOCKET,
5557
HA_MANAGED_URL,
5658
RECOMMENDED_VERSION,
5759
)
@@ -73,7 +75,7 @@
7375
extra=vol.ALLOW_EXTRA,
7476
)
7577

76-
_DATA_GO2RTC: HassKey[str] = HassKey(DOMAIN)
78+
_DATA_GO2RTC: HassKey[Go2RtcConfig] = HassKey(DOMAIN)
7779
_RETRYABLE_ERRORS = (ClientConnectionError, ServerConnectionError)
7880
type Go2RtcConfigEntry = ConfigEntry[WebRTCProvider]
7981

@@ -100,8 +102,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
100102
return False
101103

102104
# HA will manage the binary
105+
session = ClientSession(connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET))
103106
server = Server(
104-
hass, binary, enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False)
107+
hass,
108+
binary,
109+
session,
110+
enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False),
105111
)
106112
try:
107113
await server.start()
@@ -111,12 +117,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
111117

112118
async def on_stop(event: Event) -> None:
113119
await server.stop()
120+
await session.close()
114121

115122
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
116123

117124
url = HA_MANAGED_URL
125+
else:
126+
session = async_get_clientsession(hass)
118127

119-
hass.data[_DATA_GO2RTC] = url
128+
hass.data[_DATA_GO2RTC] = Go2RtcConfig(url, session)
120129
discovery_flow.async_create_flow(
121130
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
122131
)
@@ -132,8 +141,9 @@ async def _remove_go2rtc_entries(hass: HomeAssistant) -> None:
132141
async def async_setup_entry(hass: HomeAssistant, entry: Go2RtcConfigEntry) -> bool:
133142
"""Set up go2rtc from a config entry."""
134143

135-
url = hass.data[_DATA_GO2RTC]
136-
session = async_get_clientsession(hass)
144+
config = hass.data[_DATA_GO2RTC]
145+
url = config.url
146+
session = config.session
137147
client = Go2RtcRestClient(session, url)
138148
# Validate the server URL
139149
try:
@@ -342,3 +352,11 @@ async def teardown(self) -> None:
342352
for ws_client in self._sessions.values():
343353
await ws_client.close()
344354
self._sessions.clear()
355+
356+
357+
@dataclass
358+
class Go2RtcConfig:
359+
"""Go2rtc configuration."""
360+
361+
url: str
362+
session: ClientSession

homeassistant/components/go2rtc/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
DEBUG_UI_URL_MESSAGE = "Url and debug_ui cannot be set at the same time."
77
HA_MANAGED_API_PORT = 11984
88
HA_MANAGED_URL = f"http://localhost:{HA_MANAGED_API_PORT}/"
9+
HA_MANAGED_UNIX_SOCKET = "/run/go2rtc.sock"
910
RECOMMENDED_VERSION = "1.9.12"

homeassistant/components/go2rtc/server.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
import logging
77
from tempfile import NamedTemporaryFile
88

9+
from aiohttp import ClientSession
910
from go2rtc_client import Go2RtcRestClient
1011

1112
from homeassistant.core import HomeAssistant
1213
from homeassistant.exceptions import HomeAssistantError
13-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1414

15-
from .const import HA_MANAGED_API_PORT, HA_MANAGED_URL
15+
from .const import HA_MANAGED_API_PORT, HA_MANAGED_UNIX_SOCKET, HA_MANAGED_URL
1616

1717
_LOGGER = logging.getLogger(__name__)
1818
_TERMINATE_TIMEOUT = 5
@@ -23,7 +23,8 @@
2323
_RESPAWN_COOLDOWN = 1
2424

2525
# Default configuration for HA
26-
# - Api is listening only on localhost
26+
# - Unix socket for secure local communication
27+
# - HTTP API only enabled when UI is enabled
2728
# - Enable rtsp for localhost only as ffmpeg needs it
2829
# - Clear default ice servers
2930
_GO2RTC_CONFIG_FORMAT = r"""# This file is managed by Home Assistant
@@ -33,7 +34,8 @@
3334
modules: {app_modules}
3435
3536
api:
36-
listen: "{api_ip}:{api_port}"
37+
listen: "{listen_config}"
38+
unix_listen: "{unix_socket}"
3739
allow_paths: {api_allow_paths}
3840
3941
# ffmpeg needs the exec module
@@ -120,20 +122,24 @@ def _create_temp_file(enable_ui: bool) -> str:
120122
"""Create temporary config file."""
121123
app_modules: tuple[str, ...] = _APP_MODULES
122124
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
123-
api_ip = _LOCALHOST_IP
125+
124126
if enable_ui:
125127
app_modules = _UI_APP_MODULES
126128
api_paths = _UI_API_ALLOW_PATHS
127129
# Listen on all interfaces for allowing access from all ips
128-
api_ip = ""
130+
listen_config = f":{HA_MANAGED_API_PORT}"
131+
else:
132+
# Disable HTTP listening when UI is not enabled
133+
# as HA does not use it.
134+
listen_config = ""
129135

130136
# Set delete=False to prevent the file from being deleted when the file is closed
131137
# Linux is clearing tmp folder on reboot, so no need to delete it manually
132138
with NamedTemporaryFile(prefix="go2rtc_", suffix=".yaml", delete=False) as file:
133139
file.write(
134140
_GO2RTC_CONFIG_FORMAT.format(
135-
api_ip=api_ip,
136-
api_port=HA_MANAGED_API_PORT,
141+
listen_config=listen_config,
142+
unix_socket=HA_MANAGED_UNIX_SOCKET,
137143
app_modules=_format_list_for_yaml(app_modules),
138144
api_allow_paths=_format_list_for_yaml(api_paths),
139145
).encode()
@@ -145,11 +151,17 @@ class Server:
145151
"""Go2rtc server."""
146152

147153
def __init__(
148-
self, hass: HomeAssistant, binary: str, *, enable_ui: bool = False
154+
self,
155+
hass: HomeAssistant,
156+
binary: str,
157+
session: ClientSession,
158+
*,
159+
enable_ui: bool = False,
149160
) -> None:
150161
"""Initialize the server."""
151162
self._hass = hass
152163
self._binary = binary
164+
self._session = session
153165
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
154166
self._process: asyncio.subprocess.Process | None = None
155167
self._startup_complete = asyncio.Event()
@@ -197,7 +209,7 @@ async def _start(self) -> None:
197209
raise Go2RTCServerStartError from err
198210

199211
# Check the server version
200-
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
212+
client = Go2RtcRestClient(self._session, HA_MANAGED_URL)
201213
await client.validate_server_version()
202214

203215
async def _log_output(self, process: asyncio.subprocess.Process) -> None:
@@ -269,7 +281,7 @@ async def _monitor_process(self) -> None:
269281

270282
async def _monitor_api(self) -> None:
271283
"""Raise if the go2rtc process terminates."""
272-
client = Go2RtcRestClient(async_get_clientsession(self._hass), HA_MANAGED_URL)
284+
client = Go2RtcRestClient(self._session, HA_MANAGED_URL)
273285

274286
_LOGGER.debug("Monitoring go2rtc API")
275287
try:

tests/components/go2rtc/snapshots/test_server.ambr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
_CallList([
44
_Call(
55
tuple(
6-
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: "127.0.0.1:11984"\n allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws"]\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',
6+
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\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',
77
),
88
dict({
99
}),
@@ -14,7 +14,7 @@
1414
_CallList([
1515
_Call(
1616
tuple(
17-
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 allow_paths: ["/","/api","/api/frame.jpeg","/api/schemes","/api/streams","/api/webrtc","/api/ws","/api/config","/api/log","/api/streams.dot"]\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',
17+
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\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',
1818
),
1919
dict({
2020
}),

tests/components/go2rtc/test_init.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from collections.abc import Awaitable, Callable
44
import logging
55
from typing import NamedTuple
6-
from unittest.mock import AsyncMock, Mock, patch
6+
from unittest.mock import ANY, AsyncMock, Mock, patch
77

8+
from aiohttp import UnixConnector
89
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
910
from awesomeversion import AwesomeVersion
1011
from go2rtc_client import Stream
@@ -38,11 +39,13 @@
3839
CONF_DEBUG_UI,
3940
DEBUG_UI_URL_MESSAGE,
4041
DOMAIN,
42+
HA_MANAGED_UNIX_SOCKET,
4143
RECOMMENDED_VERSION,
4244
)
4345
from homeassistant.components.stream import Orientation
4446
from homeassistant.config_entries import ConfigEntryState
4547
from homeassistant.const import CONF_URL
48+
from homeassistant.core import EVENT_HOMEASSISTANT_STOP
4649
from homeassistant.exceptions import HomeAssistantError
4750
from homeassistant.helpers import issue_registry as ir
4851
from homeassistant.helpers.typing import ConfigType
@@ -215,7 +218,9 @@ async def test_setup_go_binary(
215218
assert (len(hass.config_entries.async_entries(DOMAIN)) == 1) == has_go2rtc_entry
216219

217220
def after_setup() -> None:
218-
server.assert_called_once_with(hass, "/usr/bin/go2rtc", enable_ui=ui_enabled)
221+
server.assert_called_once_with(
222+
hass, "/usr/bin/go2rtc", ANY, enable_ui=ui_enabled
223+
)
219224
server_start.assert_called_once()
220225

221226
await _test_setup_and_signaling(
@@ -905,3 +910,53 @@ async def test_stream_orientation_with_generic_camera(
905910
rest_client,
906911
"ffmpeg:https://test.stream/video.m3u8#video=h264#audio=copy#raw=-vf vflip",
907912
)
913+
914+
915+
@pytest.mark.usefixtures(
916+
"mock_get_binary",
917+
"mock_is_docker_env",
918+
"mock_go2rtc_entry",
919+
"rest_client",
920+
"server",
921+
)
922+
async def test_unix_socket_connection(hass: HomeAssistant) -> None:
923+
"""Test Unix socket is used for HA-managed go2rtc instances."""
924+
config = {DOMAIN: {}}
925+
926+
with patch("homeassistant.components.go2rtc.ClientSession") as mock_session_cls:
927+
mock_session = AsyncMock()
928+
mock_session_cls.return_value = mock_session
929+
930+
assert await async_setup_component(hass, DOMAIN, config)
931+
await hass.async_block_till_done(wait_background_tasks=True)
932+
933+
# Verify ClientSession was created with UnixConnector
934+
mock_session_cls.assert_called_once()
935+
call_kwargs = mock_session_cls.call_args[1]
936+
assert "connector" in call_kwargs
937+
connector = call_kwargs["connector"]
938+
assert isinstance(connector, UnixConnector)
939+
assert connector.path == HA_MANAGED_UNIX_SOCKET
940+
941+
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
942+
await hass.async_block_till_done()
943+
944+
mock_session.close.assert_called_once()
945+
946+
947+
@pytest.mark.usefixtures("rest_client", "server")
948+
async def test_unix_socket_not_used_for_custom_server(hass: HomeAssistant) -> None:
949+
"""Test Unix socket is not used for custom go2rtc instances."""
950+
config = {DOMAIN: {CONF_URL: "http://localhost:1984/"}}
951+
952+
with patch(
953+
"homeassistant.components.go2rtc.async_get_clientsession"
954+
) as mock_get_session:
955+
mock_session = AsyncMock()
956+
mock_get_session.return_value = mock_session
957+
958+
assert await async_setup_component(hass, DOMAIN, config)
959+
await hass.async_block_till_done(wait_background_tasks=True)
960+
961+
# Verify standard clientsession was used, not UnixConnector
962+
mock_get_session.assert_called_once_with(hass)

tests/components/go2rtc/test_server.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@ def enable_ui() -> bool:
2323

2424

2525
@pytest.fixture
26-
def server(hass: HomeAssistant, enable_ui: bool) -> Server:
26+
def mock_session() -> AsyncMock:
27+
"""Fixture to provide a mock ClientSession."""
28+
return AsyncMock()
29+
30+
31+
@pytest.fixture
32+
def server(hass: HomeAssistant, mock_session: AsyncMock, enable_ui: bool) -> Server:
2733
"""Fixture to initialize the Server."""
28-
return Server(hass, binary=TEST_BINARY, enable_ui=enable_ui)
34+
return Server(hass, binary=TEST_BINARY, session=mock_session, enable_ui=enable_ui)
2935

3036

3137
@pytest.fixture

0 commit comments

Comments
 (0)