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
@@ -11,8 +11,9 @@ const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
|
||||
const LISTEN_PORT = Number(process.env.PROXY_BRIDGE_LISTEN_PORT || 8888);
|
||||
const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.json';
|
||||
const CONFIG_FILE = process.env.PROXY_BRIDGE_CONFIG_FILE || '/opt/proxy-bridge/config.json';
|
||||
const PROFILE_FILE = '/opt/proxy-bridge/profile.json';
|
||||
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 REAUTH_FILE = process.env.PROXY_BRIDGE_REAUTH_FILE || '/opt/proxy-bridge/reauth-stamp.json';
|
||||
const LOCAL_PROXY_URL = `http://${LISTEN_HOST}:${LISTEN_PORT}`;
|
||||
const VSCODE_CERT_FLAG = ''; // e.g. '--ignore-certificate-errors'
|
||||
|
||||
@@ -211,6 +212,51 @@ function setup() {
|
||||
child.on('exit', (code) => process.exit(code || 0));
|
||||
}
|
||||
|
||||
// reauth: replace the stored upstream password after an expiry/407 lockout.
|
||||
// The new password is verified ONCE against the upstream before it ever touches
|
||||
// the keyring; on success we drop a stamp file the bridge watches (clears the
|
||||
// auth-lockout state within ~1s, no service restart).
|
||||
async function reauth() {
|
||||
const readline = require('readline');
|
||||
const config = (() => { try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } catch { return null; } })();
|
||||
if (!config || config.enabled === false || !config.host) {
|
||||
console.error('reauth: upstream proxy is disabled or unconfigured.');
|
||||
process.exit(1);
|
||||
}
|
||||
const username = (() => { try { return JSON.parse(fs.readFileSync(USER_FILE, 'utf8')).username; } catch { return null; } })();
|
||||
if (!username) {
|
||||
console.error(`reauth: no username in ${USER_FILE}. Run 'node install.js' for initial setup.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const password = await new Promise((resolve) => {
|
||||
const oldWrite = rl._writeToOutput;
|
||||
rl._writeToOutput = function (s) {
|
||||
if (s === '\n' || s === '\r' || s === '\r\n') rl.output.write(s);
|
||||
else if (!s.includes(username)) rl.output.write('*');
|
||||
else rl.output.write(s);
|
||||
};
|
||||
rl.question(`New password for ${username}: `, (a) => { rl._writeToOutput = oldWrite; rl.output.write('\n'); rl.close(); resolve(a.trim()); });
|
||||
});
|
||||
if (!password) { console.error('reauth: empty password, aborted. Nothing was stored.'); process.exit(1); }
|
||||
|
||||
const { verifyProxyCredentials } = require('./verify-creds.js');
|
||||
const verdict = await verifyProxyCredentials(config.host, Number(config.port), username, password);
|
||||
if (verdict === 'ok') {
|
||||
spawnSync('secret-tool', ['store', '--label=Proxy Bridge Credentials', 'service', 'proxy-bridge', 'account', username], { input: password });
|
||||
fs.writeFileSync(REAUTH_FILE, JSON.stringify({ at: Date.now(), user: username }));
|
||||
console.log(`Credentials verified by ${config.host}:${config.port}, stored in keyring, and the bridge was notified (lockout clears within ~1s — no restart).`);
|
||||
return;
|
||||
}
|
||||
if (verdict === 'authfail') {
|
||||
console.error('reauth: upstream rejected these credentials (407). NOT stored — check the password and try again.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.error(`reauth: could not reach ${config.host}:${config.port} to verify. Credentials NOT stored (retry when online).`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function config() {
|
||||
const all = getAvailableProfiles();
|
||||
console.log(`listen_host: ${LISTEN_HOST}`);
|
||||
@@ -360,6 +406,7 @@ Commands:
|
||||
enable Enable the user service at login
|
||||
disable Disable the user service at login
|
||||
logs Follow service logs
|
||||
reauth Replace upstream password (verified once, no service restart)
|
||||
setup Store or update keyring credentials and upstream config
|
||||
config Print effective non-secret configuration
|
||||
vscode Manage VS Code proxy settings
|
||||
@@ -402,6 +449,7 @@ async function main() {
|
||||
case 'enable':
|
||||
case 'disable': control(command); break;
|
||||
case 'logs': logs(); break;
|
||||
case 'reauth': await reauth(); break;
|
||||
case 'setup': setup(); break;
|
||||
case 'config': config(); break;
|
||||
case 'vscode': await vscode(process.argv.slice(3)); break;
|
||||
|
||||
Reference in new issue
Block a user