mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-14 15:15:22 +03:00
Removed flask app in favour of FastAPI app. Implemented FastAPI middleware to check CSRF, connect and disconnect from DB. Added middleware x-forwared-for headers
This commit is contained in:
parent
655d24a653
commit
8f8d8e1e4c
@ -1,6 +1,7 @@
|
||||
click == 8.1.*
|
||||
Flask == 3.0.*
|
||||
Flask_Limiter == 3.8.*
|
||||
# FastAPI
|
||||
starlette-context == 0.3.6
|
||||
fastapi == 0.115.0
|
||||
imutils == 0.5.*
|
||||
joserfc == 1.0.*
|
||||
markupsafe == 2.1.*
|
||||
|
||||
@ -26,6 +26,8 @@ In the event that you are locked out of your instance, you can tell Frigate to r
|
||||
|
||||
## Login failure rate limiting
|
||||
|
||||
# TODO: Rui Update to use FastAPI
|
||||
|
||||
In order to limit the risk of brute force attacks, rate limiting is available for login failures. This is implemented with Flask-Limiter, and the string notation for valid values is available in [the documentation](https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation).
|
||||
|
||||
For example, `1/second;5/minute;20/hour` will rate limit the login endpoint when failures occur more than:
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
import faulthandler
|
||||
import threading
|
||||
|
||||
from flask import cli
|
||||
|
||||
from frigate.app import FrigateApp
|
||||
|
||||
faulthandler.enable()
|
||||
|
||||
threading.current_thread().name = "frigate"
|
||||
|
||||
cli.show_server_banner = lambda *x: None
|
||||
|
||||
if __name__ == "__main__":
|
||||
frigate_app = FrigateApp()
|
||||
|
||||
|
||||
@ -14,25 +14,15 @@ from fastapi import APIRouter, Path, Request, Response
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.params import Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from flask import Flask, jsonify, request
|
||||
from markupsafe import escape
|
||||
from peewee import operator
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from frigate.api.auth import get_jwt_secret, limiter
|
||||
from frigate.api.defs.app_body import AppConfigSetBody
|
||||
from frigate.api.defs.app_query_parameters import AppTimelineHourlyQueryParameters
|
||||
from frigate.api.defs.tags import Tags
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.const import CONFIG_DIR
|
||||
from frigate.embeddings import EmbeddingsContext
|
||||
from frigate.events.external import ExternalEventProcessor
|
||||
from frigate.models import Event, Timeline
|
||||
from frigate.plus import PlusApi
|
||||
from frigate.ptz.onvif import OnvifController
|
||||
from frigate.stats.emitter import StatsEmitter
|
||||
from frigate.storage import StorageMaintainer
|
||||
from frigate.util.builtin import (
|
||||
clean_camera_user_pass,
|
||||
get_tz_modifiers,
|
||||
@ -47,56 +37,6 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(tags=[Tags.app])
|
||||
|
||||
|
||||
def create_app(
|
||||
frigate_config,
|
||||
database: SqliteQueueDatabase,
|
||||
embeddings: Optional[EmbeddingsContext],
|
||||
detected_frames_processor,
|
||||
storage_maintainer: StorageMaintainer,
|
||||
onvif: OnvifController,
|
||||
external_processor: ExternalEventProcessor,
|
||||
plus_api: PlusApi,
|
||||
stats_emitter: StatsEmitter,
|
||||
):
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.before_request
|
||||
def check_csrf():
|
||||
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
||||
pass
|
||||
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
||||
return jsonify({"success": False, "message": "Missing CSRF header"}), 401
|
||||
|
||||
@app.before_request
|
||||
def _db_connect():
|
||||
if database.is_closed():
|
||||
database.connect()
|
||||
|
||||
@app.teardown_request
|
||||
def _db_close(exc):
|
||||
if not database.is_closed():
|
||||
database.close()
|
||||
|
||||
app.frigate_config = frigate_config
|
||||
app.embeddings = embeddings
|
||||
app.detected_frames_processor = detected_frames_processor
|
||||
app.storage_maintainer = storage_maintainer
|
||||
app.onvif = onvif
|
||||
app.external_processor = external_processor
|
||||
app.plus_api = plus_api
|
||||
app.camera_error_image = None
|
||||
app.stats_emitter = stats_emitter
|
||||
app.jwt_token = get_jwt_secret() if frigate_config.auth.enabled else None
|
||||
# update the request_address with the x-forwarded-for header from nginx
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
|
||||
# initialize the rate limiter for the login endpoint
|
||||
limiter.init_app(app)
|
||||
if frigate_config.auth.failed_login_rate_limit is None:
|
||||
limiter.enabled = False
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@router.get("/")
|
||||
def is_healthy():
|
||||
return "Frigate is running. Alive and healthy!"
|
||||
@ -306,7 +246,7 @@ def config_save(save_option: str, body: dict):
|
||||
|
||||
|
||||
@router.put("/config/set")
|
||||
def config_set(body: AppConfigSetBody):
|
||||
def config_set(request: Request, body: AppConfigSetBody):
|
||||
config_file = os.environ.get("CONFIG_FILE", f"{CONFIG_DIR}/config.yml")
|
||||
|
||||
# Check if we can use .yaml instead of .yml
|
||||
|
||||
@ -182,20 +182,19 @@ def auth(request: Request):
|
||||
auth_config: AuthConfig = request.app.frigate_config.auth
|
||||
proxy_config: ProxyConfig = request.app.frigate_config.proxy
|
||||
|
||||
success_response = Response(content={}, status_code=202)
|
||||
success_response = Response("", status_code=202)
|
||||
|
||||
# dont require auth if the request is on the internal port
|
||||
# this header is set by Frigate's nginx proxy, so it cant be spoofed
|
||||
if request.headers.get("x-server-port", 0, type=int) == 5000:
|
||||
if int(request.headers.get("x-server-port", default=0)) == 5000:
|
||||
return success_response
|
||||
|
||||
fail_response = Response(content={}, status_code=401)
|
||||
fail_response = Response("", status_code=401)
|
||||
|
||||
# ensure the proxy secret matches if configured
|
||||
if (
|
||||
proxy_config.auth_secret is not None
|
||||
and request.headers.get("x-proxy-secret", "", type=str)
|
||||
!= proxy_config.auth_secret
|
||||
and request.headers.get("x-proxy-secret", "") != proxy_config.auth_secret
|
||||
):
|
||||
logger.debug("X-Proxy-Secret header does not match configured secret value")
|
||||
return fail_response
|
||||
@ -207,7 +206,6 @@ def auth(request: Request):
|
||||
if proxy_config.header_map.user is not None:
|
||||
upstream_user_header_value = request.headers.get(
|
||||
proxy_config.header_map.user,
|
||||
type=str,
|
||||
default="anonymous",
|
||||
)
|
||||
success_response.headers["remote-user"] = upstream_user_header_value
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from starlette_context import middleware, plugins
|
||||
|
||||
from frigate.api import app as main_app
|
||||
from frigate.api import auth, event, export, media, notification, preview, review
|
||||
@ -16,21 +19,62 @@ from frigate.storage import StorageMaintainer
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_csrf(request: Request):
|
||||
if request.method in ["GET", "HEAD", "OPTIONS", "TRACE"]:
|
||||
pass
|
||||
if "origin" in request.headers and "x-csrf-token" not in request.headers:
|
||||
return JSONResponse(
|
||||
content={"success": False, "message": "Missing CSRF header"},
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
|
||||
def create_fastapi_app(
|
||||
frigate_config,
|
||||
database: SqliteQueueDatabase,
|
||||
embeddings: Optional[EmbeddingsContext],
|
||||
detected_frames_processor,
|
||||
storage_maintainer: StorageMaintainer,
|
||||
onvif: OnvifController,
|
||||
external_processor: ExternalEventProcessor,
|
||||
plus_api: PlusApi,
|
||||
stats_emitter: StatsEmitter,
|
||||
external_processor: ExternalEventProcessor,
|
||||
):
|
||||
logger.info("Starting FastAPI app")
|
||||
app = FastAPI(
|
||||
debug=False,
|
||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||
)
|
||||
|
||||
# update the request_address with the x-forwarded-for header from nginx
|
||||
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
|
||||
check_csrf(request)
|
||||
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
|
||||
|
||||
# TODO: Rui
|
||||
# initialize the rate limiter for the login endpoint
|
||||
# limiter.init_app(app)
|
||||
# if frigate_config.auth.failed_login_rate_limit is None:
|
||||
# limiter.enabled = False
|
||||
|
||||
# Routes
|
||||
app.include_router(main_app.router)
|
||||
app.include_router(media.router)
|
||||
|
||||
@ -15,13 +15,11 @@ from typing import Optional
|
||||
|
||||
import psutil
|
||||
import uvicorn
|
||||
from fastapi.middleware.wsgi import WSGIMiddleware
|
||||
from peewee_migrate import Router
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from pydantic import ValidationError
|
||||
|
||||
from frigate.api.app import create_app
|
||||
from frigate.api.auth import hash_password
|
||||
from frigate.api.fastapi_app import create_fastapi_app
|
||||
from frigate.comms.config_updater import ConfigPublisher
|
||||
@ -388,7 +386,7 @@ class FrigateApp:
|
||||
self.inter_zmq_proxy = ZmqProxy()
|
||||
|
||||
def init_web_server(self) -> None:
|
||||
self.flask_app = create_app(
|
||||
self.fastapi_app = create_fastapi_app(
|
||||
self.config,
|
||||
self.db,
|
||||
self.embeddings,
|
||||
@ -400,17 +398,6 @@ class FrigateApp:
|
||||
self.stats_emitter,
|
||||
)
|
||||
|
||||
self.fastapi_app = create_fastapi_app(
|
||||
self.config,
|
||||
self.embeddings,
|
||||
self.detected_frames_processor,
|
||||
self.storage_maintainer,
|
||||
self.onvif_controller,
|
||||
self.plus_api,
|
||||
self.stats_emitter,
|
||||
self.external_event_processor,
|
||||
)
|
||||
|
||||
def init_onvif(self) -> None:
|
||||
self.onvif_controller = OnvifController(self.config, self.ptz_metrics)
|
||||
|
||||
@ -761,6 +748,7 @@ class FrigateApp:
|
||||
self.start_watchdog()
|
||||
self.init_auth()
|
||||
|
||||
# TODO: Rui. What to do in this case? Maybe https://github.com/encode/uvicorn/issues/1579#issuecomment-1419635974
|
||||
# Flask only listens for SIGINT, so we need to catch SIGTERM and send SIGINT
|
||||
def receiveSignal(signalNumber: int, frame: Optional[FrameType]) -> None:
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
@ -768,8 +756,6 @@ class FrigateApp:
|
||||
signal.signal(signal.SIGTERM, receiveSignal)
|
||||
|
||||
try:
|
||||
# Run the flask app inside fastapi: https://fastapi.tiangolo.com/advanced/sub-applications/
|
||||
self.fastapi_app.mount("", WSGIMiddleware(self.flask_app))
|
||||
uvicorn.run(
|
||||
self.fastapi_app,
|
||||
host="127.0.0.1",
|
||||
@ -778,7 +764,7 @@ class FrigateApp:
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
logger.info("FastAPI/Flask has exited...")
|
||||
logger.info("FastAPI has exited...")
|
||||
|
||||
self.stop()
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ from playhouse.shortcuts import model_to_dict
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
from playhouse.sqliteq import SqliteQueueDatabase
|
||||
|
||||
from frigate.api.app import create_app
|
||||
from frigate.api.fastapi_app import create_fastapi_app
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.models import Event, Recordings
|
||||
@ -115,7 +114,7 @@ class TestHttp(unittest.TestCase):
|
||||
pass
|
||||
|
||||
def test_get_event_list(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -152,7 +151,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert not events
|
||||
|
||||
def test_get_good_event(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -174,7 +173,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert event == model_to_dict(Event.get(Event.id == id))
|
||||
|
||||
def test_get_bad_event(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -195,7 +194,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert not event
|
||||
|
||||
def test_delete_event(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -218,7 +217,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert not event
|
||||
|
||||
def test_event_retention(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -245,7 +244,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert event["retain_indefinitely"] is False
|
||||
|
||||
def test_event_time_filtering(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -284,7 +283,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert len(events) == 1
|
||||
|
||||
def test_set_delete_sub_label(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -320,7 +319,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert event["sub_label"] == ""
|
||||
|
||||
def test_sub_label_list(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config),
|
||||
self.db,
|
||||
None,
|
||||
@ -346,7 +345,7 @@ class TestHttp(unittest.TestCase):
|
||||
assert sub_labels == [sub_label]
|
||||
|
||||
def test_config(self):
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||
self.db,
|
||||
None,
|
||||
@ -386,7 +385,7 @@ class TestHttp(unittest.TestCase):
|
||||
def test_stats(self):
|
||||
stats = Mock(spec=StatsEmitter)
|
||||
stats.get_latest_stats.return_value = self.test_stats
|
||||
app = create_app(
|
||||
app = create_fastapi_app(
|
||||
FrigateConfig(**self.minimal_config).runtime_config(),
|
||||
self.db,
|
||||
None,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user