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 import FastAPI, Request
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
from fastapi_vue import Frontend
|
from fastapi_vue import Frontend
|
||||||
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
||||||
|
|
||||||
from pagerite import api, files, pages, tracking
|
from pagerite import api, files, pages, tracking
|
||||||
from pagerite.__main__ import DEVMODE
|
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
|
@asynccontextmanager
|
||||||
async def lifespan(_app: FastAPI) -> AsyncGenerator:
|
async def lifespan(_app: FastAPI) -> AsyncGenerator:
|
||||||
"""Open the database (migrations run inside kanta.open), load assets, load GeoIP."""
|
"""Open the database (migrations run inside kanta.open), load assets, load GeoIP."""
|
||||||
@@ -83,6 +106,8 @@ app = FastAPI(
|
|||||||
openapi_url=None,
|
openapi_url=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.add_middleware(_AccessLogExtraMiddleware)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def _headers(request: Request, call_next) -> Response:
|
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
|
DB-IP geoip lookups happen in background tasks so message handling is
|
||||||
never delayed by slow DNS or the first MMDB decompress.
|
never delayed by slow DNS or the first MMDB decompress.
|
||||||
"""
|
"""
|
||||||
await ws.accept()
|
|
||||||
ip = _client_ip(ws)
|
ip = _client_ip(ws)
|
||||||
ua = ws.headers.get("user-agent", "")
|
ua = ws.headers.get("user-agent", "")
|
||||||
accept_language = ws.headers.get("accept-language", "")
|
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:
|
try:
|
||||||
while True:
|
while True:
|
||||||
text = await ws.receive_text()
|
text = await ws.receive_text()
|
||||||
|
|||||||
Reference in New Issue
Block a user