Compare commits

..
1 Commits
Author SHA1 Message Date
dependabot[bot]andGitHub a2628ab6aa Merge 8da8c420f9 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 os
import sys
from pathlib import Path
from typing import Any
from ruamel.yaml import YAML
+8 -16
View File
@@ -254,14 +254,7 @@ rateLimiter = RateLimiter()
def get_remote_addr(request: Request):
# fall back to the direct TCP peer when no proxy chain is present
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(",")))
route = list(reversed(request.headers.get("x-forwarded-for").split(",")))
logger.debug(f"IP Route: {[r for r in route]}")
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)}")
return str(ip)
# every hop in the route was trusted, so fall back to the direct peer
return direct_addr or "127.0.0.1"
# if there wasn't anything in the route, just return the default
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:
@@ -418,12 +416,6 @@ def create_encoded_jwt(user, role, expiration, secret):
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, expiration, secure):
# 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(
key=cookie_name,
value=encoded_jwt,
+11 -9
View File
@@ -269,12 +269,12 @@ async def no_recordings(
cameras = params.cameras
if cameras != "all":
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:
camera_list = list(allowed_cameras)
if not camera_list:
return JSONResponse(content=[])
cameras = allowed_cameras
before = params.before or datetime.datetime.now().timestamp()
after = (
@@ -283,10 +283,12 @@ async def no_recordings(
)
scale = params.scale
clauses = [
(Recordings.end_time >= after) & (Recordings.start_time <= before),
(Recordings.camera << camera_list),
]
clauses = [(Recordings.end_time >= after) & (Recordings.start_time <= before)]
if cameras != "all":
camera_list = cameras.split(",")
clauses.append((Recordings.camera << camera_list))
else:
camera_list = allowed_cameras
# Get recording start times
data: list[Recordings] = (
-45
View File
@@ -13,10 +13,6 @@ from frigate.comms.event_metadata_updater import (
EventMetadataTypeEnum,
)
from frigate.config import CameraConfig, FrigateConfig
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateSubscriber,
)
logger = logging.getLogger(__name__)
@@ -33,11 +29,6 @@ class CameraActivityManager:
self.zone_all_object_counts: dict[str, Counter] = {}
self.zone_active_object_counts: dict[str, Counter] = {}
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():
if not camera_config.enabled_in_config:
@@ -65,40 +56,7 @@ class CameraActivityManager:
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:
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]] = []
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/active", sum(list(active_objects.values())))
def stop(self) -> None:
self.config_subscriber.stop()
class AudioActivityManager:
def __init__(
-2
View File
@@ -397,8 +397,6 @@ class Dispatcher:
comm.publish(topic, payload, retain)
def stop(self) -> None:
self.camera_activity.stop()
for comm in self.comms:
comm.stop()
-52
View File
@@ -475,55 +475,3 @@ class TestHttpMedia(BaseTestHttp):
assert response.status_code == 200
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");
setSearch({ ...search, plus_id: "new_upload" });
mutate(
(key) => isEventsKey(key),
(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);
+2 -10
View File
@@ -47,7 +47,6 @@ type HlsVideoPlayerProps = {
frigateControls?: boolean;
inpointOffset?: number;
onClipEnded?: (currentTime: number) => void;
onClipPrevious?: (diff: number) => void;
onPlayerLoaded?: () => void;
onTimeUpdate?: (time: number) => void;
onPlaying?: () => void;
@@ -75,7 +74,6 @@ export default function HlsVideoPlayer({
frigateControls = true,
inpointOffset = 0,
onClipEnded,
onClipPrevious,
onPlayerLoaded,
onTimeUpdate,
onPlaying,
@@ -341,17 +339,11 @@ export default function HlsVideoPlayer({
onSeek={(diff) => {
const currentTime = videoRef.current?.currentTime;
if (!videoRef.current || currentTime == undefined) {
if (!videoRef.current || !currentTime) {
return;
}
const newTime = currentTime + diff;
if (newTime < 0 && onClipPrevious) {
onClipPrevious(diff);
} else {
videoRef.current.currentTime = Math.max(0, newTime);
}
videoRef.current.currentTime = Math.max(0, currentTime + diff);
}}
onSetPlaybackRate={(rate) => {
setPlaybackRate(rate, true);
@@ -49,7 +49,6 @@ type DynamicVideoPlayerProps = {
onControllerReady: (controller: DynamicVideoController) => void;
onTimestampUpdate?: (timestamp: number) => void;
onClipEnded?: () => void;
onClipPrevious?: (diff: number) => void;
onSeekToTime?: (timestamp: number, play?: boolean) => void;
setFullResolution: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
toggleFullscreen: () => void;
@@ -69,7 +68,6 @@ export default function DynamicVideoPlayer({
onControllerReady,
onTimestampUpdate,
onClipEnded,
onClipPrevious,
onSeekToTime,
setFullResolution,
toggleFullscreen,
@@ -345,7 +343,6 @@ export default function DynamicVideoPlayer({
onTimeUpdate={onTimeUpdate}
onPlayerLoaded={onPlayerLoaded}
onClipEnded={onValidateClipEnd}
onClipPrevious={onClipPrevious}
onSeekToTime={(timestamp, play) => {
if (onSeekToTime) {
onSeekToTime(timestamp, play);
-11
View File
@@ -336,16 +336,6 @@ export function RecordingView({
[currentTimeRange, updateSelectedSegment],
);
const onClipPrevious = useCallback(
(diff: number) => {
manuallySetCurrentTime(
currentTime + diff,
mainControllerRef.current?.isPlaying() ?? false,
);
},
[currentTime, manuallySetCurrentTime],
);
const onShareReviewLink = useCallback(
(timestamp: number) => {
const reviewUrl = createRecordingReviewUrl(location.pathname, {
@@ -912,7 +902,6 @@ export function RecordingView({
);
}}
onClipEnded={onClipEnded}
onClipPrevious={onClipPrevious}
onSeekToTime={manuallySetCurrentTime}
onControllerReady={(controller) => {
mainControllerRef.current = controller;