Skip to content

Commit de22e0f

Browse files
committed
fix(ws): cookie-first auth, fall back to token only on 4001
The WS client always appended ?token=<bearer> to the URL, but the server rejects query-token auth by default (WS_QUERY_TOKEN_ENABLED is false). The httpOnly session cookie was already attached to the handshake by the browser — cookie auth would have worked, the client just never gave it a chance. Result: every connection rejected, "WS Connection timeout" loop in dev tools. - public/js/ws.js: prefer cookie-only on first attempt. If close 4001 AND a Bearer token exists, retry once with token-in-query. Reset on successful open so a rotated token is re-tried correctly. Also adds an inline "Update available" banner to the What's New page header (between title and version+GitHub controls — same row, no header growth) per user request. Release: v7.3.5
1 parent 4bae521 commit de22e0f

6 files changed

Lines changed: 87 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
All notable changes to Docker Dash are documented here.
44

5+
## [7.3.5] - 2026-04-25 — WS cookie-first auth + What's New update banner
6+
7+
### Fixed
8+
9+
- **WebSocket connection failures** (rejected by server with `WS rejected: query token auth disabled`). The client always appended `?token=<bearer>` to the WS URL, but the server rejects query-token auth by default for security (set `WS_QUERY_TOKEN_ENABLED=true` to allow). The session cookie (httpOnly `dd_sid`) was already attached to the WS handshake by the browser, so cookie auth would have worked — the client just never gave it a chance.
10+
11+
Now the client tries **cookie-only first**. Only if that closes with code 4001 (auth failed) AND a Bearer token is in `sessionStorage` does it fall back to token-in-query for one retry. This keeps the security default intact for everyone using cookies, while preserving the fallback for browsers that block them (Edge Tracking Prevention etc.). Reset to cookie-first on every successful open so a rotated token gets re-tried correctly. ([public/js/ws.js:6-25, 44-56, 68-91](public/js/ws.js))
12+
13+
### Added
14+
15+
- **Inline "Update available" banner** in the What's New page header. When `UpdateNotifier._state.hasUpdate === true`, a small accent-colored chip appears between the H2 and the version+GitHub controls — same row, no header growth. Click → opens the same release-notes modal as the sidebar badge. Hidden when up-to-date or feature disabled. ([public/js/pages/whatsnew.js:1369-1418](public/js/pages/whatsnew.js))
16+
517
## [7.3.3] - 2026-04-25 — System → Updates surfaces app updates too
618

