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
+15 -2
View File
@@ -134,8 +134,21 @@ export function setProp(el, key, value) {
el.className = Object.keys(value).filter(k => value[k]).join(' ');
return;
}
if (key === 'style' && typeof value === 'object' && value !== null) {
for (const [sk, sv] of Object.entries(value)) el.style[sk] = sv;
if (key === 'style') {
// Inline styles go through the CSSOM (el.style), never through
// setAttribute('style', ...) — the latter applies an inline style
// attribute, which the management-domain CSP (style-src 'self',
// no 'unsafe-inline') blocks.
if (value == null || value === false) {
el.removeAttribute('style');
return;
}
if (typeof value === 'object') {
el.style.cssText = ''; // clear stale props from a previous style
for (const [sk, sv] of Object.entries(value)) el.style[sk] = sv;
} else {
el.style.cssText = String(value);
}
return;
}
+6 -2
View File
@@ -34,8 +34,12 @@ function _wsUrl() {
}
/** Attempt a WebSocket connection.
* Passes the JWT in the WebSocket subprotocol header (Sec-WebSocket-Protocol)
* Passes the JWT as the WebSocket subprotocol name (Sec-WebSocket-Protocol)
* instead of a query parameter, keeping it out of logs and browser history.
* The token is sent as-is, WITHOUT a "Bearer " prefix: subprotocol names
* must be valid RFC 6455 tokens and a JWT (base64url + dots) is one, but
* the space in "Bearer <token>" is not a token character — the browser
* rejects the whole constructor with a SyntaxError.
* No token: no socket is created — the daemon 401s unauthenticated WS
* connections and connect() only runs while authenticated.
*/
@@ -46,7 +50,7 @@ function _wsConnect() {
const token = getAuthToken();
if (!token) return;
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
_wsConn = new WebSocket(_wsUrl(), [token]);
_wsConn.onopen = () => {
_wsReconnectMs = 0;