mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 13:12:48 +03:00
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* restrict go2rtc ffmpeg and ngrok options without arbitrary exec The GO2RTC_ALLOW_ARBITRARY_EXEC checks only looked at go2rtc.streams, so an admin could save a config with a custom ffmpeg.bin or ngrok.command and run any command as the go2rtc user after a restart. go2rtc builds every ffmpeg: source into a command that starts with ffmpeg.bin, and it runs ngrok.command at startup. A custom template also brought back arbitrary ffmpeg arguments, since a stream can reference any template key with #video=<name>. config/save and config/set now reject any added or changed key under go2rtc.ffmpeg or go2rtc.ngrok unless arbitrary exec is enabled. Keys already in the saved config are left alone, the same as stream sources. At startup, ffmpeg.bin is always Frigate's ffmpeg and ngrok is dropped unless arbitrary exec is enabled. Templates already in the config file, such as the documented vaapi global, keep working. * reject ffmpeg paths with spaces, quotes or shell characters ffmpeg.path was used as-is when absolute, and several ffmpeg commands are built as strings and split on spaces before running. A path like `/bin/sh -c <cmd>;#` ran a shell command instead of ffmpeg in the preview and export processes, and in go2rtc through the birdseye restream exec source. The path is now validated as a version alias or an absolute path of plain characters, so config/save and config/set reject an unsafe value. resolve_ffmpeg_path falls back to the default ffmpeg for one, and the go2rtc startup script prints an error.
187 lines
7.2 KiB
Python
187 lines
7.2 KiB
Python
"""Creates a go2rtc config file."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from ruamel.yaml import YAML
|
|
|
|
sys.path.insert(0, "/opt/frigate")
|
|
from frigate.config.env import substitute_frigate_vars
|
|
from frigate.const import BIRDSEYE_PIPE
|
|
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
|
from frigate.util.config import (
|
|
find_config_file,
|
|
is_valid_ffmpeg_path,
|
|
resolve_ffmpeg_path,
|
|
)
|
|
from frigate.util.services import (
|
|
apply_go2rtc_ffmpeg_config,
|
|
is_go2rtc_arbitrary_exec_allowed,
|
|
is_restricted_go2rtc_source,
|
|
)
|
|
|
|
sys.path.remove("/opt/frigate")
|
|
|
|
yaml = YAML()
|
|
|
|
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
|
|
# read docker secret files as env vars too
|
|
if os.path.isdir("/run/secrets"):
|
|
for secret_file in os.listdir("/run/secrets"):
|
|
if secret_file.startswith("FRIGATE_"):
|
|
FRIGATE_ENV_VARS[secret_file] = (
|
|
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
|
|
)
|
|
|
|
config_file = find_config_file()
|
|
|
|
try:
|
|
with open(config_file) as f:
|
|
raw_config = f.read()
|
|
|
|
if config_file.endswith((".yaml", ".yml")):
|
|
config: dict[str, Any] = yaml.load(raw_config)
|
|
elif config_file.endswith(".json"):
|
|
config: dict[str, Any] = json.loads(raw_config)
|
|
except FileNotFoundError:
|
|
config: dict[str, Any] = {}
|
|
|
|
go2rtc_config: dict[str, Any] = config.get("go2rtc", {})
|
|
|
|
# Need to enable CORS for go2rtc so the frigate integration / card work automatically
|
|
if go2rtc_config.get("api") is None:
|
|
go2rtc_config["api"] = {"origin": "*"}
|
|
elif go2rtc_config["api"].get("origin") is None:
|
|
go2rtc_config["api"]["origin"] = "*"
|
|
|
|
# Need to set default location for HA config
|
|
if go2rtc_config.get("hass") is None:
|
|
go2rtc_config["hass"] = {"config": "/homeassistant"}
|
|
|
|
# we want to ensure that logs are easy to read
|
|
if go2rtc_config.get("log") is None:
|
|
go2rtc_config["log"] = {"format": "text"}
|
|
elif go2rtc_config["log"].get("format") is None:
|
|
go2rtc_config["log"]["format"] = "text"
|
|
|
|
# ensure there is a default webrtc config
|
|
if go2rtc_config.get("webrtc") is None:
|
|
go2rtc_config["webrtc"] = {}
|
|
|
|
if go2rtc_config["webrtc"].get("candidates") is None:
|
|
default_candidates = []
|
|
# use internal candidate if it was discovered when running through the add-on
|
|
internal_candidate = os.environ.get("FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL")
|
|
if internal_candidate is not None:
|
|
default_candidates.append(internal_candidate)
|
|
# should set default stun server so webrtc can work
|
|
default_candidates.append("stun:8555")
|
|
|
|
go2rtc_config["webrtc"]["candidates"] = default_candidates
|
|
|
|
if go2rtc_config.get("rtsp", {}).get("username") is not None:
|
|
go2rtc_config["rtsp"]["username"] = substitute_frigate_vars(
|
|
go2rtc_config["rtsp"]["username"]
|
|
)
|
|
|
|
if go2rtc_config.get("rtsp", {}).get("password") is not None:
|
|
go2rtc_config["rtsp"]["password"] = substitute_frigate_vars(
|
|
go2rtc_config["rtsp"]["password"]
|
|
)
|
|
|
|
# ensure ffmpeg path is set correctly
|
|
path = config.get("ffmpeg", {}).get("path", "default")
|
|
if not is_valid_ffmpeg_path(path):
|
|
print(
|
|
"[ERROR] ffmpeg path contains spaces, quotes or shell characters, using the default ffmpeg instead."
|
|
)
|
|
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
|
|
|
|
for key in apply_go2rtc_ffmpeg_config(go2rtc_config, ffmpeg_path):
|
|
print(
|
|
f"[ERROR] go2rtc option '{key}' was ignored because it is disabled by default for security. "
|
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec."
|
|
)
|
|
|
|
|
|
for name in list(go2rtc_config.get("streams", {})):
|
|
stream = go2rtc_config["streams"][name]
|
|
|
|
if isinstance(stream, str):
|
|
try:
|
|
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
|
|
if is_restricted_go2rtc_source(formatted_stream):
|
|
print(
|
|
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
|
)
|
|
del go2rtc_config["streams"][name]
|
|
continue
|
|
go2rtc_config["streams"][name] = formatted_stream
|
|
except KeyError as e:
|
|
print(
|
|
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
|
)
|
|
sys.exit(e)
|
|
|
|
elif isinstance(stream, list):
|
|
filtered_streams = []
|
|
for i, stream_item in enumerate(stream):
|
|
try:
|
|
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
|
|
if is_restricted_go2rtc_source(formatted_stream):
|
|
print(
|
|
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
|
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
|
)
|
|
continue
|
|
|
|
filtered_streams.append(formatted_stream)
|
|
except KeyError as e:
|
|
print(
|
|
"[ERROR] Invalid substitution found, see https://docs.frigate.video/configuration/restream#advanced-restream-configurations for more info."
|
|
)
|
|
sys.exit(e)
|
|
|
|
if filtered_streams:
|
|
go2rtc_config["streams"][name] = filtered_streams
|
|
else:
|
|
print(
|
|
f"[ERROR] Stream '{name}' was removed because all sources were restricted (echo/expr/exec). "
|
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
|
)
|
|
del go2rtc_config["streams"][name]
|
|
|
|
elif isinstance(stream, dict):
|
|
# The map form ({"url": ...}) lets go2rtc resolve the source
|
|
# recursively, so it is effectively a dynamic way to generate the URL
|
|
# for a stream. That can only be backed by an exec source, so it cannot
|
|
# be allowed unless arbitrary exec is explicitly enabled. When it is
|
|
# enabled, leave the map untouched for go2rtc to resolve.
|
|
if not is_go2rtc_arbitrary_exec_allowed():
|
|
print(
|
|
f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. "
|
|
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
|
|
)
|
|
del go2rtc_config["streams"][name]
|
|
continue
|
|
|
|
# add birdseye restream stream if enabled
|
|
if config.get("birdseye", {}).get("restream", False):
|
|
birdseye: dict[str, Any] = config.get("birdseye")
|
|
|
|
input = f"-f rawvideo -pix_fmt yuv420p -video_size {birdseye.get('width', 1280)}x{birdseye.get('height', 720)} -r 10 -i {BIRDSEYE_PIPE}"
|
|
ffmpeg_cmd = f"exec:{parse_preset_hardware_acceleration_encode(ffmpeg_path, config.get('ffmpeg', {}).get('hwaccel_args', ''), input, '-rtsp_transport tcp -f rtsp {output}')}"
|
|
|
|
if go2rtc_config.get("streams"):
|
|
go2rtc_config["streams"]["birdseye"] = ffmpeg_cmd
|
|
else:
|
|
go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd}
|
|
|
|
# Write go2rtc_config to /dev/shm/go2rtc.yaml
|
|
with open("/dev/shm/go2rtc.yaml", "w") as f:
|
|
yaml.dump(go2rtc_config, f)
|