feat(proxy): desktop lockout notification + 'proxy-bridge reauth' command
Notification path (bridge.js): entering auth lockout fires ONE desktop notification per episode (notify-send, critical urgency), only when a session is detectable (DBUS_SESSION_BUS_ADDRESS or DISPLAY) — headless stays silent. Message explains the 407 cause and points at 'proxy-bridge reauth'. Recovery path (proxyctl.js): 'proxy-bridge reauth' prompts masked, verifies the new password with a SINGLE upstream CONNECT (407 -> NOT stored), stores via secret-tool on success, then writes a reauth-stamp.json the bridge watches (1s poll): clears in-memory authState and reloads keyring creds — no restart. Shared one-shot verifier extracted to verify-creds.js (install.js + proxyctl.js use the same code; also fixes proxyctl 'setup' shelling to the retired /opt/setup.js — 'reauth' is its replacement for the password-expiry flow). Tests: +3 bridge (notify once-per-episode incl. remediation hint; headless silent; stamp clears lockout without restart) +2 proxyctl (verified stored + stamped; rejected never stored/stamped). Suite: 28/28.
This commit is contained in:
1 parent
e49b4af14a
commit
be0e2d84cc
6 files changed
+409
-31
No files matched your search
+27
-1
@@ -2,7 +2,7 @@ const http = require('http');
|
||||
const https = require('https');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { execFileSync, spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
const path = require('path');
|
||||
@@ -13,6 +13,7 @@ const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.
|
||||
const PROFILE_FILE = process.env.PROXY_BRIDGE_PROFILE_FILE || '/opt/proxy-bridge/profile.json';
|
||||
const OVERRIDES_FILE = process.env.PROXY_BRIDGE_OVERRIDES_FILE || '/opt/proxy-bridge/overrides.json';
|
||||
const REMOTES_FILE = process.env.PROXY_BRIDGE_REMOTES_FILE || '/opt/proxy-bridge/remotes.json';
|
||||
const REAUTH_FILE = process.env.PROXY_BRIDGE_REAUTH_FILE || '/opt/proxy-bridge/reauth-stamp.json';
|
||||
const PAC_DIR = process.env.PROXY_BRIDGE_PAC_DIR || path.join(os.homedir(), '.mozilla');
|
||||
const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
|
||||
const BASE_PORT = Number(process.env.PROXY_BRIDGE_BASE_PORT || 8888);
|
||||
@@ -129,9 +130,23 @@ function recordAuthFailure(key) {
|
||||
if (st.failures.length >= AUTH_LOCK_THRESHOLD && st.lockedUntil <= now) {
|
||||
st.lockedUntil = now + AUTH_LOCK_BACKOFF_MS;
|
||||
log('WARN', 'auth', 'repeated 407 from upstream; withholding credentials until backoff expires (check keyring / password expiry)', { upstream: key, failures: st.failures.length, backoffMs: AUTH_LOCK_BACKOFF_MS });
|
||||
notifyAuthLockout(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop notification on lockout entry. One per episode (callers gate on the
|
||||
// lockedUntil transition). Silent no-op without a desktop session; never throws.
|
||||
function notifyAuthLockout(key) {
|
||||
if (!process.env.DBUS_SESSION_BUS_ADDRESS && !process.env.DISPLAY) return;
|
||||
const summary = 'Proxy Bridge: upstream authentication rejected';
|
||||
const body = `Proxy ${key} keeps answering 407 (expired credentials?). Auth withheld for ${Math.round(AUTH_LOCK_BACKOFF_MS / 60000)} min to protect the account. Fix: run 'proxy-bridge reauth' or 'node install.js'.`;
|
||||
try {
|
||||
const child = spawn('notify-send', ['--urgency=critical', '--app-name=proxy-bridge', summary, body], { detached: true, stdio: 'ignore' });
|
||||
child.on('error', () => {});
|
||||
child.unref();
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function recordAuthSuccess(key) {
|
||||
const st = authState.get(key);
|
||||
if (st && (st.failures.length > 0 || st.lockedUntil !== 0)) {
|
||||
@@ -294,6 +309,17 @@ function matchOverride(host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A `proxy-bridge reauth` run drops this stamp file; on change we reload the
|
||||
// credentials from the keyring and clear EVERY lockout — no service restart.
|
||||
function onReauthStamp() {
|
||||
try { fs.readFileSync(REAUTH_FILE, 'utf8'); } catch { return; }
|
||||
const hadLocks = authState.size > 0;
|
||||
authState.clear();
|
||||
refreshAuth();
|
||||
log('INFO', 'auth', 'reauth stamp observed; credentials reloaded, lockout state cleared', { cleared: hadLocks });
|
||||
}
|
||||
fs.watchFile(REAUTH_FILE, { interval: 1000 }, onReauthStamp);
|
||||
|
||||
fs.watchFile(PROFILE_FILE, { interval: 1000 }, updateDynamicProfile);
|
||||
fs.watchFile(OVERRIDES_FILE, { interval: 1000 }, loadOverrides);
|
||||
updateDynamicProfile();
|
||||
|
||||
Reference in new issue
Block a user