#!/usr/bin/env node const net = require('net'); const { spawn, spawnSync } = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const SERVICE = 'proxy-bridge.service'; 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 LOCAL_PROXY_URL = `http://${LISTEN_HOST}:${LISTEN_PORT}`; const VSCODE_CERT_FLAG = ''; // e.g. '--ignore-certificate-errors' function run(command, args, options = {}) { return spawnSync(command, args, { encoding: 'utf8', stdio: options.stdio || 'pipe', }); } function getUpstreamConfig() { try { if (fs.existsSync(CONFIG_FILE)) { const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); if (config.enabled === false) return 'DISABLED'; return `${config.host}:${config.port}`; } } catch {} return 'your-proxy.example.com:8080 (default)'; } function getProfile() { try { if (fs.existsSync(PROFILE_FILE)) { const current = JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8')).profile; const valid = getAvailableProfiles(); if (current && valid.includes(current)) return current; } } catch {} return isUpstreamEnabled() ? 'internet' : 'direct'; } function isUpstreamEnabled() { try { if (fs.existsSync(CONFIG_FILE)) { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')).enabled !== false; } } catch {} return true; } function getAvailableProfiles() { const profiles = []; if (isUpstreamEnabled()) profiles.push('internet'); profiles.push('direct', 'off'); const pacDir = path.join(os.homedir(), '.mozilla'); if (fs.existsSync(pacDir)) { const files = fs.readdirSync(pacDir); for (const f of files) { if (f.endsWith('.pac')) { profiles.push(f.slice(0, -4)); } } } return profiles; } function setProfile(profile) { const valid = getAvailableProfiles(); if (!valid.includes(profile)) { console.error(`Invalid profile: ${profile}. Valid options: ${valid.join(', ')}`); process.exit(1); } fs.mkdirSync(path.dirname(PROFILE_FILE), { recursive: true }); fs.writeFileSync(PROFILE_FILE, JSON.stringify({ profile })); console.log(`Profile set to: ${profile}`); } function systemctl(args, options = {}) { return run('systemctl', ['--user', ...args], options); } function printCommandFailure(result) { if (result.error) { console.error(result.error.message); return; } if (result.stderr) { console.error(result.stderr.trim()); } } function serviceValue(property) { const result = systemctl(['show', SERVICE, '--property', property, '--value']); if (result.status !== 0) return ''; return result.stdout.trim(); } function serviceState(command) { const result = systemctl([command, SERVICE]); if (result.status !== 0) return 'unknown'; return result.stdout.trim() || 'unknown'; } function checkPort() { return new Promise((resolve) => { const socket = net.connect({ host: LISTEN_HOST, port: LISTEN_PORT }); const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 1000); socket.once('connect', () => { clearTimeout(timer); socket.end(); resolve(true); }); socket.once('error', () => { clearTimeout(timer); resolve(false); }); }); } function readAccount() { try { return JSON.parse(fs.readFileSync(USER_FILE, 'utf8')).username || 'unknown'; } catch { return 'not configured'; } } async function status() { const active = serviceState('is-active'); const enabled = serviceState('is-enabled'); const pid = serviceValue('MainPID'); const listening = await checkPort(); const current = getProfile(); const all = getAvailableProfiles(); console.log(`service: ${SERVICE}`); console.log(`active: ${active}`); console.log(`enabled: ${enabled}`); console.log(`pid: ${pid && pid !== '0' ? pid : 'none'}`); if (isUpstreamEnabled()) console.log(`account: ${readAccount()}`); console.log(`upstream: ${getUpstreamConfig()}`); console.log(`\nPort Mappings:`); console.log(` ${LISTEN_PORT.toString().padEnd(5)} -> dynamic (currently: ${current})${listening ? '' : ' [NOT REACHABLE]'}`); all.forEach((name, i) => { console.log(` ${(LISTEN_PORT + 1 + i).toString().padEnd(5)} -> ${name}`); }); } function control(action) { const result = systemctl([action, SERVICE], { stdio: 'inherit' }); if (result.status !== 0) { printCommandFailure(result); process.exit(result.status || 1); } } function logs() { const child = spawn('journalctl', ['--user', '-u', SERVICE, '-f'], { stdio: 'inherit' }); child.on('exit', (code) => process.exit(code || 0)); } function setup() { const child = spawn('node', ['/opt/proxy-bridge/setup.js'], { stdio: 'inherit' }); child.on('exit', (code) => process.exit(code || 0)); } function config() { const all = getAvailableProfiles(); console.log(`listen_host: ${LISTEN_HOST}`); console.log(`base_port: ${LISTEN_PORT}`); if (isUpstreamEnabled()) { console.log(`user_file: ${USER_FILE}`); console.log(`account: ${readAccount()}`); } console.log(`config_file: ${CONFIG_FILE}`); console.log(`upstream: ${getUpstreamConfig()}`); console.log(`\nEffective Mappings:`); console.log(` ${LISTEN_PORT} -> dynamic (${getProfile()})`); all.forEach((name, i) => { console.log(` ${(LISTEN_PORT + 1 + i).toString().padEnd(5)} -> ${name}`); }); } function codeSettingsPath() { const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); return path.join(configHome, 'Code', 'User', 'settings.json'); } function readJsonFile(file) { if (!fs.existsSync(file)) return {}; const content = fs.readFileSync(file, 'utf8').trim(); return content ? JSON.parse(content) : {}; } function writeJsonFile(file, data) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(data, null, 4)}\n`); } function desktopApplicationsDir() { const dataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'); return path.join(dataHome, 'applications'); } function desktopLauncherPaths() { const userDir = desktopApplicationsDir(); return [ { name: 'main', system: '/usr/share/applications/code.desktop', user: path.join(userDir, 'code.desktop') }, { name: 'url-handler', system: '/usr/share/applications/code-url-handler.desktop', user: path.join(userDir, 'code-url-handler.desktop') }, ]; } function addCodeFlag(command) { if (!VSCODE_CERT_FLAG) return command; return command.includes(VSCODE_CERT_FLAG) ? command : command.replace(/^(Exec=\S+)/, `$1 ${VSCODE_CERT_FLAG}`); } function launcherHasFlag(file) { if (!VSCODE_CERT_FLAG) return false; return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').includes(VSCODE_CERT_FLAG) : false; } function vscodeLaunchStatus() { for (const launcher of desktopLauncherPaths()) { const exists = fs.existsSync(launcher.user); const enabled = launcherHasFlag(launcher.user); console.log(`${launcher.name}: ${launcher.user}`); console.log(` user override: ${exists ? 'yes' : 'no'}`); console.log(` flag present: ${enabled ? 'yes' : 'no'}`); } const userCode = path.join(os.homedir(), '.local', 'bin', 'code'); console.log(`terminal wrapper: ${userCode}`); console.log(` installed: ${fs.existsSync(userCode) ? 'yes' : 'no'}`); console.log(` flag present: ${launcherHasFlag(userCode) ? 'yes' : 'no'}`); } function userCodeWrapperPath() { return path.join(os.homedir(), '.local', 'bin', 'code'); } function vscodeTerminalSetup() { const file = userCodeWrapperPath(); fs.mkdirSync(path.dirname(file), { recursive: true }); const flagStr = VSCODE_CERT_FLAG ? ` ${VSCODE_CERT_FLAG}` : ''; fs.writeFileSync(file, `#!/bin/sh\nexec /usr/bin/code${flagStr} "$@"\n`); fs.chmodSync(file, 0o755); console.log(`Updated ${file}`); } function vscodeLaunchSetup() { fs.mkdirSync(desktopApplicationsDir(), { recursive: true }); for (const launcher of desktopLauncherPaths()) { if (!fs.existsSync(launcher.system)) { console.error(`Missing system launcher: ${launcher.system}`); continue; } const content = fs.readFileSync(launcher.system, 'utf8').split('\n').map((line) => (line.startsWith('Exec=') ? addCodeFlag(line) : line)).join('\n'); fs.writeFileSync(launcher.user, content); console.log(`Updated ${launcher.user}`); } vscodeTerminalSetup(); } async function vscodeStatus() { const file = codeSettingsPath(); let settings = readJsonFile(file); console.log(`settings: ${file}`); console.log(`http.proxy: ${settings['http.proxy'] || 'not set'}`); console.log(`http.proxySupport: ${settings['http.proxySupport'] || 'not set'}`); console.log(`http.proxyStrictSSL: ${settings['http.proxyStrictSSL'] === undefined ? 'not set' : settings['http.proxyStrictSSL']}`); console.log(`http.systemCertificates: ${settings['http.systemCertificates'] === undefined ? 'not set' : settings['http.systemCertificates']}`); console.log(`local proxy reachable: ${await checkPort() ? 'yes' : 'no'}`); } async function vscodeSetup() { const file = codeSettingsPath(); let settings = readJsonFile(file); settings['http.proxy'] = LOCAL_PROXY_URL; settings['http.proxySupport'] = 'override'; settings['http.proxyStrictSSL'] = false; settings['http.systemCertificates'] = true; writeJsonFile(file, settings); console.log(`Updated ${file}\nhttp.proxy: ${LOCAL_PROXY_URL}\nhttp.proxySupport: override\nhttp.proxyStrictSSL: false\nhttp.systemCertificates: true`); console.log(`local proxy reachable: ${await checkPort() ? 'yes' : 'no'}`); } async function vscode(args) { const action = args[0] || 'status'; switch (action) { case 'status': await vscodeStatus(); break; case 'setup': await vscodeSetup(); break; case 'launch': switch (args[1] || 'status') { case 'status': vscodeLaunchStatus(); break; case 'setup': vscodeLaunchSetup(); break; default: console.error('Usage: proxy-bridge vscode launch [status|setup]'); process.exit(1); } break; default: console.error('Usage: proxy-bridge vscode [status|setup|launch]'); process.exit(1); } } function help() { console.log(`Usage: proxy-bridge Commands: status Show service, listener, upstream, profile, and account profile [name] Show or set the active routing profile toggle Cycle through profiles (internet -> direct -> off -> [pacs...]) start Start the local proxy bridge stop Stop the local proxy bridge restart Restart the local proxy bridge enable Enable the user service at login disable Disable the user service at login logs Follow service logs setup Store or update keyring credentials and upstream config config Print effective non-secret configuration vscode Manage VS Code proxy settings help Show this help VS Code: proxy-bridge vscode status proxy-bridge vscode setup proxy-bridge vscode launch status proxy-bridge vscode launch setup `); } async function main() { const command = process.argv[2] || 'status'; switch (command) { case 'status': await status(); break; case 'profile': if (process.argv[3]) setProfile(process.argv[3]); else console.log(getProfile()); break; case 'toggle': const current = getProfile(); const profiles = getAvailableProfiles(); const next = profiles[(profiles.indexOf(current) + 1) % profiles.length]; setProfile(next); break; case 'start': case 'stop': case 'restart': case 'enable': case 'disable': control(command); break; case 'logs': logs(); break; case 'setup': setup(); break; case 'config': config(); break; case 'vscode': await vscode(process.argv.slice(3)); break; default: help(); process.exit(1); } } main().catch((error) => { console.error(error.message); process.exit(1); });