fix: address auth subsystem issues from ws-debug review
- lib/auth: make RateLimiter.is_allowed read-only (no dict mutation on read) - daemon/server: add periodic blacklist_expired cleanup to poll loop (60s interval) - daemon/server: negotiate only matched Bearer subprotocol on WebSocket connect - webui/server: rewrite _is_personal_auth with path-prefix matching, cover WebAuthn register routes - daemon/handlers/auth: eliminate redundant get_user call in auth_update_user
This commit is contained in:
+22
-2
@@ -10,6 +10,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -17,6 +18,7 @@ from typing import Any
|
||||
from aiohttp import web
|
||||
|
||||
from daemon.iface import PathLike
|
||||
from lib.auth import blacklist_expired
|
||||
from lib.state import _DEFAULT_POLL_INTERVALS
|
||||
from lib.state import state as state_store
|
||||
|
||||
@@ -361,6 +363,8 @@ def create_app() -> web.Application:
|
||||
_ws_subscribers: set[web.WebSocketResponse] = set()
|
||||
_ws_tasks: set[asyncio.Task[None]] = set()
|
||||
_poll_tasks: set[asyncio.Task[None]] = set()
|
||||
_last_blacklist_cleanup: float = 0
|
||||
_last_blacklist_cleanup_lock: asyncio.Lock | None = None
|
||||
|
||||
|
||||
async def _handle_ws(request: web.Request) -> web.Response:
|
||||
@@ -376,12 +380,14 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
||||
from lib.auth import validate_token
|
||||
|
||||
token_param = None
|
||||
matched_proto = None
|
||||
|
||||
# Prefer subprotocol header (client JS sends "Bearer <token>")
|
||||
subprotocols = request.get_subprotocols()
|
||||
for proto in subprotocols or []:
|
||||
if proto and proto.startswith("Bearer "):
|
||||
token_param = proto[7:]
|
||||
matched_proto = proto
|
||||
break
|
||||
|
||||
if token_param is None:
|
||||
@@ -401,8 +407,10 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
||||
if payload is None:
|
||||
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
|
||||
|
||||
# Negotiate the subprotocol the client sent
|
||||
ws = web.WebSocketResponse(protocols=request.get_subprotocols())
|
||||
# Negotiate only the matched auth subprotocol (or all if token came from header)
|
||||
ws = web.WebSocketResponse(
|
||||
protocols=[matched_proto] if matched_proto else subprotocols
|
||||
)
|
||||
await ws.prepare(request)
|
||||
_ws_subscribers.add(ws)
|
||||
|
||||
@@ -453,6 +461,12 @@ async def broadcast_tick(subsystems: list[str]) -> None:
|
||||
|
||||
async def _poll_loop(subsystem: str, interval: int) -> None:
|
||||
"""Periodically poll a subsystem for state changes and broadcast as needed."""
|
||||
global _last_blacklist_cleanup, _last_blacklist_cleanup_lock
|
||||
|
||||
# Lazy-init lock (requires running event loop)
|
||||
if _last_blacklist_cleanup_lock is None:
|
||||
_last_blacklist_cleanup_lock = asyncio.Lock()
|
||||
|
||||
offset = int(hashlib.md5(subsystem.encode()).hexdigest(), 16) % interval
|
||||
await asyncio.sleep(offset)
|
||||
while True:
|
||||
@@ -463,6 +477,12 @@ async def _poll_loop(subsystem: str, interval: int) -> None:
|
||||
await broadcast_versions()
|
||||
elif volatile:
|
||||
await broadcast_tick([subsystem])
|
||||
# Periodic blacklist cleanup — coordinated across all poll loops
|
||||
async with _last_blacklist_cleanup_lock:
|
||||
now = time.time()
|
||||
if now - _last_blacklist_cleanup >= 60:
|
||||
blacklist_expired()
|
||||
_last_blacklist_cleanup = now
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user