mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-26 08:24:42 +03:00
* reject push endpoints that crash the vapid audience parse The validator accepted hostnames like "." so https://./x passed, and check_registrations then raised ValueError slicing the endpoint, killing the dispatcher's IPC thread. The validator now rejects empty hostname labels and lives in webpush.py, the audience comes from urlparse, and _build_web_pushers skips stored rows whose endpoint fails validation. * keep the ipc thread alive when a handler raises An exception from any dispatcher handler escaped InterProcessCommunicator.read, which only caught ZMQError. The REP thread died owing a reply, so every InterProcessRequestor blocked forever. read now logs the error and replies with an empty response, and Dispatcher.publish and publish_local catch per communicator so one failing communicator can't take down the rest. * stop push sends from reaching internal addresses pywebpush posts through requests with redirects enabled, so an endpoint could answer 307 to http://127.0.0.1:5000/api/restart and the sender would follow it into the internal admin port on every alert. Pushers now use a session that never follows redirects, and each send first resolves the endpoint host and skips it unless every address is global. * keep the previous region grid when the refresh request fails The IPC reader now replies [] when a handler raises, and process_frames assigned that reply to region_grid unconditionally. The next region lookup then indexed into an empty list and killed the camera's detect process. An empty reply now keeps the current grid until the next daily refresh. * drop the send-time public address check Refusing non-global resolved addresses broke Tailscale (100.64.0.0/10), split-horizon DNS and HTTPS_PROXY-only setups, and added little once redirects are no longer followed and TLS verification stays on. * only skip stored push rows that break the vapid audience Running the full registration validator over stored rows dropped self-hosted endpoints registered before 0.18 with a non-443 port or a bare / path. _build_web_pushers now skips only non-dict rows, rows with no endpoint, unparsable endpoints and hosts with empty labels, and the warning names the host so the row can be found. Registration still uses the full validator.
130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
"""Notification apis."""
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
from cryptography.hazmat.primitives import serialization
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import JSONResponse
|
|
from py_vapid import Vapid01, utils
|
|
|
|
from frigate.api.auth import allow_any_authenticated
|
|
from frigate.api.defs.tags import Tags
|
|
from frigate.comms.webpush import validate_push_endpoint
|
|
from frigate.const import CONFIG_DIR
|
|
from frigate.models import User
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(tags=[Tags.notifications])
|
|
|
|
|
|
def _validate_subscription(sub: Any) -> str | None:
|
|
"""Return a reason the subscription is unusable, or None when it is valid."""
|
|
if not isinstance(sub, dict):
|
|
return "subscription must be an object"
|
|
|
|
reason = validate_push_endpoint(sub.get("endpoint"))
|
|
|
|
if reason:
|
|
return reason
|
|
|
|
keys = sub.get("keys")
|
|
|
|
if not isinstance(keys, dict):
|
|
return "subscription must include keys"
|
|
|
|
# WebPusher raises on a missing key, which would break every send for the
|
|
# user rather than just this registration
|
|
for name in ("p256dh", "auth"):
|
|
value = keys.get(name)
|
|
|
|
if not isinstance(value, str) or not value:
|
|
return f"subscription keys must include {name}"
|
|
|
|
return None
|
|
|
|
|
|
@router.get(
|
|
"/notifications/pubkey",
|
|
dependencies=[Depends(allow_any_authenticated())],
|
|
summary="Get VAPID public key",
|
|
description="""Gets the VAPID public key for the notifications.
|
|
Returns the public key or an error if notifications are not enabled.
|
|
""",
|
|
)
|
|
def get_vapid_pub_key(request: Request):
|
|
config = request.app.frigate_config
|
|
notifications_enabled = config.notifications.enabled
|
|
camera_notifications_enabled = [
|
|
c for c in config.cameras.values() if c.enabled and c.notifications.enabled
|
|
]
|
|
if not (notifications_enabled or camera_notifications_enabled):
|
|
return JSONResponse(
|
|
content=({"success": False, "message": "Notifications are not enabled."}),
|
|
status_code=400,
|
|
)
|
|
|
|
key = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem"))
|
|
raw_pub = key.public_key.public_bytes(
|
|
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
|
|
)
|
|
return JSONResponse(content=utils.b64urlencode(raw_pub), status_code=200)
|
|
|
|
|
|
@router.post(
|
|
"/notifications/register",
|
|
dependencies=[Depends(allow_any_authenticated())],
|
|
summary="Register notifications",
|
|
description="""Registers a notifications subscription.
|
|
Returns a success message or an error if the subscription is not provided.
|
|
""",
|
|
)
|
|
def register_notifications(request: Request, body: dict = None):
|
|
if request.app.frigate_config.auth.enabled:
|
|
# FIXME: For FastAPI the remote-user is not being populated
|
|
username = request.headers.get("remote-user") or "admin"
|
|
else:
|
|
username = "admin"
|
|
|
|
json: dict[str, Any] = body or {}
|
|
sub = json.get("sub")
|
|
|
|
if not sub:
|
|
return JSONResponse(
|
|
content={"success": False, "message": "Subscription must be provided."},
|
|
status_code=400,
|
|
)
|
|
|
|
reason = _validate_subscription(sub)
|
|
|
|
if reason:
|
|
logger.warning(
|
|
"Rejected notification registration for %s: %s", username, reason
|
|
)
|
|
return JSONResponse(
|
|
content={"success": False, "message": f"Invalid subscription: {reason}"},
|
|
status_code=400,
|
|
)
|
|
|
|
updated = (
|
|
User.update(notification_tokens=User.notification_tokens.append(sub))
|
|
.where(User.username == username)
|
|
.execute()
|
|
)
|
|
|
|
if not updated:
|
|
logger.warning(
|
|
"Rejected notification registration for unknown user %s", username
|
|
)
|
|
return JSONResponse(
|
|
content=({"success": False, "message": "Could not find user."}),
|
|
status_code=404,
|
|
)
|
|
|
|
return JSONResponse(
|
|
content=({"success": True, "message": "Successfully saved token."}),
|
|
status_code=200,
|
|
)
|