feat(proxy): idempotent interactive installer; retire credential-embedding legacy scripts
install.js (node, replaces install_proxy.sh + setup.js + update_*.sh + get_proxy.sh): - sections: preflight, deploy, config import/create, credentials, legacy migration, systemd unit, system-wide proxy (bashrc/apt/docker/snapd/ollama), verify - idempotent: re-run converges; tests prove a second non-interactive run shows zero CHANGED - imports existing /opt configs non-destructively; asks before changing - credentials: prompted when keyring entry missing; verified with ONE CONNECT round-trip before storing; 407 responses are never stored (anti-lockout); unreachable upstream stores nothing - legacy migration: removes bashrc #PROXY_A93JK2 line + plaintext-cred files, rewrites cred-embedding apt/snap/service drop-ins to the bridge URL - flags: --dry-run, --non-interactive (fails listing missing info), --no-systemd Tests cover dry-run-no-op, idempotency, verify-store-on-200, refuse-on-407.
This commit is contained in:
1 parent
c514f32661
commit
3f9be30784
8 files changed
+711
-380
No files matched your search
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
proxy_ip=$(env | grep http_proxy | grep -Pio '[^@/:]+(?=:\d+/?$)')
|
||||
proxy_port=$(env | grep http_proxy | grep -Pio '(?<=:)(\d+)(?=/?$)')
|
||||
|
||||
if [ -z "${proxy_ip}" ]; then
|
||||
echo "Enter Proxy IP or Hostname (no port): "
|
||||
read proxy_ip
|
||||
else
|
||||
echo "Using >>${proxy_ip}<< as Proxy-Address"
|
||||
fi
|
||||
|
||||
if [ -z "${proxy_port}" ]; then
|
||||
echo -n "Proxy-Port: "
|
||||
read proxy_port
|
||||
else
|
||||
echo "Using >>${proxy_port}<< as Proxy-Port"
|
||||
fi
|
||||
|
||||
echo "Using ${proxy_ip}:${proxy_port} as Proxy!"
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Proxy Bridge installer — idempotent, interactive where information is missing.
|
||||
// Replaces the retired legacy scripts (install_proxy.sh, update_apt_proxy.sh,
|
||||
// update_bashrc_proxy.sh, update_service_proxy.sh, get_proxy.sh, setup.js).
|
||||
//
|
||||
// node install.js interactive install/repair (re-runnable)
|
||||
// node install.js --dry-run print the plan, change nothing
|
||||
// node install.js --non-interactive use existing/defaults; fail listing missing info
|
||||
// node install.js --no-systemd skip systemd user service + system service steps
|
||||
//
|
||||
// Hard rules honored here:
|
||||
// - Passwords are NEVER printed, logged, or stored in a file. Username goes to
|
||||
// user.json; the password goes to the keyring via secret-tool only AFTER a
|
||||
// single successful verification against the upstream (never on failure).
|
||||
// - Keyring presence is probed by `secret-tool lookup`'s exit code only.
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
const { spawnSync, execFileSync } = require('child_process');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI parsing + paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const FLAGS = {
|
||||
dryRun: args.includes('--dry-run'),
|
||||
nonInteractive: args.includes('--non-interactive'),
|
||||
noSystemd: args.includes('--no-systemd'),
|
||||
};
|
||||
|
||||
const BRIDGE_DIR = process.env.PROXY_BRIDGE_DIR || '/opt/proxy-bridge';
|
||||
const BIN_DIR = process.env.PROXY_BRIDGE_BIN_DIR || path.join(os.homedir(), '.local', 'bin');
|
||||
const SYSTEMD_USER_DIR = process.env.PROXY_BRIDGE_SYSTEMD_USER_DIR || path.join(os.homedir(), '.config', 'systemd', 'user');
|
||||
const SRC_DIR = __dirname; // install.js lives next to bridge.js/proxyctl.js in the repo
|
||||
const BRIDGE_URL = 'http://127.0.0.1:8888/';
|
||||
const BASHRC = process.env.PROXY_BRIDGE_BASHRC || path.join(os.homedir(), '.bashrc');
|
||||
const APT_CONF = process.env.PROXY_BRIDGE_APT_CONF || '/etc/apt/apt.conf.d/80proxy-bridge';
|
||||
|
||||
const RUNTIME_FILES = ['config.json', 'user.json', 'profile.json', 'overrides.json', 'remotes.json'];
|
||||
const CODE_FILES = ['bridge.js', 'proxyctl.js'];
|
||||
|
||||
const sectionResults = []; // { section, status: CHANGED|OK|SKIPPED|FAILED, detail }
|
||||
function report(section, status, detail = '') {
|
||||
sectionResults.push({ section, status, detail });
|
||||
const color = { CHANGED: '\x1b[33m', OK: '\x1b[32m', SKIPPED: '\x1b[36m', FAILED: '\x1b[31m' }[status] || '';
|
||||
console.log(`${color}[${status}]\x1b[0m ${section}${detail ? ` — ${detail}` : ''}`);
|
||||
}
|
||||
|
||||
function run(cmd, args, opts = {}) {
|
||||
if (FLAGS.dryRun && !opts.readOnly) {
|
||||
return { status: 0, stdout: '', stderr: '', dryRun: true };
|
||||
}
|
||||
return spawnSync(cmd, args, { encoding: 'utf8', ...opts });
|
||||
}
|
||||
|
||||
function sudoOk() {
|
||||
const r = run('sudo', ['-n', 'true'], { readOnly: true });
|
||||
return r.status === 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interactive prompting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let rl = null;
|
||||
function getRl() {
|
||||
if (!rl) rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
return rl;
|
||||
}
|
||||
|
||||
function ask(question, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
getRl().question(question + (defaultValue ? ` [${defaultValue}]: ` : ': '), (a) => {
|
||||
resolve(a.trim() || defaultValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function askSecret(question) {
|
||||
return new Promise((resolve) => {
|
||||
const r = getRl();
|
||||
const oldWrite = r._writeToOutput;
|
||||
r._writeToOutput = function (s) {
|
||||
if (s === '\n' || s === '\r' || s === '\r\n') r.output.write(s);
|
||||
else if (s.includes(question)) r.output.write(s); // initial prompt
|
||||
else r.output.write('*');
|
||||
};
|
||||
r.question(question + ': ', (a) => { r._writeToOutput = oldWrite; r.output.write('\n'); resolve(a.trim()); });
|
||||
});
|
||||
}
|
||||
|
||||
async function askConfirm(question, defaultYes = true) {
|
||||
if (FLAGS.nonInteractive) return defaultYes;
|
||||
const a = (await ask(`${question} (y/n)`, defaultYes ? 'y' : 'n')).toLowerCase();
|
||||
return a === 'y';
|
||||
}
|
||||
|
||||
function requireInfo(name, value) {
|
||||
if (value) return value;
|
||||
throw new Error(`MISSING_INFO:${name}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: preflight
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sectionPreflight() {
|
||||
const S = 'Preflight';
|
||||
if (process.getuid && process.getuid() === 0) {
|
||||
report(S, 'FAILED', 'run as your user, not root — sudo is invoked per-step when needed');
|
||||
return false;
|
||||
}
|
||||
const nodeV = run('node', ['--version'], { readOnly: true });
|
||||
if (nodeV.status !== 0) { report(S, 'FAILED', 'node not on PATH'); return false; }
|
||||
const st = run('which', ['secret-tool'], { readOnly: true });
|
||||
if (st.status !== 0) {
|
||||
if (FLAGS.nonInteractive) { report(S, 'FAILED', 'libsecret-tools missing (apt install -y libsecret-tools)'); return false; }
|
||||
report(S, 'CHANGED', 'libsecret-tools missing — will attempt install (sudo apt-get install -y libsecret-tools)');
|
||||
run('sudo', ['apt-get', 'install', '-y', 'libsecret-tools', '-qq']);
|
||||
if (run('which', ['secret-tool'], { readOnly: true }).status !== 0) {
|
||||
report(S, 'FAILED', 'libsecret-tools install failed');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
report(S, 'OK', `node ${nodeV.stdout.trim()}, secret-tool present`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: deploy code to BRIDGE_DIR
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sectionDeploy() {
|
||||
const S = 'Deploy code';
|
||||
let changed = false;
|
||||
|
||||
if (!FLAGS.dryRun) {
|
||||
fs.mkdirSync(BRIDGE_DIR, { recursive: true });
|
||||
}
|
||||
for (const f of CODE_FILES) {
|
||||
const src = path.join(SRC_DIR, f);
|
||||
const dst = path.join(BRIDGE_DIR, f);
|
||||
const srcBuf = fs.readFileSync(src);
|
||||
let dstBuf = null;
|
||||
try { dstBuf = fs.readFileSync(dst); } catch {}
|
||||
if (!dstBuf || !srcBuf.equals(dstBuf)) {
|
||||
changed = true;
|
||||
if (!FLAGS.dryRun) {
|
||||
fs.copyFileSync(src, dst);
|
||||
if (f === 'proxyctl.js') fs.chmodSync(dst, 0o755);
|
||||
}
|
||||
}
|
||||
}
|
||||
const cliLink = path.join(BIN_DIR, 'proxy-bridge');
|
||||
const wantTarget = path.join(BRIDGE_DIR, 'proxyctl.js');
|
||||
let current = null;
|
||||
try { current = fs.readlinkSync(cliLink); } catch {}
|
||||
if (current !== wantTarget) {
|
||||
changed = true;
|
||||
if (!FLAGS.dryRun) {
|
||||
fs.mkdirSync(BIN_DIR, { recursive: true });
|
||||
try { fs.unlinkSync(cliLink); } catch {}
|
||||
fs.symlinkSync(wantTarget, cliLink);
|
||||
}
|
||||
}
|
||||
report(S, changed ? 'CHANGED' : 'OK', changed ? 'code deployed to ' + BRIDGE_DIR : 'bridge.js/proxyctl.js identical, CLI symlink in place');
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: import / create runtime config (non-destructive)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
|
||||
function fileExists(p) { try { fs.accessSync(p); return true; } catch { return false; } }
|
||||
|
||||
function keyringHasPassword(username) {
|
||||
if (!username) return false;
|
||||
const r = run('secret-tool', ['lookup', 'service', 'proxy-bridge', 'account', username], { readOnly: true });
|
||||
// Never inspect r.stdout — just the exit code.
|
||||
return r.status === 0 && !r.error;
|
||||
}
|
||||
|
||||
async function sectionConfig() {
|
||||
const S = 'Runtime config';
|
||||
const configPath = path.join(BRIDGE_DIR, 'config.json');
|
||||
const userPath = path.join(BRIDGE_DIR, 'user.json');
|
||||
const existingConfig = readJson(configPath);
|
||||
const existingUser = readJson(userPath);
|
||||
|
||||
// Import existing — show, ask before changing.
|
||||
if (existingConfig && existingConfig.host) {
|
||||
const enabled = existingConfig.enabled !== false;
|
||||
report(S, 'OK', `imported existing: ${enabled ? 'enabled' : 'disabled'} upstream ${existingConfig.host}:${existingConfig.port} (kept)`);
|
||||
if (!enabled) return { enabled: false };
|
||||
const username = existingUser && existingUser.username ? existingUser.username : null;
|
||||
return { enabled: true, host: existingConfig.host, port: String(existingConfig.port), username };
|
||||
}
|
||||
|
||||
// Nothing to import — ask.
|
||||
if (FLAGS.nonInteractive) {
|
||||
report(S, 'FAILED', 'no existing config.json to import and --non-interactive set (missing: upstream host/port or profiles mode)');
|
||||
return null;
|
||||
}
|
||||
console.log('\n--- Upstream proxy setup (no existing configuration found) ---');
|
||||
const enable = await askConfirm('Enable corporate upstream proxy?', true);
|
||||
if (!enable) {
|
||||
const cfg = { enabled: false };
|
||||
if (!FLAGS.dryRun) fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
||||
report(S, 'CHANGED', 'wrote config.json with upstream disabled');
|
||||
return { enabled: false };
|
||||
}
|
||||
const host = requireInfo('upstream-host', await ask('Corporate proxy hostname'));
|
||||
const port = await ask('Corporate proxy port', '8080');
|
||||
const username = requireInfo('username', await ask('Corporate username'));
|
||||
const cfg = { enabled: true, host, port };
|
||||
if (!FLAGS.dryRun) {
|
||||
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
||||
fs.writeFileSync(userPath, JSON.stringify({ username }));
|
||||
}
|
||||
report(S, 'CHANGED', `wrote config.json (${host}:${port}) + user.json`);
|
||||
return { enabled: true, host, port: String(port), username };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: credentials — verify before storing (anti-lockout)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// One single CONNECT attempt with candidate creds directly against the upstream.
|
||||
// NEVER retries, NEVER prints the password, resolves: ok | authfail | unreachable
|
||||
function verifyCredentials(host, port, username, password) {
|
||||
return new Promise((resolve) => {
|
||||
const net = require('net');
|
||||
const socket = net.connect(Number(port), host);
|
||||
let buf = Buffer.alloc(0);
|
||||
const finish = (r) => { try { socket.destroy(); } catch {} resolve(r); };
|
||||
socket.setTimeout(10000, () => finish('unreachable'));
|
||||
socket.once('connect', () => {
|
||||
const auth = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
||||
socket.write(`CONNECT 127.0.0.1:443 HTTP/1.1\r\nHost: 127.0.0.1:443\r\nProxy-Authorization: ${auth}\r\n\r\n`);
|
||||
});
|
||||
socket.on('data', (d) => {
|
||||
buf = Buffer.concat([buf, d]);
|
||||
const head = buf.toString('latin1').split('\r\n')[0] || '';
|
||||
const m = head.match(/^HTTP\/\d\.\d\s+(\d{3})/i);
|
||||
if (m || buf.includes('\r\n\r\n')) {
|
||||
const code = m ? Number(m[1]) : 0;
|
||||
// 200/4xx non-407 (e.g. 403 blocked target) = creds accepted; 407 = rejected.
|
||||
finish(code === 407 ? 'authfail' : (code > 0 ? 'ok' : 'unreachable'));
|
||||
}
|
||||
});
|
||||
socket.once('error', () => finish('unreachable'));
|
||||
socket.once('end', () => { if (buf.length === 0) finish('unreachable'); });
|
||||
});
|
||||
}
|
||||
|
||||
async function sectionCredentials(config) {
|
||||
const S = 'Credentials';
|
||||
if (!config || !config.enabled) { report(S, 'SKIPPED', 'upstream disabled'); return true; }
|
||||
|
||||
const username = config.username;
|
||||
if (username && keyringHasPassword(username)) {
|
||||
report(S, 'OK', `keyring entry present for account (not displayed)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (FLAGS.nonInteractive) {
|
||||
report(S, 'FAILED', `keyring has no password for the configured account and --non-interactive set (missing: password)`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('\n--- Credential setup (one verification round-trip, result never stored on failure) ---');
|
||||
const user = username || requireInfo('username', await ask('Corporate username'));
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
const pass = await askSecret(`Corporate password for ${user} (attempt ${attempt}/3)`);
|
||||
if (!pass) { console.log(' empty password, try again'); continue; }
|
||||
const verdict = await verifyCredentials(config.host, config.port, user, pass);
|
||||
if (verdict === 'ok') {
|
||||
if (!FLAGS.dryRun) {
|
||||
execFileSync('secret-tool', ['store', '--label=Proxy Bridge Credentials', 'service', 'proxy-bridge', 'account', user], { input: pass });
|
||||
}
|
||||
report(S, attempt > 1 ? 'CHANGED' : 'OK', 'credentials verified against upstream and stored in keyring');
|
||||
return true;
|
||||
}
|
||||
if (verdict === 'authfail') console.log(' upstream rejected these credentials (407). Not stored. Check the password.');
|
||||
else console.log(' could not reach the upstream proxy to verify. Credentials NOT stored (retry when online).');
|
||||
if (verdict === 'unreachable') return false; // don't re-prompt if we can't even verify
|
||||
}
|
||||
report(S, 'FAILED', 'credentials could not be verified after 3 attempts — nothing stored');
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: legacy artifact migration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function sectionLegacy() {
|
||||
const S = 'Legacy migration';
|
||||
const findings = [];
|
||||
|
||||
// bashrc: legacy marker sourcing ~/.http_proxy (cred-bearing file from update_bashrc_proxy.sh)
|
||||
let bashrc = '';
|
||||
try { bashrc = fs.readFileSync(BASHRC, 'utf8'); } catch {}
|
||||
const legacyLineRe = /^.*#PROXY_A93JK2.*$/m;
|
||||
const legacyMatch = bashrc.match(legacyLineRe);
|
||||
const credFiles = ['.http_proxy', '.http_proxy.off', '.http_noproxy'].map(f => path.join(os.homedir(), f)).filter(fileExists);
|
||||
if (legacyMatch || credFiles.length > 0) {
|
||||
findings.push({ what: 'bashrc #PROXY_A93JK2 block' + (credFiles.length ? ` + ${credFiles.join(', ')} (contain PLAINTEXT credentials)` : ''), fix: 'remove legacy line' + (credFiles.length ? ' and delete cred files' : '') });
|
||||
}
|
||||
|
||||
// apt legacy file with userinfo
|
||||
const aptLegacy = '/etc/apt/apt.conf.d/80proxy';
|
||||
let aptLegacyHasCreds = false;
|
||||
try {
|
||||
const content = fs.readFileSync(aptLegacy, 'utf8');
|
||||
aptLegacyHasCreds = /:\/\/[^/\s]+:[^@\s]+@/.test(content); // presence only
|
||||
} catch {}
|
||||
if (aptLegacyHasCreds) findings.push({ what: `${aptLegacy} (contains embedded credentials)`, fix: 'delete (80proxy-bridge supersedes)' });
|
||||
|
||||
// snap proxy with userinfo
|
||||
const snapGet = run('snap', ['get', 'system', 'proxy.http', 'proxy.https'], { readOnly: true });
|
||||
if (snapGet.status === 0 && /:\/\/[^/\s]+:[^@\s]+@/.test(snapGet.stdout)) {
|
||||
findings.push({ what: 'snap system proxy.* (contain embedded credentials)', fix: 'point at bridge URL without creds (needs sudo)' });
|
||||
}
|
||||
|
||||
// systemd service drop-ins with userinfo
|
||||
for (const svc of ['docker', 'snapd', 'ollama']) {
|
||||
const f = `/etc/systemd/system/${svc}.service.d/http-proxy.conf`;
|
||||
try {
|
||||
const content = fs.readFileSync(f, 'utf8');
|
||||
if (/:\/\/[^/\s]+:[^@\s]+@/.test(content)) {
|
||||
findings.push({ what: `${f} (contains embedded credentials)`, fix: `rewrite to bridge URL (needs sudo)` });
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (findings.length === 0) { report(S, 'OK', 'no legacy credential-embedding artifacts found'); return true; }
|
||||
|
||||
console.log('\nLegacy artifacts found:');
|
||||
findings.forEach((f, i) => console.log(` ${i + 1}. ${f.what}\n fix: ${f.fix}`));
|
||||
const proceed = await askConfirm('Migrate/remove these now?', true);
|
||||
if (!proceed) { report(S, 'SKIPPED', 'user deferred legacy migration'); return true; }
|
||||
|
||||
let changed = false;
|
||||
if (legacyMatch) {
|
||||
if (!FLAGS.dryRun) {
|
||||
const cleaned = bashrc.split('\n').filter((line, idx, arr) => {
|
||||
if (line.includes('#PROXY_A93JK2')) return false;
|
||||
return true;
|
||||
}).join('\n');
|
||||
fs.writeFileSync(BASHRC, cleaned);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
if (credFiles.length > 0) {
|
||||
const del = await askConfirm(`Delete plaintext-credential file(s) ${credFiles.join(', ')}?`, true);
|
||||
if (del && !FLAGS.dryRun) credFiles.forEach(f => fs.rmSync(f, { force: true }));
|
||||
changed = changed || del;
|
||||
}
|
||||
if (aptLegacyHasCreds) {
|
||||
if (!FLAGS.dryRun) run('sudo', ['rm', '-f', aptLegacy]);
|
||||
changed = true;
|
||||
}
|
||||
if (findings.some(f => f.what.startsWith('snap system'))) {
|
||||
if (!FLAGS.dryRun) {
|
||||
run('sudo', ['snap', 'set', 'system', `proxy.http=${BRIDGE_URL}`, `proxy.https=${BRIDGE_URL}`]);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
for (const svc of ['docker', 'snapd', 'ollama']) {
|
||||
const f = `/etc/systemd/system/${svc}.service.d/http-proxy.conf`;
|
||||
try {
|
||||
const content = fs.readFileSync(f, 'utf8');
|
||||
if (/:\/\/[^/\s]+:[^@\s]+@/.test(content)) {
|
||||
if (!FLAGS.dryRun) {
|
||||
const clean = `[Service]\nEnvironment="HTTP_PROXY=${BRIDGE_URL}"\nEnvironment="HTTPS_PROXY=${BRIDGE_URL}"\nEnvironment="NO_PROXY=localhost,127.0.0.1,::1"\n`;
|
||||
fs.writeFileSync(`/tmp/proxy-bridge-dropin.tmp`, clean);
|
||||
run('sudo', ['cp', `/tmp/proxy-bridge-dropin.tmp`, f]);
|
||||
fs.rmSync(`/tmp/proxy-bridge-dropin.tmp`, { force: true });
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
report(S, changed ? 'CHANGED' : 'OK', changed ? `migrated ${findings.length} artifact(s)` : 'no changes needed');
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: systemd user service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sectionUnit() {
|
||||
const S = 'systemd service';
|
||||
if (FLAGS.noSystemd) { report(S, 'SKIPPED', '--no-systemd'); return true; }
|
||||
const unitPath = path.join(SYSTEMD_USER_DIR, 'proxy-bridge.service');
|
||||
const nodeBin = process.execPath;
|
||||
const want = `[Unit]\nDescription=Local Proxy Bridge (Keyring Auth)\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=${nodeBin} ${BRIDGE_DIR}/bridge.js\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
|
||||
let current = null;
|
||||
try { current = fs.readFileSync(unitPath, 'utf8'); } catch {}
|
||||
if (current === want) {
|
||||
report(S, 'OK', 'unit file up to date');
|
||||
return true;
|
||||
}
|
||||
if (!FLAGS.dryRun) {
|
||||
fs.mkdirSync(SYSTEMD_USER_DIR, { recursive: true });
|
||||
fs.writeFileSync(unitPath, want);
|
||||
run('systemctl', ['--user', 'daemon-reload']);
|
||||
run('systemctl', ['--user', 'enable', 'proxy-bridge.service']);
|
||||
}
|
||||
report(S, 'CHANGED', 'unit file written + enabled (restart happens in verify section with your consent)');
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: system-wide proxy point-at-bridge (idempotent)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function sectionSystemWide() {
|
||||
const S = 'System-wide proxy';
|
||||
let changed = false;
|
||||
|
||||
// bashrc bridge block (marker-guarded)
|
||||
let bashrc = '';
|
||||
try { bashrc = fs.readFileSync(BASHRC, 'utf8'); } catch {}
|
||||
const marker = '#PROXY_BRIDGE_CONFIG';
|
||||
const block = `\n# Local Proxy Bridge Configuration ${marker}\nexport http_proxy="${BRIDGE_URL}"\nexport https_proxy="${BRIDGE_URL}"\nexport HTTP_PROXY="${BRIDGE_URL}"\nexport HTTPS_PROXY="${BRIDGE_URL}"\nexport NO_PROXY="localhost,127.0.0.1,::1"\nexport no_proxy="localhost,127.0.0.1,::1"\n`;
|
||||
if (!bashrc.includes(marker)) {
|
||||
changed = true;
|
||||
if (!FLAGS.dryRun) fs.writeFileSync(BASHRC, bashrc.replace(/\n*$/, '\n') + block);
|
||||
}
|
||||
|
||||
// apt conf (needs sudo; content-compare via sudo cat is awkward — check existence + no-userinfo suffices for OK)
|
||||
let aptOk = false;
|
||||
try {
|
||||
const c = fs.readFileSync(APT_CONF, 'utf8');
|
||||
aptOk = c.includes('127.0.0.1:8888') && !/:\/\/[^/\s]+:[^@\s]+@/.test(c);
|
||||
} catch {}
|
||||
if (!aptOk) {
|
||||
const doIt = FLAGS.nonInteractive ? true : await askConfirm(`Write ${APT_CONF} with bridge URL (sudo)?`, true);
|
||||
if (doIt) {
|
||||
changed = true;
|
||||
if (!FLAGS.dryRun) {
|
||||
const tmpW = `/tmp/proxy-bridge-apt.tmp`;
|
||||
fs.writeFileSync(tmpW, `Acquire::http::Proxy "${BRIDGE_URL}";\nAcquire::https::Proxy "${BRIDGE_URL}";\n`);
|
||||
run('sudo', ['cp', tmpW, APT_CONF]);
|
||||
fs.rmSync(tmpW, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// service drop-ins (docker/snapd/ollama) — bridge URL only
|
||||
for (const svc of ['docker', 'snapd', 'ollama']) {
|
||||
const f = `/etc/systemd/system/${svc}.service.d/http-proxy.conf`;
|
||||
let ok = false;
|
||||
try {
|
||||
const c = fs.readFileSync(f, 'utf8');
|
||||
ok = c.includes('127.0.0.1:8888') && !/:\/\/[^/\s]+:[^@\s]+@/.test(c);
|
||||
} catch {}
|
||||
if (!ok) {
|
||||
const svcExists = run('systemctl', ['list-unit-files', `${svc}.service`], { readOnly: true });
|
||||
if (svcExists.status !== 0 || !svcExists.stdout.includes(svc)) continue; // service not installed
|
||||
const doIt = FLAGS.nonInteractive ? true : await askConfirm(`Point ${svc} at the bridge (sudo + restart ${svc})?`, true);
|
||||
if (doIt) {
|
||||
changed = true;
|
||||
if (!FLAGS.dryRun) {
|
||||
const tmpW = `/tmp/proxy-bridge-dropin.tmp`;
|
||||
fs.writeFileSync(tmpW, `[Service]\nEnvironment="HTTP_PROXY=${BRIDGE_URL}"\nEnvironment="HTTPS_PROXY=${BRIDGE_URL}"\nEnvironment="NO_PROXY=localhost,127.0.0.1,::1"\n`);
|
||||
run('sudo', ['mkdir', '-p', path.dirname(f)]);
|
||||
run('sudo', ['cp', tmpW, f]);
|
||||
fs.rmSync(tmpW, { force: true });
|
||||
run('sudo', ['systemctl', 'daemon-reload']);
|
||||
run('sudo', ['systemctl', 'restart', svc]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
report(S, changed ? 'CHANGED' : 'OK', changed ? 'system-wide proxy config updated' : 'bashrc/apt/services already point at the bridge');
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section: verify (service restart happens here, with consent)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function sectionVerify() {
|
||||
const S = 'Verify';
|
||||
if (FLAGS.noSystemd) { report(S, 'SKIPPED', '--no-systemd (no restart/checks)'); return true; }
|
||||
|
||||
const active = run('systemctl', ['--user', 'is-active', 'proxy-bridge'], { readOnly: true }).stdout.trim();
|
||||
if (active !== 'active') {
|
||||
const doIt = FLAGS.nonInteractive ? true : await askConfirm('Start/restart proxy-bridge now? (drops bridged connectivity briefly)', true);
|
||||
if (doIt && !FLAGS.dryRun) {
|
||||
run('systemctl', ['--user', 'restart', 'proxy-bridge']);
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
}
|
||||
}
|
||||
const state = run('systemctl', ['--user', 'is-active', 'proxy-bridge'], { readOnly: true }).stdout.trim();
|
||||
if (state !== 'active') { report(S, 'FAILED', `service state: ${state || 'unknown'}`); return false; }
|
||||
|
||||
// ports listening
|
||||
const ss = run('ss', ['-tln'], { readOnly: true });
|
||||
const ports = [8888, 8889, 8890, 8891];
|
||||
const listening = ports.filter(p => ss.stdout.includes(`127.0.0.1:${p}`));
|
||||
const allCore = listening.length === ports.length;
|
||||
report(S, allCore ? 'OK' : 'FAILED', `service active; core ports listening: ${listening.join(',') || 'none'}`);
|
||||
return allCore;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
console.log(`=== Proxy Bridge installer ${FLAGS.dryRun ? '(DRY RUN) ' : ''}===`);
|
||||
console.log(`target: ${BRIDGE_DIR}`);
|
||||
const missingInfo = [];
|
||||
let credsFailed = false;
|
||||
try {
|
||||
if (!sectionPreflight()) throw new Error('PREFLIGHT_FAILED');
|
||||
if (!sectionDeploy()) throw new Error('DEPLOY_FAILED');
|
||||
const config = await sectionConfig();
|
||||
if (config === null) throw new Error('CONFIG_FAILED');
|
||||
credsFailed = !(await sectionCredentials(config));
|
||||
// Do not abort on credential failure: remaining sections show/prep what
|
||||
// else needs doing, and the summary still exits non-zero.
|
||||
if (!await sectionLegacy()) throw new Error('LEGACY_FAILED');
|
||||
if (!sectionUnit()) throw new Error('UNIT_FAILED');
|
||||
if (!await sectionSystemWide()) throw new Error('SYSWIDE_FAILED');
|
||||
if (!FLAGS.dryRun && !credsFailed) await sectionVerify();
|
||||
else report('Verify', 'SKIPPED', FLAGS.dryRun ? 'dry run' : 'credentials not verified; skipping restart');
|
||||
} catch (e) {
|
||||
if (String(e.message).startsWith('MISSING_INFO:')) {
|
||||
missingInfo.push(e.message.split(':')[1]);
|
||||
} else if (!['PREFLIGHT_FAILED', 'DEPLOY_FAILED', 'CONFIG_FAILED', 'CREDS_FAILED', 'LEGACY_FAILED', 'UNIT_FAILED', 'SYSWIDE_FAILED'].includes(e.message)) {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
if (rl) rl.close();
|
||||
}
|
||||
const failed = sectionResults.filter(r => r.status === 'FAILED');
|
||||
console.log('\n=== Summary ===');
|
||||
sectionResults.forEach(r => console.log(` ${r.status.padEnd(8)} ${r.section}`));
|
||||
if (missingInfo.length) console.log(`\nMissing information for --non-interactive: ${missingInfo.join(', ')}`);
|
||||
if (FLAGS.dryRun) console.log('\nDry run: no changes were made.');
|
||||
process.exit(failed.length > 0 || missingInfo.length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('installer error:', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,157 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { once } = require('node:events');
|
||||
const { spawnSync, spawn } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const net = require('node:net');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const INSTALLER = path.join(__dirname, 'install.js');
|
||||
const PROXY_DIR = __dirname;
|
||||
|
||||
function mkTmp(t) {
|
||||
const dirs = {
|
||||
bridge: fs.mkdtempSync(path.join(os.tmpdir(), 'pi-bridge-')),
|
||||
bin: fs.mkdtempSync(path.join(os.tmpdir(), 'pi-bin-')),
|
||||
systemd: fs.mkdtempSync(path.join(os.tmpdir(), 'pi-systemd-')),
|
||||
home: fs.mkdtempSync(path.join(os.tmpdir(), 'pi-home-')),
|
||||
apt: fs.mkdtempSync(path.join(os.tmpdir(), 'pi-apt-')),
|
||||
};
|
||||
t.after(() => Object.values(dirs).forEach(d => fs.rmSync(d, { recursive: true, force: true })));
|
||||
return dirs;
|
||||
}
|
||||
|
||||
function envFor(dirs) {
|
||||
return {
|
||||
...process.env,
|
||||
PROXY_BRIDGE_DIR: dirs.bridge,
|
||||
PROXY_BRIDGE_BIN_DIR: dirs.bin,
|
||||
PROXY_BRIDGE_SYSTEMD_USER_DIR: dirs.systemd,
|
||||
PROXY_BRIDGE_BASHRC: path.join(dirs.home, '.bashrc'),
|
||||
PROXY_BRIDGE_APT_CONF: path.join(dirs.apt, '80proxy-bridge'),
|
||||
};
|
||||
}
|
||||
|
||||
function runInstaller(t, dirs, extraArgs = []) {
|
||||
const r = spawnSync('node', [INSTALLER, '--no-systemd', ...extraArgs], {
|
||||
env: envFor(dirs),
|
||||
encoding: 'utf8',
|
||||
timeout: 60000,
|
||||
input: '', // no stdin: any interactive prompt must not be reached in these non-interactive paths
|
||||
});
|
||||
r.stdout = String(r.stdout).replace(/\x1b\[[0-9;]*m/g, '');
|
||||
r.stderr = String(r.stderr || '').replace(/\x1b\[[0-9;]*m/g, '');
|
||||
return r;
|
||||
}
|
||||
|
||||
// Async variant: use whenever the test process itself hosts a TCP stub server —
|
||||
// spawnSync would block the event loop and the stub would never answer.
|
||||
function runInstallerAsync(t, dirs, { args = [], stdinLines = [] } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('node', [INSTALLER, '--no-systemd', ...args], { env: envFor(dirs) });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', d => { stdout += d; });
|
||||
child.stderr.on('data', d => { stderr += d; });
|
||||
const timer = setTimeout(() => { child.kill(); reject(new Error('installer hung')); }, 55000);
|
||||
child.on('exit', code => {
|
||||
clearTimeout(timer);
|
||||
resolve({
|
||||
status: code,
|
||||
stdout: stdout.replace(/\x1b\[[0-9;]*m/g, ''),
|
||||
stderr: stderr.replace(/\x1b\[[0-9;]*m/g, ''),
|
||||
});
|
||||
});
|
||||
// Feed stdin with a small delay so readline has attached before lines arrive.
|
||||
let i = 0;
|
||||
const feed = () => {
|
||||
if (i >= stdinLines.length) { try { child.stdin.end(); } catch {} return; }
|
||||
child.stdin.write(stdinLines[i++] + '\n');
|
||||
setTimeout(feed, 150);
|
||||
};
|
||||
setTimeout(feed, 300);
|
||||
});
|
||||
}
|
||||
|
||||
test('dry-run changes nothing on disk', { timeout: 30000 }, async t => {
|
||||
const dirs = mkTmp(t);
|
||||
const r = runInstaller(t, dirs, ['--dry-run', '--non-interactive']);
|
||||
// non-interactive with no imported config -> CONFIG section reports FAILED by design; fine for this test
|
||||
assert.equal(fs.readdirSync(dirs.bridge).length, 0, 'bridge dir must stay empty in dry-run');
|
||||
assert.equal(fs.readdirSync(dirs.bin).length, 0, 'bin dir must stay empty in dry-run');
|
||||
assert.equal(fs.readdirSync(dirs.systemd).length, 0, 'systemd dir must stay empty in dry-run');
|
||||
assert.ok(!fs.existsSync(path.join(dirs.home, '.bashrc')), 'bashrc must not be created in dry-run');
|
||||
assert.match(r.stdout, /Dry run: no changes were made\./);
|
||||
});
|
||||
|
||||
test('import-only run is idempotent: second run reports zero CHANGED', { timeout: 60000 }, async t => {
|
||||
const dirs = mkTmp(t);
|
||||
// Seed an 'imported' config the way the live /opt has it
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'config.json'), JSON.stringify({ enabled: true, host: '127.0.0.1', port: '1' }));
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'user.json'), JSON.stringify({ username: 'seeded-user' }));
|
||||
// Seed bashrc with the EXACT bridge block the installer would write (idempotency target)
|
||||
fs.writeFileSync(path.join(dirs.home, '.bashrc'),
|
||||
`\n# Local Proxy Bridge Configuration #PROXY_BRIDGE_CONFIG\nexport http_proxy="http://127.0.0.1:8888/"\nexport https_proxy="http://127.0.0.1:8888/"\nexport HTTP_PROXY="http://127.0.0.1:8888/"\nexport HTTPS_PROXY="http://127.0.0.1:8888/"\nexport NO_PROXY="localhost,127.0.0.1,::1"\nexport no_proxy="localhost,127.0.0.1,::1"\n`);
|
||||
// Seed the apt conf the installer would want (no sudo needed: our APT_CONF is a tmp path)
|
||||
fs.writeFileSync(path.join(dirs.apt, '80proxy-bridge'),
|
||||
`Acquire::http::Proxy "http://127.0.0.1:8888/";\nAcquire::https::Proxy "http://127.0.0.1:8888/";\n`);
|
||||
|
||||
const r1 = runInstaller(t, dirs, ['--non-interactive']);
|
||||
assert.equal(r1.status, 1, JSON.stringify(r1.stdout + r1.stderr)); // creds missing -> expected FAILED in this seed
|
||||
assert.match(r1.stdout, /\[OK\] Runtime config/);
|
||||
assert.match(r1.stdout, /\[FAILED\] Credentials/); // no keyring entry for seeded-user; correct
|
||||
|
||||
// Second run must show zero CHANGED in the sections that did run
|
||||
const r2 = runInstaller(t, dirs, ['--non-interactive']);
|
||||
const changedLines = r2.stdout.split('\n').filter(l => l.includes('[CHANGED]'));
|
||||
assert.deepEqual(changedLines, [], `second run must be a no-op, got CHANGED: ${changedLines.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('credential verification: 200 from upstream stores the password (junk test account)', { timeout: 60000 }, async t => {
|
||||
const dirs = mkTmp(t);
|
||||
const account = `bridge-test-${process.pid}`;
|
||||
// Stub "upstream" that accepts any creds once
|
||||
const upstream = net.createServer(socket => {
|
||||
socket.once('data', () => {
|
||||
socket.end('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
});
|
||||
});
|
||||
upstream.listen(0, '127.0.0.1');
|
||||
await once(upstream, 'listening');
|
||||
t.after(() => upstream.close());
|
||||
const port = upstream.address().port;
|
||||
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'config.json'), JSON.stringify({ enabled: true, host: '127.0.0.1', port: String(port) }));
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'user.json'), JSON.stringify({ username: account }));
|
||||
fs.writeFileSync(path.join(dirs.home, '.bashrc'), '');
|
||||
|
||||
const r = await runInstallerAsync(t, dirs, { stdinLines: ['junk-test-password-not-real', 'n'] });
|
||||
// creds stored via real secret-tool; clean up regardless of outcome
|
||||
spawnSync('secret-tool', ['clear', 'service', 'proxy-bridge', 'account', account], { encoding: 'utf8' });
|
||||
assert.match(r.stdout, /\[OK\] Credentials|\[CHANGED\] Credentials/);
|
||||
assert.match(r.stdout, /credentials verified against upstream and stored/);
|
||||
});
|
||||
|
||||
test('credential verification: 407 from upstream does NOT store and reports FAILED', { timeout: 60000 }, async t => {
|
||||
const dirs = mkTmp(t);
|
||||
const account = `bridge-test407-${process.pid}`;
|
||||
const upstream = net.createServer(socket => {
|
||||
socket.once('data', () => socket.end('HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'));
|
||||
});
|
||||
upstream.listen(0, '127.0.0.1');
|
||||
await once(upstream, 'listening');
|
||||
t.after(() => upstream.close());
|
||||
const port = upstream.address().port;
|
||||
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'config.json'), JSON.stringify({ enabled: true, host: '127.0.0.1', port: String(port) }));
|
||||
fs.writeFileSync(path.join(dirs.bridge, 'user.json'), JSON.stringify({ username: account }));
|
||||
fs.writeFileSync(path.join(dirs.home, '.bashrc'), '');
|
||||
|
||||
const r = await runInstallerAsync(t, dirs, { stdinLines: ['wrong1', 'wrong2', 'wrong3', 'n'] });
|
||||
// make sure nothing was stored for this account
|
||||
const check = spawnSync('secret-tool', ['lookup', 'service', 'proxy-bridge', 'account', account], { encoding: 'utf8' });
|
||||
assert.notEqual(check.status, 0, 'no keyring entry must exist for rejected creds');
|
||||
assert.match(r.stdout, /\[FAILED\] Credentials/);
|
||||
assert.match(r.stdout, /nothing stored/i);
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- HELPER FUNCTIONS ---
|
||||
function info() { echo -e "\e[34mINFO:\e[0m $1"; }
|
||||
function warn() { echo -e "\e[33mWARN:\e[0m $1"; }
|
||||
function error() { echo -e "\e[31mERROR:\e[0m $1"; exit 1; }
|
||||
function success() { echo -e "\e[32mSUCCESS:\e[0m $1"; }
|
||||
|
||||
# 1. PREREQUISITE CHECKS
|
||||
info "Verifying prerequisites..."
|
||||
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
error "Please run this script as your standard user, not as root.\nThe script will prompt for sudo access automatically when needed."
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
error "Node.js is not installed. Please install Node.js first."
|
||||
fi
|
||||
|
||||
# Check for files
|
||||
[ -f "bridge.js" ] || error "bridge.js not found."
|
||||
[ -f "setup.js" ] || error "setup.js not found."
|
||||
[ -f "proxyctl.js" ] || error "proxyctl.js not found."
|
||||
|
||||
# 2. SYSTEM PACKAGES
|
||||
info "Installing required system packages (libsecret-tools)..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y libsecret-tools -qq
|
||||
|
||||
# 3. DIRECTORY SETUP
|
||||
BRIDGE_DIR="/opt/proxy-bridge"
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
info "Setting up $BRIDGE_DIR..."
|
||||
sudo mkdir -p "$BRIDGE_DIR"
|
||||
sudo chown -R $USER:$USER "$BRIDGE_DIR"
|
||||
|
||||
# 4. DEPLOY FILES
|
||||
info "Deploying bridge scripts..."
|
||||
cp bridge.js "$BRIDGE_DIR/"
|
||||
cp setup.js "$BRIDGE_DIR/"
|
||||
cp proxyctl.js "$BRIDGE_DIR/"
|
||||
chmod +x "$BRIDGE_DIR/proxyctl.js"
|
||||
|
||||
info "Installing management CLI to $BIN_DIR/proxy-bridge..."
|
||||
mkdir -p "$BIN_DIR"
|
||||
ln -sf "$BRIDGE_DIR/proxyctl.js" "$BIN_DIR/proxy-bridge"
|
||||
|
||||
# 5. CONFIGURATION & CREDENTIALS
|
||||
if [ ! -f "$BRIDGE_DIR/config.json" ] || [ ! -f "$BRIDGE_DIR/user.json" ]; then
|
||||
info "Launching interactive setup..."
|
||||
node "$BRIDGE_DIR/setup.js"
|
||||
fi
|
||||
|
||||
# 6. SYSTEMD SERVICE SETUP
|
||||
info "Configuring user-level systemd service..."
|
||||
mkdir -p ~/.config/systemd/user/
|
||||
|
||||
cat <<EOF > ~/.config/systemd/user/proxy-bridge.service
|
||||
[Unit]
|
||||
Description=Local Proxy Bridge (Keyring Auth)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=$(command -v node) $BRIDGE_DIR/bridge.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable proxy-bridge.service
|
||||
systemctl --user restart proxy-bridge.service
|
||||
|
||||
# 7. SYSTEM-WIDE PROXY CONFIGURATION (Point to local bridge)
|
||||
BRIDGE_URL="http://127.0.0.1:8888/"
|
||||
|
||||
# 7a. APT Configuration
|
||||
info "Updating APT proxy configuration..."
|
||||
APT_CONF="/etc/apt/apt.conf.d/80proxy-bridge"
|
||||
echo "Acquire::http::Proxy \"$BRIDGE_URL\";" | sudo tee "$APT_CONF" > /dev/null
|
||||
echo "Acquire::https::Proxy \"$BRIDGE_URL\";" | sudo tee -a "$APT_CONF" > /dev/null
|
||||
|
||||
# 7b. Bashrc Configuration
|
||||
info "Updating .bashrc proxy settings..."
|
||||
BASHRC="$HOME/.bashrc"
|
||||
MARKER="#PROXY_BRIDGE_CONFIG"
|
||||
|
||||
if ! grep -q "$MARKER" "$BASHRC"; then
|
||||
cat <<EOF >> "$BASHRC"
|
||||
|
||||
# Local Proxy Bridge Configuration $MARKER
|
||||
export http_proxy="$BRIDGE_URL"
|
||||
export https_proxy="$BRIDGE_URL"
|
||||
export HTTP_PROXY="$BRIDGE_URL"
|
||||
export HTTPS_PROXY="$BRIDGE_URL"
|
||||
export NO_PROXY="localhost,127.0.0.1,::1"
|
||||
export no_proxy="localhost,127.0.0.1,::1"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# 7c. System Services (Docker, Snap, etc.)
|
||||
SERVICES=(docker snapd ollama)
|
||||
info "Updating system services proxy configuration..."
|
||||
|
||||
for service in "${SERVICES[@]}"; do
|
||||
if systemctl list-unit-files | grep -q "^${service}.service"; then
|
||||
info "Configuring $service..."
|
||||
SERVICE_DIR="/etc/systemd/system/${service}.service.d"
|
||||
sudo mkdir -p "$SERVICE_DIR"
|
||||
cat <<EOF | sudo tee "$SERVICE_DIR/http-proxy.conf" > /dev/null
|
||||
[Service]
|
||||
Environment="HTTP_PROXY=$BRIDGE_URL"
|
||||
Environment="HTTPS_PROXY=$BRIDGE_URL"
|
||||
Environment="NO_PROXY=localhost,127.0.0.1,::1"
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart "$service" 2>/dev/null || warn "Failed to restart $service (maybe not running?)"
|
||||
fi
|
||||
done
|
||||
|
||||
success "Installation and system-wide configuration complete!"
|
||||
info "Management command: proxy-bridge status|toggle|profile"
|
||||
info "The Proxy Bridge is running and configured for APT, Bash, and core services."
|
||||
info "Logs: journalctl --user -u proxy-bridge.service -f"
|
||||
@@ -1,105 +0,0 @@
|
||||
const readline = require('readline');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const CONFIG_DIR = '/opt/proxy-bridge';
|
||||
const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
|
||||
const USER_FILE = `${CONFIG_DIR}/user.json`;
|
||||
|
||||
console.log("=== Proxy Bridge Configuration Setup ===");
|
||||
|
||||
function ask(question, defaultValue = "") {
|
||||
return new Promise((resolve) => {
|
||||
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
||||
rl.question(prompt, (answer) => {
|
||||
resolve(answer.trim() || defaultValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// 1. Upstream Proxy Configuration
|
||||
console.log("\n--- Upstream Proxy (Corporate) ---");
|
||||
const enableUpstream = (await ask("Enable Corporate Upstream Proxy? (y/n)", "y")).toLowerCase() === 'y';
|
||||
|
||||
let host = "";
|
||||
let port = "8080";
|
||||
let user = "";
|
||||
let pass = "";
|
||||
|
||||
if (enableUpstream) {
|
||||
host = await ask("Corporate Proxy Hostname (e.g. proxy.company.com)");
|
||||
port = await ask("Corporate Proxy Port", "8080");
|
||||
|
||||
if (!host) {
|
||||
console.error("❌ Hostname is required if upstream is enabled.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 2. Credentials
|
||||
console.log("\n--- Credentials ---");
|
||||
user = await ask("Corporate Username");
|
||||
|
||||
// Setup password mask (temporary swap)
|
||||
const oldWrite = rl._writeToOutput;
|
||||
rl._writeToOutput = function _writeToOutput(stringToWrite) {
|
||||
// If it's a newline or specific strings, let it through
|
||||
if (stringToWrite === "\n" || stringToWrite === "\r" || stringToWrite === "\r\n") {
|
||||
rl.output.write(stringToWrite);
|
||||
} else {
|
||||
rl.output.write("*");
|
||||
}
|
||||
};
|
||||
pass = await ask("Corporate Password");
|
||||
rl._writeToOutput = oldWrite;
|
||||
}
|
||||
|
||||
// Save Config
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify({
|
||||
enabled: enableUpstream,
|
||||
host: host,
|
||||
port: port
|
||||
}, null, 2));
|
||||
|
||||
if (enableUpstream) {
|
||||
// Save Username
|
||||
fs.writeFileSync(USER_FILE, JSON.stringify({ username: user }));
|
||||
|
||||
// Store Password in Keyring
|
||||
console.log("\n--> Storing password in system keyring...");
|
||||
try {
|
||||
execFileSync('secret-tool', [
|
||||
'store',
|
||||
'--label=Proxy Bridge Credentials',
|
||||
'service',
|
||||
'proxy-bridge',
|
||||
'account',
|
||||
user,
|
||||
], {
|
||||
input: pass
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("⚠️ Warning: Failed to store password in keyring. Ensure libsecret-tools is installed.");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n✅ Configuration successfully updated.");
|
||||
} catch (error) {
|
||||
console.error("\n❌ Setup failed:", error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
config_file='/etc/apt/apt.conf.d/80proxy'
|
||||
#remove proxy settings from docker
|
||||
if [[ "${1}" == "off" ]]; then
|
||||
echo "TURNING OFF PROXY FOR APT"
|
||||
sudo rm -rf "${config_file}"
|
||||
sudo snap unset system proxy.http
|
||||
sudo snap unset system proxy.https
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#populate proxy_ip and proxy_port variables
|
||||
path=$(dirname $(readlink -f "${0}"))
|
||||
. "${path}/get_proxy.sh"
|
||||
|
||||
|
||||
echo -n "Username: "
|
||||
read username
|
||||
echo -n "Password: "
|
||||
read -s password
|
||||
encpassword=$(echo -n ${password} | xxd -p | sed -e 's/\(..\)/%\1/g' )
|
||||
echo 'Acquire::http::Proxy "http://'"${username}:${encpassword}@${proxy_ip}:${proxy_port}/"'";' | sudo tee "${config_file}"
|
||||
echo 'Acquire::http::Timeout "360";' | sudo tee -a "${config_file}"
|
||||
|
||||
echo "SETTING PROXY FOR SNAP ASWELL"
|
||||
sudo snap set system proxy.http=http://${username}:${encpassword}@${proxy_ip}:${proxy_port}/
|
||||
sudo snap set system proxy.https=http://${username}:${encpassword}@${proxy_ip}:${proxy_port}/
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
user=$(logname)
|
||||
bashrc_file=/home/${user}/.bashrc
|
||||
proxy_file=/home/${user}/.http_proxy
|
||||
noproxy_file=/home/${user}/.http_noproxy
|
||||
|
||||
#turn off bashrc proxy settings
|
||||
if [[ "${1}" == "off" ]]; then
|
||||
echo "TURNING OFF PROXY IN BASHRC"
|
||||
mv -v "${proxy_file}" "${proxy_file}.off"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#turn on bashrc proxy settings
|
||||
if [[ "${1}" == "on" ]]; then
|
||||
if [[ -f "${proxy_file}.off" ]] ; then
|
||||
echo "TURNING ON PROXY IN BASHRC"
|
||||
mv -v "${proxy_file}.off" "${proxy_file}"
|
||||
exit 0
|
||||
else
|
||||
echo "No disabled Proxy-Config found. Creating a new one!"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
marker="#PROXY_A93JK2"
|
||||
path=$(dirname $(readlink -f "${0}"))
|
||||
. "${path}/get_proxy.sh"
|
||||
echo -n "Username: "
|
||||
read username
|
||||
echo -n "Password: "
|
||||
read -s password
|
||||
|
||||
|
||||
encpassword=$(echo -n ${password} | xxd -p | sed -e 's/\(..\)/%\1/g' )
|
||||
|
||||
if ! grep -qF -e "PROXY_A93JK2" "${bashrc_file}" ; then
|
||||
echo "[ -f ${proxy_file} ] && . ${proxy_file} #PROXY_A93JK2" >> "${bashrc_file}"
|
||||
fi
|
||||
|
||||
echo 'export "HTTP_PROXY=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" > "${proxy_file}"
|
||||
echo 'export "HTTPS_PROXY=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" >> "${proxy_file}"
|
||||
echo 'export "http_proxy=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" >> "${proxy_file}"
|
||||
echo 'export "https_proxy=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" >> "${proxy_file}"
|
||||
echo 'export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt' >> "${proxy_file}"
|
||||
echo "[ -f ${noproxy_file} ] && . ${noproxy_file}" >> "${proxy_file}"
|
||||
|
||||
[ -f "${noproxy_file}" ] || echo 'export "NO_PROXY=localhost,127.0.0.1"' > "${noproxy_file}"
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
services=(docker snapd ollama)
|
||||
|
||||
function restart_services(){
|
||||
echo "Reloading Systemd - Configfiles"
|
||||
sudo systemctl daemon-reload
|
||||
for service in ${services[*]} ; do
|
||||
echo "Restarting Service $service"
|
||||
sudo systemctl restart $service
|
||||
done
|
||||
}
|
||||
|
||||
#remove proxy settings from docker
|
||||
if [[ "${1}" == "off" ]]; then
|
||||
for service in ${services[*]} ; do
|
||||
echo "TURNING OFF PROXY FOR ${service^^}"
|
||||
sudo rm -v "/etc/systemd/system/${service}.service.d/http-proxy.conf"
|
||||
done
|
||||
restart_services
|
||||
exit 0
|
||||
fi
|
||||
|
||||
#populate proxy_ip and proxy_port variables
|
||||
path=$(dirname $(readlink -f "${0}"))
|
||||
. "${path}/get_proxy.sh"
|
||||
|
||||
|
||||
echo -n "Username: "
|
||||
read username
|
||||
echo -n "Password: "
|
||||
read -s password
|
||||
#encpassword=$(perl -MURI::Escape -e 'print uri_escape($ARGV[0]);' "${password}")
|
||||
#encpassword=$(echo -n ${password} | sed -e 's/\@/\\x40/g' -e 's/\!/\\x21/g' -e 's/\$/\\x24/g' -e 's/\*/\\x2a/g' -e 's/\%/\\x25/g' -e 's/\&/\\x26/g' -e 's/\#/\\x30/g')
|
||||
encpassword=$(echo -n ${password} | xxd -p | sed -e 's/\(..\)/%%\1/g' )
|
||||
|
||||
for service in ${services[*]} ; do
|
||||
if ! [[ -d "/etc/systemd/system/${service}.service.d" ]] ; then
|
||||
sudo mkdir -p "/etc/systemd/system/${service}.service.d/"
|
||||
fi
|
||||
echo "[Service]" | sudo tee "/etc/systemd/system/${service}.service.d/http-proxy.conf" >/dev/null
|
||||
echo 'Environment="HTTP_PROXY=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" | sudo tee -a "/etc/systemd/system/${service}.service.d/http-proxy.conf" >/dev/null
|
||||
echo 'Environment="HTTPS_PROXY=http://'"${username}"':"'"${encpassword}@${proxy_ip}:${proxy_port}/" | sudo tee -a "/etc/systemd/system/${service}.service.d/http-proxy.conf" >/dev/null
|
||||
if [[ -z "$NO_PROXY" ]] ; then
|
||||
echo -n 'Environment="NO_PROXY=localhost,127.0.0.1"' | sudo tee -a "/etc/systemd/system/${service}.service.d/http-proxy.conf" >/dev/null
|
||||
else
|
||||
echo 'Environment="'$(env | grep NO_PROXY)'"' | sudo tee -a "/etc/systemd/system/${service}.service.d/http-proxy.conf" >/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
restart_services
|
||||
Reference in new issue
Block a user