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:
2026-08-18 13:36:55 +00:00
parent 183904faad
commit 4bd4c374fd
7 changed files with 118 additions and 18 deletions
+47
View File
@@ -0,0 +1,47 @@
"""Tests for WebSocket subprotocol token extraction (daemon.server)."""
import pytest
from daemon.server import _extract_ws_token
# Realistic-looking access token (base64url header.payload.signature).
TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.Y3WNO0CTxb4dlcVCPjUnQi"
class TestExtractWsToken:
def test_raw_jwt_subprotocol(self):
"""Bundled client path: the JWT is sent as the subprotocol name itself."""
assert _extract_ws_token([TOKEN]) == (TOKEN, TOKEN)
def test_raw_jwt_among_other_subprotocols(self):
assert _extract_ws_token(["vacuum-wall", TOKEN]) == (TOKEN, TOKEN)
@pytest.mark.parametrize(
"raw",
["none", "vacuum-wall", "a.b", "a.b.c.d"],
)
def test_no_auth_subprotocol(self, raw):
assert _extract_ws_token([raw]) == (None, None)
def test_empty_list(self):
assert _extract_ws_token([]) == (None, None)
def test_bearer_prefix_form(self):
"""Legacy non-browser form: 'Bearer <token>' subprotocol."""
assert _extract_ws_token([f"Bearer {TOKEN}"]) == (TOKEN, f"Bearer {TOKEN}")
def test_bearer_with_non_jwt_token(self):
assert _extract_ws_token(["Bearer abc123"]) == ("abc123", "Bearer abc123")
def test_bearer_without_token_ignored(self):
assert _extract_ws_token(["Bearer ", "Bearer"]) == (None, None)
def test_jwt_like_name_preferred_over_bearer(self):
"""A JWT-shaped subprotocol wins even when listed after a Bearer one."""
result = _extract_ws_token([f"Bearer {TOKEN}", TOKEN])
assert result == (TOKEN, TOKEN)
def test_subprotocol_with_space_rejected(self):
"""A space is not an RFC 6455 token character — 'Bearer <token>' must
go through the Bearer branch, never the JWT-shape branch."""
assert _extract_ws_token(["a b.c.d"]) == (None, None)