Skip to content

Commit 80151b2

Browse files
edenhausCopilot
andauthored
Use basic auth in go2rtc (home-assistant#157008)
Co-authored-by: Copilot <[email protected]>
1 parent 4488fdd commit 80151b2

File tree

5 files changed

+355
-41
lines changed

5 files changed

+355
-41
lines changed

homeassistant/components/go2rtc/__init__.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from dataclasses import dataclass
66
import logging
7+
from secrets import token_hex
78
import shutil
89

9-
from aiohttp import ClientSession, UnixConnector
10+
from aiohttp import BasicAuth, ClientSession, UnixConnector
1011
from aiohttp.client_exceptions import ClientConnectionError, ServerConnectionError
1112
from awesomeversion import AwesomeVersion
1213
from go2rtc_client import Go2RtcRestClient
@@ -36,15 +37,23 @@
3637
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
3738
from homeassistant.components.stream import Orientation
3839
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
39-
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
40+
from homeassistant.const import (
41+
CONF_PASSWORD,
42+
CONF_URL,
43+
CONF_USERNAME,
44+
EVENT_HOMEASSISTANT_STOP,
45+
)
4046
from homeassistant.core import Event, HomeAssistant, callback
4147
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
4248
from homeassistant.helpers import (
4349
config_validation as cv,
4450
discovery_flow,
4551
issue_registry as ir,
4652
)
47-
from homeassistant.helpers.aiohttp_client import async_get_clientsession
53+
from homeassistant.helpers.aiohttp_client import (
54+
async_create_clientsession,
55+
async_get_clientsession,
56+
)
4857
from homeassistant.helpers.typing import ConfigType
4958
from homeassistant.util.hass_dict import HassKey
5059
from homeassistant.util.package import is_docker_env
@@ -62,14 +71,43 @@
6271
_LOGGER = logging.getLogger(__name__)
6372

6473
_FFMPEG = "ffmpeg"
74+
_AUTH = "auth"
75+
76+
77+
def _validate_auth(config: dict) -> dict:
78+
"""Validate that username and password are only set when a URL is configured or when debug UI is enabled."""
79+
auth_exists = CONF_USERNAME in config
80+
debug_ui_enabled = config.get(CONF_DEBUG_UI, False)
81+
82+
if debug_ui_enabled and not auth_exists:
83+
raise vol.Invalid("Username and password must be set when debug_ui is true")
84+
85+
if auth_exists and CONF_URL not in config and not debug_ui_enabled:
86+
raise vol.Invalid(
87+
"Username and password can only be set when a URL is configured or debug_ui is true"
88+
)
89+
90+
return config
91+
6592

6693
CONFIG_SCHEMA = vol.Schema(
6794
{
68-
DOMAIN: vol.Schema(
69-
{
70-
vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
71-
vol.Exclusive(CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.boolean,
72-
}
95+
DOMAIN: vol.All(
96+
vol.Schema(
97+
{
98+
vol.Exclusive(CONF_URL, DOMAIN, DEBUG_UI_URL_MESSAGE): cv.url,
99+
vol.Exclusive(
100+
CONF_DEBUG_UI, DOMAIN, DEBUG_UI_URL_MESSAGE
101+
): cv.boolean,
102+
vol.Inclusive(CONF_USERNAME, _AUTH): vol.All(
103+
cv.string, vol.Length(min=1)
104+
),
105+
vol.Inclusive(CONF_PASSWORD, _AUTH): vol.All(
106+
cv.string, vol.Length(min=1)
107+
),
108+
}
109+
),
110+
_validate_auth,
73111
)
74112
},
75113
extra=vol.ALLOW_EXTRA,
@@ -83,12 +121,19 @@
83121
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
84122
"""Set up WebRTC."""
85123
url: str | None = None
124+
username: str | None = None
125+
password: str | None = None
126+
86127
if DOMAIN not in config and DEFAULT_CONFIG_DOMAIN not in config:
87128
await _remove_go2rtc_entries(hass)
88129
return True
89130

131+
domain_config = config.get(DOMAIN, {})
132+
username = domain_config.get(CONF_USERNAME)
133+
password = domain_config.get(CONF_PASSWORD)
134+
90135
if not (configured_by_user := DOMAIN in config) or not (
91-
url := config[DOMAIN].get(CONF_URL)
136+
url := domain_config.get(CONF_URL)
92137
):
93138
if not is_docker_env():
94139
if not configured_by_user:
@@ -101,13 +146,26 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
101146
_LOGGER.error("Could not find go2rtc docker binary")
102147
return False
103148

149+
# Generate random credentials when not provided to secure the server
150+
if not username or not password:
151+
username = token_hex()
152+
password = token_hex()
153+
_LOGGER.debug("Generated random credentials for go2rtc server")
154+
155+
auth = BasicAuth(username, password)
104156
# HA will manage the binary
105-
session = ClientSession(connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET))
157+
# Manually created session (not using the helper) needs to be closed manually
158+
# See on_stop listener below
159+
session = ClientSession(
160+
connector=UnixConnector(path=HA_MANAGED_UNIX_SOCKET), auth=auth
161+
)
106162
server = Server(
107163
hass,
108164
binary,
109165
session,
110-
enable_ui=config.get(DOMAIN, {}).get(CONF_DEBUG_UI, False),
166+
enable_ui=domain_config.get(CONF_DEBUG_UI, False),
167+
username=username,
168+
password=password,
111169
)
112170
try:
113171
await server.start()
@@ -122,6 +180,10 @@ async def on_stop(event: Event) -> None:
122180
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop)
123181

124182
url = HA_MANAGED_URL
183+
elif username and password:
184+
# Create session with BasicAuth if credentials are provided
185+
auth = BasicAuth(username, password)
186+
session = async_create_clientsession(hass, auth=auth)
125187
else:
126188
session = async_get_clientsession(hass)
127189

homeassistant/components/go2rtc/server.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
# Default configuration for HA
2626
# - Unix socket for secure local communication
27+
# - Basic auth enabled, including local connections
2728
# - HTTP API only enabled when UI is enabled
2829
# - Enable rtsp for localhost only as ffmpeg needs it
2930
# - Clear default ice servers
@@ -37,6 +38,9 @@
3738
listen: "{listen_config}"
3839
unix_listen: "{unix_socket}"
3940
allow_paths: {api_allow_paths}
41+
local_auth: true
42+
username: {username}
43+
password: {password}
4044
4145
# ffmpeg needs the exec module
4246
# Restrict execution to only ffmpeg binary
@@ -118,7 +122,7 @@ def _format_list_for_yaml(items: tuple[str, ...]) -> str:
118122
return f"[{formatted_items}]"
119123

120124

121-
def _create_temp_file(enable_ui: bool) -> str:
125+
def _create_temp_file(enable_ui: bool, username: str, password: str) -> str:
122126
"""Create temporary config file."""
123127
app_modules: tuple[str, ...] = _APP_MODULES
124128
api_paths: tuple[str, ...] = _API_ALLOW_PATHS
@@ -142,6 +146,8 @@ def _create_temp_file(enable_ui: bool) -> str:
142146
unix_socket=HA_MANAGED_UNIX_SOCKET,
143147
app_modules=_format_list_for_yaml(app_modules),
144148
api_allow_paths=_format_list_for_yaml(api_paths),
149+
username=username,
150+
password=password,
145151
).encode()
146152
)
147153
return file.name
@@ -157,15 +163,19 @@ def __init__(
157163
session: ClientSession,
158164
*,
159165
enable_ui: bool = False,
166+
username: str,
167+
password: str,
160168
) -> None:
161169
"""Initialize the server."""
162170
self._hass = hass
163171
self._binary = binary
164172
self._session = session
173+
self._enable_ui = enable_ui
174+
self._username = username
175+
self._password = password
165176
self._log_buffer: deque[str] = deque(maxlen=_LOG_BUFFER_SIZE)
166177
self._process: asyncio.subprocess.Process | None = None
167178
self._startup_complete = asyncio.Event()
168-
self._enable_ui = enable_ui
169179
self._watchdog_task: asyncio.Task | None = None
170180
self._watchdog_tasks: list[asyncio.Task] = []
171181

@@ -180,7 +190,7 @@ async def _start(self) -> None:
180190
"""Start the server."""
181191
_LOGGER.debug("Starting go2rtc server")
182192
config_file = await self._hass.async_add_executor_job(
183-
_create_temp_file, self._enable_ui
193+
_create_temp_file, self._enable_ui, self._username, self._password
184194
)
185195

186196
self._startup_complete.clear()

tests/components/go2rtc/snapshots/test_server.ambr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# serializer version: 1
2-
# name: test_server_run_success[False]
2+
# name: test_server_run_success[False-d2a0b844f4cdbe773702176c47c9a675eb0c56a0779b8f880cdb3b492ed3b1c1-bc495d266a32e66ba69b9c72546e00101e04fb573f1bd08863fe4ad1aac02949]
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: ""\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',
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 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',
77
),
88
dict({
99
}),
1010
),
1111
])
1212
# ---
13-
# name: test_server_run_success[True]
13+
# name: test_server_run_success[True-user-pass]
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 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',
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 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',
1818
),
1919
dict({
2020
}),

0 commit comments

Comments
 (0)