Skip to content

fix(dashboard): accept a signed ticket on the log WebSocket (Safari fix)#359

Merged
sleep3r merged 2 commits into
mainfrom
fix/dashboard-safari-ws
Jun 13, 2026
Merged

fix(dashboard): accept a signed ticket on the log WebSocket (Safari fix)#359
sleep3r merged 2 commits into
mainfrom
fix/dashboard-safari-ws

Conversation

@sleep3r

@sleep3r sleep3r commented Jun 13, 2026

Copy link
Copy Markdown
Owner

The dashboard live-logs WebSocket failed in Safari only (WebSocket connection to ws://localhost:61208/ws/logs failed: bad response from the server); Chrome was fine.

Cause

The /ws/logs handler requires HTTP Basic auth on the handshake, relying on the browser replaying the cached same-origin Basic credentials. Safari/WebKit does not attach Basic auth to WebSocket handshakes (long-standing behaviour); Chrome/Firefox do. So Safari hit the auth gate → ws.close(1008) → "bad response from the server".

Fix

The WS now accepts either replayed Basic auth (unchanged) or a short-lived signed ticket:

  • new GET /api/ws-ticket (gated by the existing Basic-auth middleware — a normal request, which replays Basic auth in every browser incl. Safari) returns an HMAC ticket over its own expiry under the dashboard token (~30s TTL, stateless).
  • app.js fetches the ticket, then opens /ws/logs?ticket=.... Falls back to the Basic-replay path if the fetch fails.
  • The Host-pin and Origin gates are untouched, so the DNS-rebinding / cross-origin defenses still hold.

Tests

test_server.py: ticket round-trip (valid/tampered/garbage/wrong-token), /api/ws-ticket auth, unauthenticated-WS-rejected. Ticket logic verified standalone; py_compile clean.

Dashboard assets are @embedFiled into mtbuddy, so this ships with the next release (and reinstalling the dashboard rewrites the files).

sleep3r added 2 commits June 13, 2026 21:53
Safari/WebKit does not replay cached HTTP Basic credentials on a WebSocket handshake (Chrome and
Firefox do), so the dashboard's `/ws/logs` auth gate rejected Safari with close 1008 — the
browser surfaced it as "WebSocket connection failed: bad response from the server". Chrome was
fine because it replays the cached same-origin Basic creds on the upgrade.

Fix: the log WS now accepts EITHER replayed Basic auth (unchanged, Chrome/Firefox) OR a
short-lived signed ticket. The already-authenticated page fetches the ticket from a new
`GET /api/ws-ticket` (a normal request, which replays Basic auth in every browser including
Safari) and passes it in the handshake URL (`/ws/logs?ticket=...`). The ticket is a stateless
HMAC over its own expiry under the dashboard token (~30s TTL) — no server-side state. The
Host-pin and Origin gates are unchanged, so the cross-origin / DNS-rebinding defenses still hold.

- server.py: `_make_ws_ticket` / `_ws_ticket_ok`, `GET /api/ws-ticket`, WS gate accepts Basic-or-ticket.
- app.js: `connectWS` fetches the ticket before opening the socket (graceful fallback to Basic replay).
- test_server.py: ticket round-trip, endpoint auth, and unauthenticated-WS-rejected.
…e WS-ticket fix

The Safari WS fix lives in app.js, but index.html referenced it as app.js?v=20260611-paper14
(unchanged), so a cached old app.js kept being served and the fix didn't take without a hard
refresh. Bump the version query to force a re-fetch.
@sleep3r sleep3r merged commit 1830e87 into main Jun 13, 2026
8 checks passed
@sleep3r sleep3r deleted the fix/dashboard-safari-ws branch June 13, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant