Compare commits

..
1 Commits
Author SHA1 Message Date
dependabot[bot]andGitHub af5dc8a85f Merge f7218566ec into ea131e1663 2026-06-29 08:36:06 +02:00
10 changed files with 23 additions and 150 deletions
@@ -3,7 +3,6 @@
import json import json
import os import os
import sys import sys
from pathlib import Path
from typing import Any from typing import Any
from ruamel.yaml import YAML from ruamel.yaml import YAML
+8 -16
View File
@@ -254,14 +254,7 @@ rateLimiter = RateLimiter()
def get_remote_addr(request: Request): def get_remote_addr(request: Request):
# fall back to the direct TCP peer when no proxy chain is present route = list(reversed(request.headers.get("x-forwarded-for").split(",")))
direct_addr = request.client.host if request.client else None
forwarded_for = request.headers.get("x-forwarded-for")
if not forwarded_for:
return direct_addr or "127.0.0.1"
route = list(reversed(forwarded_for.split(",")))
logger.debug(f"IP Route: {[r for r in route]}") logger.debug(f"IP Route: {[r for r in route]}")
trusted_proxies = [] trusted_proxies = []
for proxy in request.app.frigate_config.auth.trusted_proxies: for proxy in request.app.frigate_config.auth.trusted_proxies:
@@ -298,8 +291,13 @@ def get_remote_addr(request: Request):
logger.debug(f"First untrusted IP: {str(ip)}") logger.debug(f"First untrusted IP: {str(ip)}")
return str(ip) return str(ip)
# every hop in the route was trusted, so fall back to the direct peer # if there wasn't anything in the route, just return the default
return direct_addr or "127.0.0.1" remote_addr = None
if hasattr(request, "remote_addr"):
remote_addr = request.remote_addr
return remote_addr or "127.0.0.1"
def _cleanup_first_load_seen() -> None: def _cleanup_first_load_seen() -> None:
@@ -418,12 +416,6 @@ def create_encoded_jwt(user, role, expiration, secret):
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure): def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
# TODO: ideally this would set secure as well, but that requires TLS # TODO: ideally this would set secure as well, but that requires TLS
# SameSite is intentionally left unset (browsers default to Lax). Setting
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
# iframes, breaking embedded views such as the Home Assistant Frigate card.
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
# cross-origin pages cannot set without a CORS preflight that Frigate never
# grants (see check_csrf in api/fastapi_app.py).
response.set_cookie( response.set_cookie(
key=cookie_name, key=cookie_name,
value=encoded_jwt, value=encoded_jwt,
+11 -9
View File
@@ -269,12 +269,12 @@ async def no_recordings(
cameras = params.cameras cameras = params.cameras
if cameras != "all": if cameras != "all":
requested = set(unquote(cameras).split(",")) requested = set(unquote(cameras).split(","))
camera_list = list(requested.intersection(allowed_cameras)) filtered = requested.intersection(allowed_cameras)
if not filtered:
return JSONResponse(content=[])
cameras = ",".join(filtered)
else: else:
camera_list = list(allowed_cameras) cameras = allowed_cameras
if not camera_list:
return JSONResponse(content=[])
before = params.before or datetime.datetime.now().timestamp() before = params.before or datetime.datetime.now().timestamp()
after = ( after = (
@@ -283,10 +283,12 @@ async def no_recordings(
) )
scale = params.scale scale = params.scale
clauses = [ clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
(Recordings.end_time >= after) & (Recordings.start_time <= before), if cameras != "all":
(Recordings.camera << camera_list), camera_list = cameras.split(",")
] clauses.append((Recordings.camera << camera_list))
else:
camera_list = allowed_cameras
# Get recording start times # Get recording start times
data: list[Recordings] = ( data: list[Recordings] = (
-45
View File
@@ -13,10 +13,6 @@ from frigate.comms.event_metadata_updater import (
EventMetadataTypeEnum, EventMetadataTypeEnum,
) )
from frigate.config import CameraConfig, FrigateConfig from frigate.config import CameraConfig, FrigateConfig
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,11 +29,6 @@ class CameraActivityManager:
self.zone_all_object_counts: dict[str, Counter] = {} self.zone_all_object_counts: dict[str, Counter] = {}
self.zone_active_object_counts: dict[str, Counter] = {} self.zone_active_object_counts: dict[str, Counter] = {}
self.all_zone_labels: dict[str, set[str]] = {} self.all_zone_labels: dict[str, set[str]] = {}
self.config_subscriber = CameraConfigUpdateSubscriber(
config,
config.cameras,
[CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.objects],
)
for camera_config in config.cameras.values(): for camera_config in config.cameras.values():
if not camera_config.enabled_in_config: if not camera_config.enabled_in_config:
@@ -65,40 +56,7 @@ class CameraActivityManager:
else camera_config.objects.track else camera_config.objects.track
) )
def __rebuild_zone_labels(self) -> None:
"""Rebuild zone label tracking after a runtime zones/objects change."""
new_zone_labels: dict[str, set[str]] = {}
for camera_config in self.config.cameras.values():
if not camera_config.enabled_in_config or camera_config.name is None:
continue
for zone, zone_config in camera_config.zones.items():
new_zone_labels.setdefault(zone, set()).update(
zone_config.objects
if zone_config.objects
else camera_config.objects.track
)
# drop counters for zones that no longer exist
for zone in list(self.zone_all_object_counts.keys()):
if zone not in new_zone_labels:
self.zone_all_object_counts.pop(zone, None)
self.zone_active_object_counts.pop(zone, None)
# ensure counters exist for new zones so the first count is published
for zone in new_zone_labels:
self.zone_all_object_counts.setdefault(zone, Counter())
self.zone_active_object_counts.setdefault(zone, Counter())
self.all_zone_labels = new_zone_labels
def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None: def update_activity(self, new_activity: dict[str, dict[str, Any]]) -> None:
updated_topics = self.config_subscriber.check_for_updates()
if "zones" in updated_topics or "objects" in updated_topics:
self.__rebuild_zone_labels()
all_objects: list[dict[str, Any]] = [] all_objects: list[dict[str, Any]] = []
for camera in new_activity.keys(): for camera in new_activity.keys():
@@ -203,9 +161,6 @@ class CameraActivityManager:
self.publish(f"{camera}/all", sum(list(all_objects.values()))) self.publish(f"{camera}/all", sum(list(all_objects.values())))
self.publish(f"{camera}/all/active", sum(list(active_objects.values()))) self.publish(f"{camera}/all/active", sum(list(active_objects.values())))
def stop(self) -> None:
self.config_subscriber.stop()
class AudioActivityManager: class AudioActivityManager:
def __init__( def __init__(
-2
View File
@@ -397,8 +397,6 @@ class Dispatcher:
comm.publish(topic, payload, retain) comm.publish(topic, payload, retain)
def stop(self) -> None: def stop(self) -> None:
self.camera_activity.stop()
for comm in self.comms: for comm in self.comms:
comm.stop() comm.stop()
-52
View File
@@ -475,55 +475,3 @@ class TestHttpMedia(BaseTestHttp):
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == [] assert response.json() == []
def test_recordings_unavailable_cameras_all_scopes_to_allowed_cameras(self):
"""cameras=all must not error and must only consider allowed cameras.
allowed_cameras is mocked to ["front_door"]. A back_door recording that
would otherwise fill the gap must be ignored, and the request must not
500 the way it did when cameras was reassigned to a list.
"""
with AuthTestClient(self.app) as client:
# front_door has a 20s gap (1010-1030).
Recordings.insert(
id="front_a",
path="/media/recordings/front_a.mp4",
camera="front_door",
start_time=1000,
end_time=1010,
duration=10,
motion=0,
).execute()
Recordings.insert(
id="front_b",
path="/media/recordings/front_b.mp4",
camera="front_door",
start_time=1030,
end_time=1040,
duration=10,
motion=0,
).execute()
# back_door is not in allowed_cameras; its full-window coverage must
# not mask the front_door gap.
Recordings.insert(
id="back_a",
path="/media/recordings/back_a.mp4",
camera="back_door",
start_time=1000,
end_time=1040,
duration=40,
motion=0,
).execute()
response = client.get(
"/recordings/unavailable",
params={
"after": 1000,
"before": 1040,
"scale": 5,
"cameras": "all",
},
)
assert response.status_code == 200
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
@@ -1263,6 +1263,7 @@ function ObjectDetailsTab({
} }
setState("submitted"); setState("submitted");
setSearch({ ...search, plus_id: "new_upload" });
mutate( mutate(
(key) => isEventsKey(key), (key) => isEventsKey(key),
(currentData: SearchResult[][] | SearchResult[] | undefined) => (currentData: SearchResult[][] | SearchResult[] | undefined) =>
@@ -1285,7 +1286,7 @@ function ObjectDetailsTab({
); );
} }
}, },
[search, mutate, mapSearchResults, isEventsKey, t], [search, mutate, mapSearchResults, setSearch, isEventsKey, t],
); );
const popoverContainerRef = useRef<HTMLDivElement | null>(null); const popoverContainerRef = useRef<HTMLDivElement | null>(null);
+2 -10
View File
@@ -47,7 +47,6 @@ type HlsVideoPlayerProps = {
frigateControls?: boolean; frigateControls?: boolean;
inpointOffset?: number; inpointOffset?: number;
onClipEnded?: (currentTime: number) => void; onClipEnded?: (currentTime: number) => void;
onClipPrevious?: (diff: number) => void;
onPlayerLoaded?: () => void; onPlayerLoaded?: () => void;
onTimeUpdate?: (time: number) => void; onTimeUpdate?: (time: number) => void;
onPlaying?: () => void; onPlaying?: () => void;
@@ -75,7 +74,6 @@ export default function HlsVideoPlayer({
frigateControls = true, frigateControls = true,
inpointOffset = 0, inpointOffset = 0,
onClipEnded, onClipEnded,
onClipPrevious,
onPlayerLoaded, onPlayerLoaded,
onTimeUpdate, onTimeUpdate,
onPlaying, onPlaying,
@@ -341,17 +339,11 @@ export default function HlsVideoPlayer({
onSeek={(diff) => { onSeek={(diff) => {
const currentTime = videoRef.current?.currentTime; const currentTime = videoRef.current?.currentTime;
if (!videoRef.current || currentTime == undefined) { if (!videoRef.current || !currentTime) {
return; return;
} }
const newTime = currentTime + diff; videoRef.current.currentTime = Math.max(0, currentTime + diff);
if (newTime < 0 && onClipPrevious) {
onClipPrevious(diff);
} else {
videoRef.current.currentTime = Math.max(0, newTime);
}
}} }}
onSetPlaybackRate={(rate) => { onSetPlaybackRate={(rate) => {
setPlaybackRate(rate, true); setPlaybackRate(rate, true);
@@ -49,7 +49,6 @@ type DynamicVideoPlayerProps = {
onControllerReady: (controller: DynamicVideoController) => void; onControllerReady: (controller: DynamicVideoController) => void;
onTimestampUpdate?: (timestamp: number) => void; onTimestampUpdate?: (timestamp: number) => void;
onClipEnded?: () => void; onClipEnded?: () => void;
onClipPrevious?: (diff: number) => void;
onSeekToTime?: (timestamp: number, play?: boolean) => void; onSeekToTime?: (timestamp: number, play?: boolean) => void;
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>; setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
toggleFullscreen: () => void; toggleFullscreen: () => void;
@@ -69,7 +68,6 @@ export default function DynamicVideoPlayer({
onControllerReady, onControllerReady,
onTimestampUpdate, onTimestampUpdate,
onClipEnded, onClipEnded,
onClipPrevious,
onSeekToTime, onSeekToTime,
setFullResolution, setFullResolution,
toggleFullscreen, toggleFullscreen,
@@ -345,7 +343,6 @@ export default function DynamicVideoPlayer({
onTimeUpdate={onTimeUpdate} onTimeUpdate={onTimeUpdate}
onPlayerLoaded={onPlayerLoaded} onPlayerLoaded={onPlayerLoaded}
onClipEnded={onValidateClipEnd} onClipEnded={onValidateClipEnd}
onClipPrevious={onClipPrevious}
onSeekToTime={(timestamp, play) => { onSeekToTime={(timestamp, play) => {
if (onSeekToTime) { if (onSeekToTime) {
onSeekToTime(timestamp, play); onSeekToTime(timestamp, play);
-11
View File
@@ -336,16 +336,6 @@ export function RecordingView({
[currentTimeRange, updateSelectedSegment], [currentTimeRange, updateSelectedSegment],
); );
const onClipPrevious = useCallback(
(diff: number) => {
manuallySetCurrentTime(
currentTime + diff,
mainControllerRef.current?.isPlaying() ?? false,
);
},
[currentTime, manuallySetCurrentTime],
);
const onShareReviewLink = useCallback( const onShareReviewLink = useCallback(
(timestamp: number) => { (timestamp: number) => {
const reviewUrl = createRecordingReviewUrl(location.pathname, { const reviewUrl = createRecordingReviewUrl(location.pathname, {
@@ -912,7 +902,6 @@ export function RecordingView({
); );
}} }}
onClipEnded={onClipEnded} onClipEnded={onClipEnded}
onClipPrevious={onClipPrevious}
onSeekToTime={manuallySetCurrentTime} onSeekToTime={manuallySetCurrentTime}
onControllerReady={(controller) => { onControllerReady={(controller) => {
mainControllerRef.current = controller; mainControllerRef.current = controller;