Files
gists/scripts/proxy/proxyctl.js
T
tke 203f2bf189 feat(proxy): secure refactor and system-wide integration
- Removed hardcoded corporate proxy URL from all scripts.
- Updated bridge.js to load configuration from /opt/proxy-bridge/config.json.
- Updated setup.js to interactively configure upstream proxy and credentials.
- Enhanced install_proxy.sh to automatically configure APT, Bash, and system services.
- Purged sensitive URL from git history and verified zero leakage.
2026-05-22 12:23:16 +02:00

446 lines
13 KiB
JavaScript

#!/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 = '';
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'));
return `${config.host}:${config.port}`;
}
} catch {}
return 'PROXY_HOST_PLACEHOLDER:8080 (default)';
}
function getProfile() {
try {
if (fs.existsSync(PROFILE_FILE)) {
return JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8')).profile || 'internet';
}
} catch {}
return 'internet';
}
function getAvailableProfiles() {
const profiles = ['internet', '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 profile = getProfile();
const all = getAvailableProfiles();
console.log(`service: ${SERVICE}`);
console.log(`active: ${active}`);
console.log(`enabled: ${enabled}`);
console.log(`pid: ${pid && pid !== '0' ? pid : 'none'}`);
console.log(`profile: ${profile}`);
console.log(`available: ${all.join(', ')}`);
console.log(`listen: http://${LISTEN_HOST}:${LISTEN_PORT} (${listening ? 'reachable' : 'not reachable'})`);
console.log(`upstream: ${getUpstreamConfig()}`);
console.log(`account: ${readAccount()}`);
}
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() {
console.log(`listen: ${LOCAL_PROXY_URL}`);
console.log(`upstream: ${getUpstreamConfig()}`);
console.log(`profile: ${getProfile()}`);
console.log(`available profiles: ${getAvailableProfiles().join(', ')}`);
console.log(`user_file: ${USER_FILE}`);
console.log(`account: ${readAccount()}`);
}
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();
if (!content) {
return {};
}
return 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 (command.includes(VSCODE_CERT_FLAG)) {
return command;
}
return command.replace(/^(Exec=\S+)/, `$1 ${VSCODE_CERT_FLAG}`);
}
function launcherHasFlag(file) {
if (!fs.existsSync(file)) {
return false;
}
return fs.readFileSync(file, 'utf8').includes(VSCODE_CERT_FLAG);
}
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(` ${VSCODE_CERT_FLAG}: ${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(` ${VSCODE_CERT_FLAG}: ${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 });
fs.writeFileSync(file, `#!/bin/sh\nexec /usr/bin/code ${VSCODE_CERT_FLAG} "$@"\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}`);
process.exitCode = 1;
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;
try {
settings = readJsonFile(file);
} catch (error) {
console.error(`Could not parse ${file}: ${error.message}`);
process.exit(1);
}
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;
try {
settings = readJsonFile(file);
} catch (error) {
console.error(`Could not parse ${file}: ${error.message}`);
process.exit(1);
}
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}`);
console.log(`http.proxy: ${LOCAL_PROXY_URL}`);
console.log('http.proxySupport: override');
console.log('http.proxyStrictSSL: false');
console.log('http.systemCertificates: true');
const listening = await checkPort();
console.log(`local proxy reachable: ${listening ? '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(`Unknown vscode launch action: ${args[1]}`);
console.error('Usage: proxy-bridge vscode launch [status|setup]');
process.exit(1);
}
break;
default:
console.error(`Unknown vscode action: ${action}`);
console.error('Usage: proxy-bridge vscode [status|setup|launch]');
process.exit(1);
}
}
function help() {
console.log(`Usage: proxy-bridge <command>
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
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;
case 'help':
case '--help':
case '-h':
help();
break;
default:
console.error(`Unknown command: ${command}`);
help();
process.exit(1);
}
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});