mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-26 02:28:59 +03:00
* add a notice registry and System Health tab Problems Frigate detects on its own (ffmpeg crash loops, stuck detectors, failed model downloads, recordings deleted before their retention period) only ever existed as log lines. This adds a `NoticeRegistry` in the main process backed by two tables, an `update_notice` IPC topic so producers in other processes can reach it through the dispatcher, an admin-only API and websocket topic, and a Health tab that lists them. Kinds declare their own mode, severity, and category in one place: state notices are resolved by their producer, event notices are dismissed by the user. * treat a prerelease as behind its final release * fix notices clearing early * rename menu items and update docs * don't resolve the update notice on a failed version lookup
200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
import asyncio
|
|
import logging
|
|
import re
|
|
|
|
from fastapi import Depends, FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
from joserfc.jwk import OctKey
|
|
from playhouse.sqliteq import SqliteQueueDatabase
|
|
from slowapi import _rate_limit_exceeded_handler
|
|
from slowapi.errors import RateLimitExceeded
|
|
from slowapi.middleware import SlowAPIMiddleware
|
|
from starlette_context import middleware, plugins
|
|
from starlette_context.plugins import Plugin
|
|
|
|
from frigate.api import app as main_app
|
|
from frigate.api import (
|
|
auth,
|
|
camera,
|
|
chat,
|
|
classification,
|
|
debug_replay,
|
|
event,
|
|
export,
|
|
hardware,
|
|
media,
|
|
motion_search,
|
|
notices,
|
|
notification,
|
|
preview,
|
|
record,
|
|
review,
|
|
)
|
|
from frigate.api.auth import get_jwt_secret, limiter, require_admin_by_default
|
|
from frigate.comms.dispatcher import Dispatcher
|
|
from frigate.comms.event_metadata_updater import (
|
|
EventMetadataPublisher,
|
|
)
|
|
from frigate.config import FrigateConfig
|
|
from frigate.config.camera.updater import CameraConfigUpdatePublisher
|
|
from frigate.config.holder import ConfigHolder
|
|
from frigate.config.profile_manager import ProfileManager
|
|
from frigate.debug_replay import DebugReplayManager, debug_replay_auto_stop_watchdog
|
|
from frigate.embeddings import EmbeddingsContext
|
|
from frigate.genai import GenAIClientManager
|
|
from frigate.notices.registry import NoticeRegistry
|
|
from frigate.ptz.onvif import OnvifController
|
|
from frigate.stats.emitter import StatsEmitter
|
|
from frigate.storage import StorageMaintainer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def check_csrf(request: Request) -> bool:
|
|
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
|
return True
|
|
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
# Used to retrieve the remote-user header: https://starlette-context.readthedocs.io/en/latest/plugins.html#easy-mode
|
|
class RemoteUserPlugin(Plugin):
|
|
key = "Remote-User"
|
|
|
|
|
|
def create_fastapi_app(
|
|
frigate_config: FrigateConfig,
|
|
database: SqliteQueueDatabase,
|
|
embeddings: EmbeddingsContext | None,
|
|
detected_frames_processor,
|
|
storage_maintainer: StorageMaintainer,
|
|
onvif: OnvifController,
|
|
stats_emitter: StatsEmitter,
|
|
event_metadata_updater: EventMetadataPublisher,
|
|
config_publisher: CameraConfigUpdatePublisher,
|
|
replay_manager: DebugReplayManager,
|
|
dispatcher: Dispatcher | None = None,
|
|
profile_manager: ProfileManager | None = None,
|
|
enforce_default_admin: bool = True,
|
|
config_holder: ConfigHolder | None = None,
|
|
notice_registry: NoticeRegistry | None = None,
|
|
):
|
|
logger.info("Starting FastAPI app")
|
|
app = FastAPI(
|
|
debug=False,
|
|
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
|
dependencies=[Depends(require_admin_by_default())]
|
|
if enforce_default_admin
|
|
else [],
|
|
)
|
|
|
|
# update the request_address with the x-forwarded-for header from nginx
|
|
# https://starlette-context.readthedocs.io/en/latest/plugins.html#forwarded-for
|
|
app.add_middleware(
|
|
middleware.ContextMiddleware,
|
|
plugins=(plugins.ForwardedForPlugin(),),
|
|
)
|
|
|
|
# Middleware to connect to DB before and close connection after request
|
|
# https://github.com/fastapi/full-stack-fastapi-template/issues/224#issuecomment-737423886
|
|
# https://fastapi.tiangolo.com/tutorial/middleware/#before-and-after-the-response
|
|
@app.middleware("http")
|
|
async def frigate_middleware(request: Request, call_next):
|
|
# Before request
|
|
if not check_csrf(request):
|
|
return JSONResponse(
|
|
content={"success": False, "message": "Missing CSRF header"},
|
|
status_code=401,
|
|
)
|
|
|
|
if database.is_closed():
|
|
database.connect()
|
|
|
|
response = await call_next(request)
|
|
|
|
# After request https://stackoverflow.com/a/75487519
|
|
if not database.is_closed():
|
|
database.close()
|
|
return response
|
|
|
|
@app.on_event("startup")
|
|
async def startup():
|
|
logger.info("FastAPI started")
|
|
asyncio.create_task(
|
|
debug_replay_auto_stop_watchdog(
|
|
replay_manager, frigate_config, config_publisher
|
|
)
|
|
)
|
|
|
|
# Rate limiter (used for login endpoint)
|
|
if frigate_config.auth.failed_login_rate_limit is None:
|
|
limiter.enabled = False
|
|
else:
|
|
auth.rateLimiter.set_limit(frigate_config.auth.failed_login_rate_limit)
|
|
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
app.add_middleware(SlowAPIMiddleware)
|
|
|
|
# Routes
|
|
# Order of include_router matters: https://fastapi.tiangolo.com/tutorial/path-params/#order-matters
|
|
app.include_router(auth.router)
|
|
app.include_router(camera.router)
|
|
app.include_router(chat.router)
|
|
app.include_router(classification.router)
|
|
app.include_router(review.router)
|
|
app.include_router(main_app.router)
|
|
app.include_router(preview.router)
|
|
app.include_router(notification.router)
|
|
app.include_router(export.router)
|
|
app.include_router(hardware.router)
|
|
app.include_router(notices.router)
|
|
app.include_router(event.router)
|
|
app.include_router(media.router)
|
|
app.include_router(motion_search.router)
|
|
app.include_router(record.router)
|
|
app.include_router(debug_replay.router)
|
|
# App Properties
|
|
app.frigate_config = frigate_config
|
|
# snapshot the port nginx bound at startup, the live config can be swapped
|
|
app.auth_internal_port = frigate_config.networking.listen.internal_port
|
|
app.genai_manager = GenAIClientManager(frigate_config)
|
|
app.embeddings = embeddings
|
|
app.detected_frames_processor = detected_frames_processor
|
|
app.storage_maintainer = storage_maintainer
|
|
app.camera_error_image = None
|
|
app.onvif = onvif
|
|
app.stats_emitter = stats_emitter
|
|
app.notice_registry = notice_registry
|
|
app.event_metadata_updater = event_metadata_updater
|
|
app.config_publisher = config_publisher
|
|
app.replay_manager = replay_manager
|
|
app.dispatcher = dispatcher
|
|
app.profile_manager = profile_manager
|
|
app.config_holder = config_holder
|
|
|
|
if frigate_config.auth.enabled:
|
|
secret = get_jwt_secret()
|
|
key_bytes = None
|
|
if isinstance(secret, str):
|
|
# If the secret looks like hex (e.g., generated by secrets.token_hex), use raw bytes
|
|
if len(secret) % 2 == 0 and re.fullmatch(r"[0-9a-fA-F]+", secret or ""):
|
|
try:
|
|
key_bytes = bytes.fromhex(secret)
|
|
except ValueError:
|
|
key_bytes = secret.encode("utf-8")
|
|
else:
|
|
key_bytes = secret.encode("utf-8")
|
|
elif isinstance(secret, (bytes, bytearray)):
|
|
key_bytes = bytes(secret)
|
|
else:
|
|
key_bytes = str(secret).encode("utf-8")
|
|
|
|
app.jwt_token = OctKey.import_key(key_bytes)
|
|
else:
|
|
app.jwt_token = None
|
|
|
|
return app
|