stop redacting ice servers and break camera path ties by role

The WebRTC player passes /config's ice_servers to RTCPeerConnection, so redacting them sent the placeholder as the TURN account. ICE servers are no longer redacted and the restore code for them is removed. The camera path widget now uses the input's roles to pick between saved paths that mask to the same value, as the backend does on save.
This commit is contained in:
Josh Hawkins
2026-10-18 15:12:01 -05:00
parent 4d89bd3309
commit 39e9612d0c
4 changed files with 57 additions and 154 deletions
@@ -235,4 +235,33 @@ test.describe("camera ffmpeg input source toggle @medium", () => {
await paths.focus();
await expect(paths).toHaveValue(sub);
});
test("inputs with the same masked path each reveal their own path by role", async ({
frigateApp,
}) => {
const record = "rtsp://admin:one@10.0.0.1:554/live";
const detect = "rtsp://viewer:two@10.0.0.1:554/live";
const masked = "rtsp://*:*@10.0.0.1:554/live";
await installRoutes(
frigateApp.page,
[
{ path: masked, roles: ["record"] },
{ path: masked, roles: ["detect"] },
],
[
{ path: record, roles: ["record"] },
{ path: detect, roles: ["detect"] },
],
);
await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door");
const paths = frigateApp.page.getByRole("textbox", { name: "Input path" });
await expect(paths).toHaveCount(2);
await paths.nth(0).focus();
await expect(paths.nth(0)).toHaveValue(record);
await paths.nth(1).focus();
await expect(paths.nth(1)).toHaveValue(detect);
});
});
@@ -1,5 +1,7 @@
import type { WidgetProps } from "@rjsf/utils";
import useSWR from "swr";
import get from "lodash/get";
import isEqual from "lodash/isEqual";
import { useMemo, useState, type FocusEvent } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
@@ -21,6 +23,7 @@ type RawPathsResponse = {
ffmpeg?: {
inputs?: Array<{
path?: string;
roles?: string[];
}>;
};
}
@@ -68,23 +71,32 @@ export function CameraPathWidget(props: WidgetProps) {
const rawValue = typeof value === "string" ? value : "";
// Match by masked value, as the backend restores on save, so deleting or
// reordering inputs never reveals another input's path
const roles =
inputIndex === undefined
? undefined
: get(formContext?.formData, ["inputs", inputIndex, "roles"]);
// Match by masked value with roles breaking a tie, as the backend restores
// on save, so deleting or reordering inputs never reveals another input's path
const rawPath = useMemo(() => {
if (!cameraName || !isMaskedPath(rawValue)) {
return undefined;
}
const matches = new Set(
(rawPaths?.cameras?.[cameraName]?.ffmpeg?.inputs ?? [])
.map((input) => input.path)
.filter(
(path): path is string =>
typeof path === "string" && maskCredentials(path) === rawValue,
),
let matches = (
rawPaths?.cameras?.[cameraName]?.ffmpeg?.inputs ?? []
).filter(
(input): input is { path: string; roles?: string[] } =>
typeof input.path === "string" &&
maskCredentials(input.path) === rawValue,
);
return matches.size === 1 ? [...matches][0] : undefined;
}, [cameraName, rawPaths, rawValue]);
if (new Set(matches.map((input) => input.path)).size > 1) {
matches = matches.filter((input) => isEqual(input.roles, roles));
}
const paths = new Set(matches.map((input) => input.path));
return paths.size === 1 ? [...paths][0] : undefined;
}, [cameraName, rawPaths, rawValue, roles]);
const resolvedValue =
isMaskedPath(rawValue) && rawPath ? rawPath : (rawValue ?? "");