mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-15 03:02:49 +03:00
Explain why a go2rtc stream change was rejected
A stream source with an undefined or malformed {FRIGATE_*} placeholder
was reported as a restricted source, which reads like a security block
when the user made a typo. The config API now reports unknown variables
and invalid placeholders separately from restricted sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c43b8f58f9
commit
d43a1d8565
+18
-10
@@ -73,6 +73,7 @@ from frigate.util.config import (
|
||||
)
|
||||
from frigate.util.schema import get_config_schema
|
||||
from frigate.util.services import (
|
||||
Go2rtcStreamIssue,
|
||||
find_restricted_go2rtc_stream_changes,
|
||||
get_nvidia_driver_info,
|
||||
process_logs,
|
||||
@@ -556,19 +557,26 @@ def _restricted_go2rtc_streams_response(
|
||||
# with no readable saved config, every source is treated as new
|
||||
old_streams = None
|
||||
|
||||
restricted = find_restricted_go2rtc_stream_changes(old_streams, new_streams)
|
||||
if not restricted:
|
||||
issues = find_restricted_go2rtc_stream_changes(old_streams, new_streams)
|
||||
if not issues:
|
||||
return None
|
||||
|
||||
logger.warning(
|
||||
"Rejected config change with restricted go2rtc stream sources: %s",
|
||||
", ".join(restricted),
|
||||
)
|
||||
messages = []
|
||||
for issue, label in (
|
||||
(Go2rtcStreamIssue.restricted, "Restricted go2rtc stream source in"),
|
||||
(Go2rtcStreamIssue.unknown_variable, "Unknown variable in go2rtc stream"),
|
||||
(
|
||||
Go2rtcStreamIssue.invalid_placeholder,
|
||||
"Invalid variable placeholder in go2rtc stream",
|
||||
),
|
||||
):
|
||||
names = [name for name, found in issues.items() if found == issue]
|
||||
if names:
|
||||
messages.append(f"{label}: {', '.join(names)}")
|
||||
|
||||
logger.warning("Rejected config change: %s", "; ".join(messages))
|
||||
return JSONResponse(
|
||||
content={
|
||||
"success": False,
|
||||
"message": f"Restricted go2rtc stream source in: {', '.join(restricted)}",
|
||||
},
|
||||
content={"success": False, "message": "; ".join(messages)},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
|
||||
@@ -177,15 +177,38 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
|
||||
def test_set_rejects_unresolvable_placeholder(self):
|
||||
before = self._read_config()
|
||||
with patch.dict(FRIGATE_ENV_VARS, {"FRIGATE_RTSP_USER": "user"}):
|
||||
for src in (
|
||||
"{FRIGATE_RTSP_USER!r}fmpeg:front_door#raw=-vn",
|
||||
"{FRIGATE_RTSP_USER:>5}",
|
||||
"rtsp://{FRIGATE_UNDEFINED}@10.0.0.2:554/video",
|
||||
for src, message in (
|
||||
(
|
||||
"{FRIGATE_RTSP_USER!r}fmpeg:front_door#raw=-vn",
|
||||
"Invalid variable placeholder in go2rtc stream: new_stream",
|
||||
),
|
||||
(
|
||||
"{FRIGATE_RTSP_USER:>5}",
|
||||
"Invalid variable placeholder in go2rtc stream: new_stream",
|
||||
),
|
||||
(
|
||||
"rtsp://{FRIGATE_UNDEFINED}@10.0.0.2:554/video",
|
||||
"Unknown variable in go2rtc stream: new_stream",
|
||||
),
|
||||
):
|
||||
resp = self._set_streams({"new_stream": [src]})
|
||||
assert resp.status_code == 400, src
|
||||
assert resp.json()["message"] == message, src
|
||||
assert self._read_config() == before
|
||||
|
||||
def test_set_reports_each_issue(self):
|
||||
resp = self._set_streams(
|
||||
{
|
||||
"typo_stream": ["rtsp://{FRIGATE_UNDEFINED}@10.0.0.2:554/video"],
|
||||
"injected_stream": [_INJECTED_SRC],
|
||||
}
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["message"] == (
|
||||
"Restricted go2rtc stream source in: injected_stream; "
|
||||
"Unknown variable in go2rtc stream: typo_stream"
|
||||
)
|
||||
|
||||
def test_set_allows_resolvable_placeholder(self):
|
||||
src = "rtsp://{FRIGATE_RTSP_USER}@10.0.0.2:554/video"
|
||||
with patch.dict(FRIGATE_ENV_VARS, {"FRIGATE_RTSP_USER": "user"}):
|
||||
|
||||
+24
-12
@@ -12,6 +12,7 @@ import subprocess as sp
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
@@ -1043,22 +1044,30 @@ def _go2rtc_stream_sources(stream: Any) -> list[Any]:
|
||||
return stream if isinstance(stream, list) else [stream]
|
||||
|
||||
|
||||
class Go2rtcStreamIssue(str, Enum):
|
||||
"""Why a go2rtc stream source was rejected."""
|
||||
|
||||
restricted = "restricted"
|
||||
unknown_variable = "unknown_variable"
|
||||
invalid_placeholder = "invalid_placeholder"
|
||||
|
||||
|
||||
def find_restricted_go2rtc_stream_changes(
|
||||
old_streams: dict[str, Any] | None, new_streams: dict[str, Any] | None
|
||||
) -> list[str]:
|
||||
"""Return the names of go2rtc streams with new restricted sources.
|
||||
) -> dict[str, Go2rtcStreamIssue]:
|
||||
"""Return the go2rtc streams with new sources that must be rejected.
|
||||
|
||||
Used when the config is changed through the API. A source that is already
|
||||
in the saved config for the same stream is left alone, so streams the
|
||||
operator configured on disk keep working; any other source must pass
|
||||
`is_restricted_go2rtc_api_source`.
|
||||
operator configured on disk keep working; any other source must resolve
|
||||
its {FRIGATE_*} placeholders and pass `is_restricted_go2rtc_api_source`.
|
||||
"""
|
||||
# imported here since frigate.config imports this module
|
||||
from frigate.config.env import substitute_frigate_vars
|
||||
|
||||
old_streams = old_streams if isinstance(old_streams, dict) else {}
|
||||
new_streams = new_streams if isinstance(new_streams, dict) else {}
|
||||
restricted = []
|
||||
issues: dict[str, Go2rtcStreamIssue] = {}
|
||||
|
||||
for name, stream in new_streams.items():
|
||||
existing = _go2rtc_stream_sources(old_streams.get(name))
|
||||
@@ -1071,24 +1080,27 @@ def find_restricted_go2rtc_stream_changes(
|
||||
# the map form is only usable with arbitrary exec enabled
|
||||
if is_go2rtc_arbitrary_exec_allowed():
|
||||
continue
|
||||
restricted.append(name)
|
||||
issues[name] = Go2rtcStreamIssue.restricted
|
||||
break
|
||||
|
||||
# go2rtc startup resolves sources with the same substitution, and
|
||||
# refuses to start on one it cannot resolve
|
||||
# only sources whose placeholders resolve here are accepted, so the
|
||||
# resolved value checked below is what go2rtc loads at startup
|
||||
try:
|
||||
resolved_source = substitute_frigate_vars(source)
|
||||
except (KeyError, ValueError):
|
||||
restricted.append(name)
|
||||
except KeyError:
|
||||
issues[name] = Go2rtcStreamIssue.unknown_variable
|
||||
break
|
||||
except ValueError:
|
||||
issues[name] = Go2rtcStreamIssue.invalid_placeholder
|
||||
break
|
||||
|
||||
if is_restricted_go2rtc_api_source(
|
||||
source
|
||||
) or is_restricted_go2rtc_api_source(resolved_source):
|
||||
restricted.append(name)
|
||||
issues[name] = Go2rtcStreamIssue.restricted
|
||||
break
|
||||
|
||||
return restricted
|
||||
return issues
|
||||
|
||||
|
||||
def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedProcess:
|
||||
|
||||
Reference in New Issue
Block a user