mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-11 08:03:01 +03:00
Add on-demand Frigate+ API key validation (#24918)
* Add on-demand Frigate+ API key validation The Frigate+ settings page reported the API key as validated whenever it matched the expected format, without ever checking it with the server. - Clarify the status text to say the key matches the expected format - Add PlusApi.validate_key(), which requests a token from Frigate+ and reports the key as valid, invalid, unreachable, or not set; server errors and timeouts are not reported as an invalid key - Add an admin-only POST /plus/validate endpoint and regenerate the API spec - Add a "Validate with Frigate+" button that shows the result inline Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde * Validate Frigate+ key asynchronously and keep cached token on bad replies - Use aiohttp for the validation request so the endpoint awaits it instead of blocking a worker thread - Only replace the cached token when the reply has a string accessToken and numeric expires; otherwise report unreachable so an unexpected response can't break later Frigate+ requests - Test against a local aiohttp server, covering timeouts, connection failures, non-JSON replies and unusable token shapes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde * Honor proxy settings when validating the Frigate+ key aiohttp ignores HTTP(S)_PROXY unless trust_env is set, so validation reported Frigate+ as unreachable on installs that need a proxy while other Frigate+ requests still worked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
52d469ed20
commit
81cf7be89a
+81
-1
@@ -3,9 +3,11 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
import aiohttp
|
||||
import cv2
|
||||
import requests
|
||||
from numpy import ndarray
|
||||
@@ -17,6 +19,17 @@ from frigate.const import MODEL_CACHE_DIR, PLUS_API_HOST, PLUS_ENV_VAR
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PLUS_VALIDATE_TIMEOUT = 10
|
||||
|
||||
|
||||
class PlusKeyStatus(str, Enum):
|
||||
"""Result of validating a Frigate+ API key with the server."""
|
||||
|
||||
valid = "valid"
|
||||
invalid = "invalid"
|
||||
unreachable = "unreachable"
|
||||
not_set = "not_set"
|
||||
|
||||
|
||||
def add_hailo_alias(model_info: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Name the hailo detector by its current key as well as its old one.
|
||||
@@ -72,6 +85,19 @@ def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
|
||||
return jpg_bytes if isinstance(jpg_bytes, bytes) else b""
|
||||
|
||||
|
||||
def _is_usable_token(token_data: Any) -> TypeGuard[dict[str, Any]]:
|
||||
"""Whether a Frigate+ token response has a usable token and expiry."""
|
||||
if not isinstance(token_data, dict):
|
||||
return False
|
||||
|
||||
expires = token_data.get("expires")
|
||||
return (
|
||||
isinstance(token_data.get("accessToken"), str)
|
||||
and isinstance(expires, (int, float))
|
||||
and not isinstance(expires, bool)
|
||||
)
|
||||
|
||||
|
||||
class PlusApi:
|
||||
def __init__(self) -> None:
|
||||
self.host = PLUS_API_HOST
|
||||
@@ -153,6 +179,60 @@ class PlusApi:
|
||||
def is_active(self) -> bool:
|
||||
return self._is_active
|
||||
|
||||
async def validate_key(self) -> PlusKeyStatus:
|
||||
"""Check the configured API key against the Frigate+ server.
|
||||
|
||||
The key format is checked at startup, but only the server can tell
|
||||
whether the key is actually valid. This requests a fresh token so the
|
||||
result reflects the key's current state rather than a cached token.
|
||||
|
||||
Returns:
|
||||
The key's status as reported by the Frigate+ server
|
||||
"""
|
||||
if self.key is None:
|
||||
return PlusKeyStatus.not_set
|
||||
|
||||
key_id, _, secret = self.key.partition(":")
|
||||
|
||||
# Use a single request with a timeout instead of the retrying session,
|
||||
# since this is user initiated and should fail fast when unreachable.
|
||||
# trust_env honors proxy settings, matching the other Frigate+ requests.
|
||||
try:
|
||||
async with aiohttp.ClientSession(
|
||||
timeout=aiohttp.ClientTimeout(total=PLUS_VALIDATE_TIMEOUT),
|
||||
trust_env=True,
|
||||
) as session:
|
||||
async with session.get(
|
||||
f"{self.host}/v1/auth/token",
|
||||
auth=aiohttp.BasicAuth(key_id, secret),
|
||||
) as r:
|
||||
status = r.status
|
||||
token_data = await r.json(content_type=None) if r.ok else None
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
logger.warning("Unable to reach Frigate+ to validate API key: %s", err)
|
||||
return PlusKeyStatus.unreachable
|
||||
except ValueError:
|
||||
logger.warning("Invalid response validating Frigate+ API key")
|
||||
return PlusKeyStatus.unreachable
|
||||
|
||||
if status in (401, 403):
|
||||
return PlusKeyStatus.invalid
|
||||
|
||||
if status >= 400:
|
||||
logger.warning(
|
||||
"Unexpected response validating Frigate+ API key: %s", status
|
||||
)
|
||||
return PlusKeyStatus.unreachable
|
||||
|
||||
# Keep the cached token unless the reply is a usable one, so an
|
||||
# unexpected response can't break later Frigate+ requests
|
||||
if not _is_usable_token(token_data):
|
||||
logger.warning("Invalid response validating Frigate+ API key")
|
||||
return PlusKeyStatus.unreachable
|
||||
|
||||
self._token_data = token_data
|
||||
return PlusKeyStatus.valid
|
||||
|
||||
def upload_image(self, image: ndarray, camera: str) -> str:
|
||||
r = self._get("image/signed_urls")
|
||||
if not r.ok:
|
||||
|
||||
Reference in New Issue
Block a user