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.
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user