719
The v7.3.0 update notifier was reachable from the sidebar badge and System Settings → General — but **not** from System → Updates, which is the page users naturally reach for "is there an update for X?" That page only checked Docker Engine + OS updates, with the Docker Dash row showing only the running version (no comparison to GitHub latest).

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
context: .
55
dockerfile: Dockerfile
66
args:
7-
APP_VERSION: "${APP_VERSION:-7.3.3}"
8-
image: docker-dash:${APP_VERSION:-7.3.3}
7+
APP_VERSION: "${APP_VERSION:-7.3.5}"
8+
image: docker-dash:${APP_VERSION:-7.3.5}
99
container_name: docker-dash
1010
restart: unless-stopped
1111
env_file:
@@ -54,7 +54,7 @@ services:
5454
dd-egress-filter:
5555
build:
5656
context: ./docker/egress-filter
57-
image: docker-dash-egress-filter:${APP_VERSION:-7.3.3}
57+
image: docker-dash-egress-filter:${APP_VERSION:-7.3.5}
5858
container_name: dd-egress-filter
5959
restart: unless-stopped
6060
# Uses the default bridge so target containers on the default bridge can

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "docker-dash",
3-
"version": "7.3.3",
3+
"version": "7.3.5",
44
"description": "Full-featured Docker management dashboard",
55
"main": "src/server.js",
66
"scripts": {

public/js/pages/whatsnew.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ const WhatsNewPage = {
99
// Add new releases at the TOP of this array.
1010
// Types: feature, fix, improvement, security, breaking
1111
_releases: [
12+
{
13+
version: '7.3.5',
14+
date: '2026-04-25',
15+
title: 'WS cookie-first auth + What\'s New update banner',
16+
changes: [
17+
{ type: 'fix', text: 'WebSocket failed to connect ("WS rejected: query token auth disabled" in server logs). The client always appended ?token=<bearer> to the WS URL, but the server rejects query-token auth by default for security. The session cookie (httpOnly dd_sid) was already attached to the WS handshake by the browser — cookie auth would have worked, the client just never tried it that way. Now: cookie-only first, fall back to token-in-query only after 4001. Reset on every successful open so token rotations get re-tried.' },
18+
{ type: 'feature', text: 'When a newer release is available on GitHub, the What\'s New page header shows a small accent-colored "Update available: vX.Y.Z" chip between the title and the version/GitHub controls. Same row, no header growth. Click → opens the release-notes modal.' },
19+
],
20+
},
1221
{
1322
version: '7.3.3',
1423
date: '2026-04-25',
@@ -1372,7 +1381,8 @@ const WhatsNewPage = {
13721381
container.innerHTML = `
13731382
<div class="page-header">
13741383
<h2><i class="fas fa-bullhorn"></i> What's New</h2>
1375-
<div class="page-actions">
1384+
<span id="whatsnew-update-banner" style="display:none;margin:0 12px;flex:0 0 auto"></span>
1385+
<div class="page-actions" style="margin-left:auto">
13761386
<span class="badge badge-info" style="font-size:12px">v${Utils.escapeHtml(current)}</span>
13771387
<a href="https://github.com/bogdanpricop/docker-dash" target="_blank" rel="noopener" class="btn btn-sm btn-secondary" style="display:inline-flex;align-items:center;gap:6px;text-decoration:none">
13781388
<i class="fab fa-github"></i> GitHub
@@ -1381,6 +1391,38 @@ const WhatsNewPage = {
13811391
</div>
13821392
<div id="whatsnew-content">${this._renderReleases()}</div>
13831393
`;
1394+
1395+
// v7.3.5: render an inline "Update available" banner in the header when
1396+
// a newer release exists on GitHub. Sized to fit on the same row as the
1397+
// existing controls so the header height doesn't grow.
1398+
this._renderUpdateBanner();
1399+
},
1400+
1401+
async _renderUpdateBanner() {
1402+
const slot = document.getElementById('whatsnew-update-banner');
1403+
if (!slot) return;
1404+
let state = window.UpdateNotifier?._state;
1405+
// If notifier hasn't run yet (rare, since app.js inits it on startup),
1406+
// fetch on-demand so the banner doesn't stay empty.
1407+
if (!state) {
1408+
try { await window.UpdateNotifier?.init(); state = window.UpdateNotifier?._state; }
1409+
catch { return; }
1410+
}
1411+
if (!state || !state.hasUpdate) return;
1412+
slot.style.display = 'inline-flex';
1413+
slot.style.alignItems = 'center';
1414+
slot.style.gap = '6px';
1415+
slot.style.padding = '4px 10px';
1416+
slot.style.borderRadius = 'var(--radius-sm)';
1417+
slot.style.background = 'rgba(56,139,253,0.12)';
1418+
slot.style.color = 'var(--accent)';
1419+
slot.style.fontSize = '12px';
1420+
slot.style.fontWeight = '600';
1421+
slot.style.cursor = 'pointer';
1422+
slot.style.lineHeight = '1';
1423+
slot.title = i18n.t('updates.badgeTooltip', { version: state.latest });
1424+
slot.innerHTML = `<i class="fas fa-arrow-up" style="font-size:10px"></i> ${i18n.t('updates.modalTitle')}: ${Utils.escapeHtml(state.latest)}`;
1425+
slot.addEventListener('click', () => window.UpdateNotifier?.openModal());
13841426
},
13851427

13861428
_renderReleases() {

public/js/ws.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ const WS = {
1212
_subscriptions: new Set(),
1313
_connected: false,
1414
_intentionalClose: false,
15+
_useTokenFallback: false, // v7.3.5: only true after a cookie-only attempt failed with 4001
1516

1617
connect() {
1718
if (this._ws && this._ws.readyState <= 1) return;
1819
this._intentionalClose = false;
1920

2021
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
21-
// Pass Bearer token as query param for when cookies are blocked
22+
// v7.3.5: prefer cookie auth (the session cookie is httpOnly so JS can't
23+
// probe it — browser attaches it automatically on the WS handshake).
24+
// The server rejects ?token= unless WS_QUERY_TOKEN_ENABLED=true is set,
25+
// and that's off by default for security (tokens can leak via logs/refer).
26+
// Only fall back to token-in-query after a cookie attempt closes 4001.
2227
const token = Api?._bearerToken || '';
23-
const url = token
28+
const url = (this._useTokenFallback && token)
2429
? `${proto}//${location.host}/ws?token=${encodeURIComponent(token)}`
2530
: `${proto}//${location.host}/ws`;
2631

@@ -44,6 +49,9 @@ const WS = {
4449
clearTimeout(connectTimeout);
4550
this._connected = true;
4651
this._reconnectDelay = 1000;
52+
// v7.3.5: cookie auth worked — clear the fallback flag so future
53+
// reconnects also try cookie-first (in case token rotated).
54+
this._useTokenFallback = false;
4755
if (window._ddDebug) console.log('[WS] Connected');
4856
this._emit('_connected');
4957
// Re-subscribe channels
@@ -67,12 +75,25 @@ const WS = {
6775
this._connected = false;
6876
if (window._ddDebug) console.log('[WS] Disconnected', evt.code);
6977
this._emit('_disconnected');
70-
if (!this._intentionalClose && evt.code !== 4001) {
71-
this._scheduleReconnect();
72-
}
7378
if (evt.code === 4001) {
74-
// Auth failure
79+
// v7.3.5: cookie auth failed. If we haven't tried token-in-query yet
80+
// and we have a Bearer token, retry once with the fallback. This
81+
// handles browsers that block the session cookie (e.g. strict
82+
// tracking prevention). Only flip + retry if the user is still
83+
// authenticated as far as the API is concerned.
84+
if (!this._useTokenFallback && Api?._bearerToken && !this._intentionalClose) {
85+
this._useTokenFallback = true;
86+
if (window._ddDebug) console.log('[WS] Cookie auth rejected, retrying with token fallback');
87+
this._scheduleReconnect();
88+
return;
89+
}
90+
// Either we already tried both modes, or we have no token to try.
91+
// Real auth failure — bounce to login (idempotent in v7.3.1+).
7592
if (typeof App !== 'undefined') App.handleUnauthorized();
93+
return;
94+
}
95+
if (!this._intentionalClose) {
96+
this._scheduleReconnect();
7697
}
7798
};
7899

src/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// Single source of truth for the application version.
33
// Updated automatically by: npm version X.Y.Z (via scripts/sync-version.js)
44
// server.js reads this to inject into index.html at startup — no build step needed.
5-
module.exports = '7.3.3';
5+
module.exports = '7.3.5';

0 commit comments

Comments
 (0)