fix: send WS JWT as bare subprotocol name; CSSOM inline styles; open mgmt services on public zone
- websocket.js passes the raw JWT as the Sec-WebSocket-Protocol subprotocol (no 'Bearer ' prefix): subprotocol names must be valid RFC 6455 tokens, and the space in 'Bearer <token>' made the browser reject the constructor with a SyntaxError. - daemon accepts a JWT-shaped subprotocol plus the legacy 'Bearer <token>' form via _extract_ws_token; unit tests in tests/test_ws_auth.py. - vdom.js applies inline styles through el.style (CSSOM) instead of setAttribute, which the management-domain CSP (no 'unsafe-inline') blocks. - install.sh opens http/https/ssh on the public zone alongside WAN setup. - docs (hoover.md, security.md) updated to match.
This commit is contained in:
+43
-12
@@ -9,6 +9,7 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
@@ -359,6 +360,38 @@ def create_app() -> web.Application:
|
||||
return app
|
||||
|
||||
|
||||
# JWTs are dot-joined base64url segments — a subset of the RFC 6455 token
|
||||
# character set. Browsers therefore carry the access token as the
|
||||
# Sec-WebSocket-Protocol subprotocol name itself (see websocket.js); the
|
||||
# "Bearer <token>" form is not a valid subprotocol (space is not a token
|
||||
# character) and is rejected by the WebSocket constructor.
|
||||
_JWT_SUBPROTOCOL_RE = re.compile(r"^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _extract_ws_token(subprotocols: list[str]) -> tuple[str | None, str | None]:
|
||||
"""Extract the auth JWT from parsed Sec-WebSocket-Protocol names.
|
||||
|
||||
Accepts the raw JWT as a subprotocol name (bundled client) or the
|
||||
legacy "Bearer <token>" form (non-browser clients that can send spaces).
|
||||
|
||||
Args:
|
||||
subprotocols: Parsed subprotocol names (already stripped/split).
|
||||
|
||||
Returns:
|
||||
``(token, matched_subprotocol)`` or ``(None, None)`` if no usable
|
||||
auth subprotocol was found.
|
||||
"""
|
||||
for proto in subprotocols:
|
||||
if _JWT_SUBPROTOCOL_RE.fullmatch(proto):
|
||||
return proto, proto
|
||||
for proto in subprotocols:
|
||||
if proto.startswith("Bearer "):
|
||||
token = proto[7:].strip()
|
||||
if token:
|
||||
return token, proto
|
||||
return None, None
|
||||
|
||||
|
||||
# WebSocket subscribers
|
||||
_ws_subscribers: set[web.WebSocketResponse] = set()
|
||||
_ws_tasks: set[asyncio.Task[None]] = set()
|
||||
@@ -374,10 +407,12 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
||||
updated subsystem versions. Clients disconnect to unsubscribe.
|
||||
|
||||
Authentication: JWT access token passed via:
|
||||
1. WebSocket subprotocol header (Sec-WebSocket-Protocol: "Bearer <token>")
|
||||
— the bundled client path.
|
||||
2. X-Auth-Token header — fallback for custom nginx setups that inject it
|
||||
(not set by the bundled nginx config).
|
||||
1. WebSocket subprotocol header — the bundled client sends the raw JWT
|
||||
as the subprotocol name (Sec-WebSocket-Protocol must carry a valid
|
||||
RFC 6455 token; a JWT is one, "Bearer <token>" is not).
|
||||
2. "Bearer <token>" subprotocol — legacy form for non-browser clients.
|
||||
3. X-Auth-Token header — fallback for custom nginx setups that inject
|
||||
it (not set by the bundled nginx config).
|
||||
"""
|
||||
from aiohttp import hdrs
|
||||
|
||||
@@ -386,16 +421,12 @@ async def _handle_ws(request: web.Request) -> web.Response:
|
||||
token_param = None
|
||||
matched_proto = None
|
||||
|
||||
# Prefer subprotocol header (client JS sends "Bearer <token>").
|
||||
# Sec-WebSocket-Protocol is a comma-separated list; parse it the same
|
||||
# way aiohttp's own handshake does (Request has no subprotocol helper).
|
||||
# Prefer the subprotocol header. Sec-WebSocket-Protocol is a
|
||||
# comma-separated list; parse it the same way aiohttp's own handshake
|
||||
# does (Request has no subprotocol helper).
|
||||
protocol_header = request.headers.get(hdrs.SEC_WEBSOCKET_PROTOCOL, "")
|
||||
subprotocols = [p.strip() for p in protocol_header.split(",") if p.strip()]
|
||||
for proto in subprotocols:
|
||||
if proto.startswith("Bearer "):
|
||||
token_param = proto[7:]
|
||||
matched_proto = proto
|
||||
break
|
||||
token_param, matched_proto = _extract_ws_token(subprotocols)
|
||||
|
||||
if token_param is None:
|
||||
token_param = request.headers.get("X-Auth-Token")
|
||||
|
||||
Reference in New Issue
Block a user