Update proxy bridge setup

This commit is contained in:
tke
2026-05-22 13:01:54 +02:00
parent 203f2bf189
commit 7f73746427
7 changed files with 341 additions and 250 deletions
+86 -189
View File
@@ -13,7 +13,7 @@ const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.
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 = '';
const VSCODE_CERT_FLAG = ''; // e.g. '--ignore-certificate-errors'
function run(command, args, options = {}) {
return spawnSync(command, args, {
@@ -26,23 +26,38 @@ 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 'PROXY_HOST_PLACEHOLDER:8080 (default)';
return 'your-proxy.example.com:8080 (default)';
}
function getProfile() {
try {
if (fs.existsSync(PROFILE_FILE)) {
return JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8')).profile || 'internet';
const current = JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8')).profile;
const valid = getAvailableProfiles();
if (current && valid.includes(current)) return current;
}
} catch {}
return 'internet';
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 = ['internet', 'direct', 'off'];
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);
@@ -71,49 +86,28 @@ function systemctl(args, options = {}) {
}
function printCommandFailure(result) {
if (result.error) {
console.error(result.error.message);
return;
}
if (result.stderr) {
console.error(result.stderr.trim());
}
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 '';
}
if (result.status !== 0) return '';
return result.stdout.trim();
}
function serviceState(command) {
const result = systemctl([command, SERVICE]);
if (result.status !== 0) {
return 'unknown';
}
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);
});
const timer = setTimeout(() => { socket.destroy(); resolve(false); }, 1000);
socket.once('connect', () => { clearTimeout(timer); socket.end(); resolve(true); });
socket.once('error', () => { clearTimeout(timer); resolve(false); });
});
}
@@ -130,49 +124,52 @@ async function status() {
const enabled = serviceState('is-enabled');
const pid = serviceValue('MainPID');
const listening = await checkPort();
const profile = getProfile();
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'}`);
console.log(`profile: ${profile}`);
console.log(`available: ${all.join(', ')}`);
console.log(`listen: http://${LISTEN_HOST}:${LISTEN_PORT} (${listening ? 'reachable' : 'not reachable'})`);
if (isUpstreamEnabled()) console.log(`account: ${readAccount()}`);
console.log(`upstream: ${getUpstreamConfig()}`);
console.log(`account: ${readAccount()}`);
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);
}
if (result.status !== 0) { printCommandFailure(result); process.exit(result.status || 1); }
}
function logs() {
const child = spawn('journalctl', ['--user', '-u', SERVICE, '-f'], {
stdio: 'inherit',
});
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',
});
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}`);
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(`profile: ${getProfile()}`);
console.log(`available profiles: ${getAvailableProfiles().join(', ')}`);
console.log(`user_file: ${USER_FILE}`);
console.log(`account: ${readAccount()}`);
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() {
@@ -181,16 +178,9 @@ function codeSettingsPath() {
}
function readJsonFile(file) {
if (!fs.existsSync(file)) {
return {};
}
if (!fs.existsSync(file)) return {};
const content = fs.readFileSync(file, 'utf8').trim();
if (!content) {
return {};
}
return JSON.parse(content);
return content ? JSON.parse(content) : {};
}
function writeJsonFile(file, data) {
@@ -206,33 +196,19 @@ function desktopApplicationsDir() {
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'),
},
{ 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}`);
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 (!fs.existsSync(file)) {
return false;
}
return fs.readFileSync(file, 'utf8').includes(VSCODE_CERT_FLAG);
if (!VSCODE_CERT_FLAG) return false;
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').includes(VSCODE_CERT_FLAG) : false;
}
function vscodeLaunchStatus() {
@@ -241,60 +217,39 @@ function vscodeLaunchStatus() {
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'}`);
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(` ${VSCODE_CERT_FLAG}: ${launcherHasFlag(userCode) ? 'yes' : 'no'}`);
console.log(` flag present: ${launcherHasFlag(userCode) ? 'yes' : 'no'}`);
}
function userCodeWrapperPath() {
return path.join(os.homedir(), '.local', 'bin', 'code');
}
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`);
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}`);
process.exitCode = 1;
continue;
}
const content = fs.readFileSync(launcher.system, 'utf8')
.split('\n')
.map((line) => (line.startsWith('Exec=') ? addCodeFlag(line) : line))
.join('\n');
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;
try {
settings = readJsonFile(file);
} catch (error) {
console.error(`Could not parse ${file}: ${error.message}`);
process.exit(1);
}
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'}`);
@@ -305,60 +260,29 @@ async function vscodeStatus() {
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);
}
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}`);
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'}`);
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 '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);
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(`Unknown vscode action: ${action}`);
console.error('Usage: proxy-bridge vscode [status|setup|launch]');
process.exit(1);
default: console.error('Usage: proxy-bridge vscode [status|setup|launch]'); process.exit(1);
}
}
@@ -375,7 +299,7 @@ Commands:
enable Enable the user service at login
disable Disable the user service at login
logs Follow service logs
setup Store or update keyring credentials
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
@@ -390,17 +314,11 @@ VS Code:
async function main() {
const command = process.argv[2] || 'status';
switch (command) {
case 'status':
await status();
break;
case 'status': await status(); break;
case 'profile':
if (process.argv[3]) {
setProfile(process.argv[3]);
} else {
console.log(getProfile());
}
if (process.argv[3]) setProfile(process.argv[3]);
else console.log(getProfile());
break;
case 'toggle':
const current = getProfile();
@@ -412,34 +330,13 @@ async function main() {
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);
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);
});
main().catch((error) => { console.error(error.message); process.exit(1); });