kirche-einfach/backend/app/middleware.py

70 lines
2.4 KiB
Python

"""Edge hardening: run throttling (slowapi), security headers (secure).
Body caps come from Starlette's RequestBodyLimitMiddleware (wired in
main.py). The run throttle is settings-driven so tests can shrink it.
State is in-process memory: correct for the single-replica SQLite
deployment; revisit with Redis if the API ever scales horizontally.
"""
from __future__ import annotations
from slowapi import Limiter
from slowapi.util import get_remote_address
from starlette.datastructures import MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from app.settings import settings
# POST .../run is the expensive endpoint (LFX execution, LLM cost):
# per client IP and minute, limit read live from settings so tests can
# shrink it without re-creating the app.
limiter = Limiter(
key_func=get_remote_address,
storage_uri="memory://",
default_limits=[],
)
def run_limit() -> str:
return f"{settings.run_throttle_per_minute}/minute"
def reset_throttle() -> None:
"""Drop all counters (tests)."""
try:
limiter._storage.reset() # type: ignore[attr-defined]
except AttributeError:
pass
class SecurityHeadersMiddleware:
"""Baseline response headers (TLS/HSTS live on the proxy).
Built with the ``secure`` package; values stay byte-identical to the
previous hand-rolled version (see test_middleware).
"""
def __init__(self, app: ASGIApp) -> None:
import secure as secure_lib
self.app = app
self.secure_headers = secure_lib.Secure(
xcto=secure_lib.XContentTypeOptions().nosniff(),
xfo=secure_lib.XFrameOptions().sameorigin(),
referrer=secure_lib.ReferrerPolicy().strict_origin_when_cross_origin(),
csp=secure_lib.ContentSecurityPolicy().frame_ancestors("'self'"),
)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def secured(message: Message) -> None:
if message["type"] == "http.response.start":
headers = MutableHeaders(raw=message.setdefault("headers", []))
for key, value in self.secure_headers.headers.items():
headers[key] = value
await send(message)
await self.app(scope, receive, secured)