Access-log extras: remote-user on /_api, visitor info on /_ws
This commit is contained in:
@@ -37,6 +37,7 @@ from pathlib import Path
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import Response
|
||||
from fastapi_vue import Frontend
|
||||
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
from pagerite import api, files, pages, tracking
|
||||
from pagerite.__main__ import DEVMODE
|
||||
@@ -53,6 +54,28 @@ frontend = Frontend(
|
||||
)
|
||||
|
||||
|
||||
class _AccessLogExtraMiddleware:
|
||||
"""Fill the ``log_extra`` slot of fastapi_vue's access log.
|
||||
|
||||
Everything under ``/_api`` is gated by the SSO forward-auth, which names
|
||||
the authenticated user in the ``remote-user`` header; put that user on
|
||||
the access-log line, for plain requests and WebSocket open/close alike.
|
||||
The scope dict is shared with the outer AccessLogMiddleware, which reads
|
||||
the slot back at response/accept/close time.
|
||||
"""
|
||||
|
||||
def __init__(self, app: ASGIApp) -> None:
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
if scope["type"] in ("http", "websocket") and scope["path"].startswith("/_api"):
|
||||
headers = dict(scope["headers"])
|
||||
user = headers.get(b"remote-user", b"").decode("latin-1")
|
||||
if user:
|
||||
scope.setdefault("state", {})["log_extra"] = user
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI) -> AsyncGenerator:
|
||||
"""Open the database (migrations run inside kanta.open), load assets, load GeoIP."""
|
||||
@@ -83,6 +106,8 @@ app = FastAPI(
|
||||
openapi_url=None,
|
||||
)
|
||||
|
||||
app.add_middleware(_AccessLogExtraMiddleware)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def _headers(request: Request, call_next) -> Response:
|
||||
|
||||
@@ -435,10 +435,16 @@ async def activity_ws(ws: WebSocket) -> None:
|
||||
DB-IP geoip lookups happen in background tasks so message handling is
|
||||
never delayed by slow DNS or the first MMDB decompress.
|
||||
"""
|
||||
await ws.accept()
|
||||
ip = _client_ip(ws)
|
||||
ua = ws.headers.get("user-agent", "")
|
||||
accept_language = ws.headers.get("accept-language", "")
|
||||
# Identify the visitor on the access-log open/close lines (the IP is
|
||||
# already printed there): compact UA plus the browser's language tag.
|
||||
lang, _country = analytics._parse_accept_language(accept_language)
|
||||
ws.scope.setdefault("state", {})["log_extra"] = " ".join(
|
||||
part for part in (analytics._compact_user_agent(ua), lang) if part
|
||||
)
|
||||
await ws.accept()
|
||||
try:
|
||||
while True:
|
||||
text = await ws.receive_text()
|
||||
|
||||
Reference in New Issue
Block a user