aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-02 21:51:31 +0100
committerAgando <roduccomm@gmail.com>2026-11-02 21:51:31 +0100
commit46a1b29d1e60fa0ac478fdff45dc6456807d6297 (patch)
treec91216d57bf2ad247a3e427fb532d8815681bed5
parent4587c757b17d8a92df7c98ec37ace4c5500a4f5a (diff)
fix(auth): one session writer for every login path; expire dead credentials
Third occurrence of the same class of bug, and the first fix is why: commit e95b7be (2026-11-01, "multi-session .newsession v3") touched exactly ONE hunk of auth-file-api.py — handle_login. handle_device_poll, handle_telegram_login and handle_register kept overwriting the whole .newsession with a single-session v2 payload, so a phone sign-in logged the desktop out and the loser held a session id no store knew: permanent `401 Invalid session or API key` (newOS4 asked for a login it could never complete). Measured: agando's store held 1 entry (sha8 45cb7819) while the phone sent 9ac607f1 for 3 h; 26 device-polls in 2 s from one page minted 26 cookies and only the last write survived, which also drained the shared nginx `webx11` zone (11x 503 on /admin/api/*). api/webx11_auth.py - add_session() is now THE writer: v3 `sessions[]` (cap 5) under a per-user `flock` (LOCK_DIR outside the spaces), plus the legacy `.session` mirror - module-level import so no handler has to reach for it lazily api/auth-file-api.py - device-poll / telegram-login / register / recover-by-backup-code all call add_session(); a bare write_newsession_file() is now only the admin-gated handle_create_session - handle_device_poll runs its approved branch under a per-code `flock` and mints exactly one session per approved code (idempotent under a poll storm), with a cheap pre-lock reject so unvalidated codes cannot mint lock files - handle_me_cookie expires an unknown session cookie (Set-Cookie Max-Age=0, code SESSION_DEAD): the cookie is HttpOnly, so no page could ever clear a dead credential and every load re-sent it api/newcrypto.py - write_newsession_file() is atomic (tmp + fsync + os.replace); the temp name carries pid+tid because a fixed `<file>.tmp` lets two concurrent writers of one store steal each other's temp file (reproduced: FileNotFoundError) admin-login.html single-flight poll (polling/finished guards) — 26 calls -> 1 dashboard.html device-code fallback when the Telegram popup has no opener spaces/newos4/* 401 -> session-repair notice + endpoint normalisation, v4.6.144 nginx /api/auth/ + /api/security/ on zone wx11auth (120r/m, burst 60) admin/scripts/verify-session-writers.py NEW static guard, wired into smoke-auth.sh Proof (live, after restart): 5 concurrent polls on one approved code -> 5x approved, 1 distinct session id, store v3 with 2 entries, pre-fix session still valid; dead cookie -> 401 + Set-Cookie expiry, valid cookie -> 200; 200-call auth burst -> 503s only on /api/auth/, /admin/ stayed 302; smoke-auth.sh PASS. Guard RED (3 offenders) -> GREEN. Backups: /home/darkbot/backups/authfix-20261102_214424
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js20
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/ollama-manager.js20
4 files changed, 44 insertions, 4 deletions
diff --git a/main.html b/main.html
index 39efdc5..2939f74 100644
--- a/main.html
+++ b/main.html
@@ -1349,13 +1349,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=v4.6.143"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.144"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=v4.6.143"></script>
+<script src="modules/core/module-loader.js?v=v4.6.144"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.143"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.144"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 2a7ec3e..6a37d61 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -1307,6 +1307,22 @@ class OllamaManager {
return err;
}
+ _sessionRepairNotice() {
+ // A 401 here means the cookie/Bearer we sent is not in any session store: the store
+ // moved on (another device signed in, poll race, expired) and this tab kept the old
+ // value. Clear the local copy so the next attempt is anonymous-demo instead of a
+ // permanent 401, and say what to do. (2026-11-02)
+ try {
+ localStorage.removeItem('webx11_session_id');
+ localStorage.removeItem('webx11_session');
+ localStorage.removeItem('webx11_encrypted_session');
+ document.cookie = 'webx11_session=; path=/; max-age=0';
+ } catch (e) {}
+ this.terminal.print('🔒 Your login session is no longer valid on the server. ' +
+ 'Open https://webx11.duckdns.org/dashboard.html , sign in with Telegram ' +
+ '(or “Sign in with a code”), then reload this page.', 'warning');
+ }
+
constructor(terminal) {
this.terminal = terminal;
// --- Backward-compat properties (reflected from active provider) ---
@@ -1403,6 +1419,7 @@ class OllamaManager {
enabled: true
}];
}
+ this.providers.forEach(p => { if (p.endpoint) p.endpoint = p.endpoint.replace(/\/+$/, ''); });
}
_saveProviders() {
@@ -1913,6 +1930,7 @@ class OllamaManager {
} catch (error) {
// Remember why this provider failed so chat() can name the real cause
// instead of falling back to the generic "No provider responded".
+ if (error.status === 401) { this._sessionRepairNotice(); }
this._lastChatError = error.message || String(error);
if (fullText && fullText.length > 0) {
this.terminal.printStreamingResponse(`\n\n[Stream error: ${error.message}]`, true);
@@ -2321,6 +2339,7 @@ class OllamaManager {
if (error.message === 'Failed to fetch' && provider.type === 'ollama') {
return await this.chatWithToolsFallback(messages, tools, useModel);
}
+ if (error.status === 401) { this._sessionRepairNotice(); }
this.terminal.print(`${provider.name} /api/chat error: ${error.message}`, 'error');
this._lastChatError = error.message || String(error);
return { message: null, tool_calls: null, error: error.message };
@@ -2574,6 +2593,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// ─── Backward-compat methods ─────────────────────────────────────────────
setEndpoint(url) {
+ if (typeof url === 'string') url = url.replace(/\/+$/, '');
this.endpoint = url;
// Also update the current provider's endpoint if it exists
if (this.currentProvider) {
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index e1ed716..2cb5003 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = 'v4.6.143';
+ const LATEST_VER = 'v4.6.144';
const CACHE = '?v=' + LATEST_VER;
// Exposed so ext modules can register their own URL with the same cache-busting version
// instead of an unversioned path the browser may already have cached (shell.js did).
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 1201280..5684210 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -27,6 +27,22 @@ class OllamaManager {
return err;
}
+ _sessionRepairNotice() {
+ // A 401 here means the cookie/Bearer we sent is not in any session store: the store
+ // moved on (another device signed in, poll race, expired) and this tab kept the old
+ // value. Clear the local copy so the next attempt is anonymous-demo instead of a
+ // permanent 401, and say what to do. (2026-11-02)
+ try {
+ localStorage.removeItem('webx11_session_id');
+ localStorage.removeItem('webx11_session');
+ localStorage.removeItem('webx11_encrypted_session');
+ document.cookie = 'webx11_session=; path=/; max-age=0';
+ } catch (e) {}
+ this.terminal.print('🔒 Your login session is no longer valid on the server. ' +
+ 'Open https://webx11.duckdns.org/dashboard.html , sign in with Telegram ' +
+ '(or “Sign in with a code”), then reload this page.', 'warning');
+ }
+
constructor(terminal) {
this.terminal = terminal;
// --- Backward-compat properties (reflected from active provider) ---
@@ -123,6 +139,7 @@ class OllamaManager {
enabled: true
}];
}
+ this.providers.forEach(p => { if (p.endpoint) p.endpoint = p.endpoint.replace(/\/+$/, ''); });
}
_saveProviders() {
@@ -633,6 +650,7 @@ class OllamaManager {
} catch (error) {
// Remember why this provider failed so chat() can name the real cause
// instead of falling back to the generic "No provider responded".
+ if (error.status === 401) { this._sessionRepairNotice(); }
this._lastChatError = error.message || String(error);
if (fullText && fullText.length > 0) {
this.terminal.printStreamingResponse(`\n\n[Stream error: ${error.message}]`, true);
@@ -1041,6 +1059,7 @@ class OllamaManager {
if (error.message === 'Failed to fetch' && provider.type === 'ollama') {
return await this.chatWithToolsFallback(messages, tools, useModel);
}
+ if (error.status === 401) { this._sessionRepairNotice(); }
this.terminal.print(`${provider.name} /api/chat error: ${error.message}`, 'error');
this._lastChatError = error.message || String(error);
return { message: null, tool_calls: null, error: error.message };
@@ -1294,6 +1313,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
// ─── Backward-compat methods ─────────────────────────────────────────────
setEndpoint(url) {
+ if (typeof url === 'string') url = url.replace(/\/+$/, '');
this.endpoint = url;
// Also update the current provider's endpoint if it exists
if (this.currentProvider) {