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:
tke
2026-05-22 12:23:16 +02:00
parent ae5d503268
commit 203f2bf189
4 changed files with 893 additions and 106 deletions
+278 -40
View File
@@ -1,57 +1,295 @@
const http = require('http');
const net = require('net');
const { execSync } = require('child_process');
const { execFileSync } = require('child_process');
const fs = require('fs');
const vm = require('vm');
const path = require('path');
const os = require('os');
// 1. CONFIGURATION
const PROXY_HOST = 'PROXY_HOST_PLACEHOLDER';
const PROXY_PORT = 8080;
const CONFIG_FILE = process.env.PROXY_BRIDGE_CONFIG_FILE || '/opt/proxy-bridge/config.json';
const USER_FILE = process.env.PROXY_BRIDGE_USER_FILE || '/opt/proxy-bridge/user.json';
const PROFILE_FILE = '/opt/proxy-bridge/profile.json';
const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
const BASE_PORT = Number(process.env.PROXY_BRIDGE_BASE_PORT || 8888);
// 2. FETCH SECRETS FROM KEYRING
let USER, PASS;
let INTERNET_PROXY_HOST, INTERNET_PROXY_PORT;
try {
// Read the username we saved during setup
USER = JSON.parse(fs.readFileSync('/opt/proxy-bridge/user.json')).username;
// Query the Ubuntu Keyring for the password associated with this user/service
PASS = execSync(`secret-tool lookup service proxy-bridge account ${USER}`).toString().trim();
if (!PASS) throw new Error("Password returned empty.");
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
INTERNET_PROXY_HOST = config.host;
INTERNET_PROXY_PORT = Number(config.port);
} catch (e) {
console.error("CRITICAL: Could not retrieve credentials from keyring. Did you run setup.js?");
console.error(e.message);
process.exit(1);
INTERNET_PROXY_HOST = 'PROXY_HOST_PLACEHOLDER';
INTERNET_PROXY_PORT = 8080;
}
// 3. GENERATE AUTH
const AUTH_HEADER = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64');
let requestCounter = 0;
let dynamicProfile = 'internet';
const pacContexts = new Map(); // profileName -> vmContext
const server = http.createServer();
function now() {
return new Date().toISOString();
}
server.on('connect', (req, clientSocket, head) => {
console.log(`--> Connecting to ${req.url}`);
function log(level, id, message, fields = {}) {
const suffix = Object.entries(fields)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(' ');
console.log(`${now()} ${level} [${id}] ${message}${suffix ? ` ${suffix}` : ''}`);
}
const serverSocket = net.connect(PROXY_PORT, PROXY_HOST, () => {
serverSocket.write(`CONNECT ${req.url} HTTP/1.1\r\n` +
`Host: ${req.url}\r\n` +
`Proxy-Authorization: ${AUTH_HEADER}\r\n` +
`Proxy-Connection: Keep-Alive\r\n\r\n`);
serverSocket.write(head);
});
function nextId(prefix) {
requestCounter += 1;
return `${prefix}-${requestCounter}`;
}
serverSocket.once('data', (data) => {
if (data.toString().includes('200')) {
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
} else {
clientSocket.write(data);
function loadCredentials() {
try {
const user = JSON.parse(fs.readFileSync(USER_FILE, 'utf8')).username;
const pass = execFileSync('secret-tool', [
'lookup',
'service',
'proxy-bridge',
'account',
user,
], { encoding: 'utf8' }).trim();
return pass ? { user, pass } : null;
} catch {
return null;
}
}
let AUTH_HEADER;
function refreshAuth() {
const creds = loadCredentials();
if (creds) {
AUTH_HEADER = 'Basic ' + Buffer.from(`${creds.user}:${creds.pass}`).toString('base64');
return true;
}
return false;
}
refreshAuth();
function createPacSandbox() {
const sandbox = {
isPlainHostName: (host) => !host.includes('.'),
dnsDomainIs: (host, domain) => host.endsWith(domain),
localHostOrDomainIs: (host, hostdom) => host === hostdom || host.split('.')[0] === hostdom,
isResolvable: (host) => {
try { execFileSync('getent', ['hosts', host]); return true; } catch { return false; }
},
isInNet: (ip, pattern, mask) => false,
dnsResolve: (host) => {
try { return execFileSync('getent', ['hosts', host]).toString().split(/\s+/)[0]; } catch { return ""; }
},
myIpAddress: () => "127.0.0.1",
dnsDomainLevels: (host) => host.split('.').length - 1,
shExpMatch: (str, pattern) => {
const p = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.');
return new RegExp('^' + p + '$').test(str);
},
weekdayRange: () => true,
dateRange: () => true,
timeRange: () => true,
alert: (msg) => log('DEBUG', 'pac', msg),
};
return vm.createContext(sandbox);
}
function loadPacFile(profileName) {
try {
const pacPath = path.join(os.homedir(), '.mozilla', `${profileName}.pac`);
if (!fs.existsSync(pacPath)) return null;
const script = fs.readFileSync(pacPath, 'utf8');
const context = createPacSandbox();
vm.runInContext(script, context);
return typeof context.FindProxyForURL === 'function' ? context : null;
} catch (e) {
log('ERROR', 'pac', 'failed to load PAC', { profile: profileName, error: e.message });
return null;
}
}
function getAvailableProfiles() {
const profiles = ['internet', 'direct', 'off'];
const pacDir = path.join(os.homedir(), '.mozilla');
if (fs.existsSync(pacDir)) {
fs.readdirSync(pacDir).forEach(f => {
if (f.endsWith('.pac')) profiles.push(f.slice(0, -4));
});
}
return profiles;
}
function refreshPacContexts() {
const all = getAvailableProfiles();
all.forEach(name => {
if (!['internet', 'direct', 'off'].includes(name)) {
pacContexts.set(name, loadPacFile(name));
}
});
}
serverSocket.on('error', () => clientSocket.end());
clientSocket.on('error', () => serverSocket.end());
function updateDynamicProfile() {
try {
if (fs.existsSync(PROFILE_FILE)) {
const data = JSON.parse(fs.readFileSync(PROFILE_FILE, 'utf8'));
const newProfile = data.profile || 'internet';
if (newProfile !== dynamicProfile) {
log('INFO', 'profile', 'dynamic profile switch', { old: dynamicProfile, new: newProfile });
dynamicProfile = newProfile;
}
}
} catch {}
}
fs.watchFile(PROFILE_FILE, { interval: 1000 }, updateDynamicProfile);
updateDynamicProfile();
refreshPacContexts();
function getUpstream(profileName, url, host) {
const profile = (profileName === 'dynamic') ? dynamicProfile : profileName;
if (profile === 'off') return 'OFF';
if (profile === 'direct') return 'DIRECT';
if (profile === 'internet') return `PROXY ${INTERNET_PROXY_HOST}:${INTERNET_PROXY_PORT}`;
const ctx = pacContexts.get(profile);
if (ctx) {
try { return ctx.FindProxyForURL(url, host) || 'DIRECT'; }
catch (e) { log('ERROR', 'pac', 'exec error', { profile, error: e.message }); return 'DIRECT'; }
}
return 'DIRECT';
}
function parseProxyConfig(config) {
const first = config.split(';')[0].trim();
if (first.startsWith('PROXY ')) {
const [host, port] = first.substring(6).split(':');
return { type: 'PROXY', host, port: Number(port) };
}
return { type: first === 'OFF' ? 'OFF' : 'DIRECT' };
}
function sanitizeProxyHeaders(headers) {
const clean = { ...headers };
['proxy-authorization', 'proxy-authenticate', 'proxy-connection', 'connection', 'via'].forEach(h => delete clean[h]);
return clean;
}
function parseUpstreamStatus(buffer) {
const firstLine = buffer.toString('latin1').split('\r\n', 1)[0];
const match = firstLine.match(/^HTTP\/\d(?:\.\d)?\s+(\d{3})\s*(.*)$/i);
return { line: firstLine, code: match ? Number(match[1]) : undefined, text: match ? match[2] : undefined };
}
function createBridge(port, profileName) {
const server = http.createServer((clientReq, clientRes) => {
const id = nextId('http');
let urlObj;
try { urlObj = new URL(clientReq.url); } catch { urlObj = new URL(clientReq.url, `http://${clientReq.headers.host}`); }
const upstreamConfig = getUpstream(profileName, urlObj.href, urlObj.hostname);
const upstream = parseProxyConfig(upstreamConfig);
log('INFO', id, 'request', { port, profile: profileName, active: (profileName === 'dynamic' ? dynamicProfile : undefined), target: urlObj.href, upstream: upstreamConfig });
if (upstream.type === 'OFF') {
clientRes.writeHead(403); return clientRes.end('Forbidden by proxy profile\n');
}
const options = {
method: clientReq.method,
headers: sanitizeProxyHeaders(clientReq.headers),
host: upstream.type === 'PROXY' ? upstream.host : urlObj.hostname,
port: upstream.type === 'PROXY' ? upstream.port : (urlObj.port || 80),
path: upstream.type === 'PROXY' ? urlObj.href : (urlObj.pathname + urlObj.search)
};
if (upstream.type === 'PROXY') {
if (!AUTH_HEADER) refreshAuth();
if (AUTH_HEADER) options.headers['Proxy-Authorization'] = AUTH_HEADER;
}
const upstreamReq = http.request(options, (upstreamRes) => {
clientRes.writeHead(upstreamRes.statusCode, sanitizeProxyHeaders(upstreamRes.headers));
upstreamRes.pipe(clientRes);
});
upstreamReq.on('error', (e) => {
if (!clientRes.headersSent) clientRes.writeHead(502);
clientRes.end('Bad Gateway\n');
});
clientReq.pipe(upstreamReq);
});
server.on('connect', (req, clientSocket, head) => {
const id = nextId('connect');
const [host, portStr] = req.url.split(':');
const portNum = Number(portStr || 443);
const upstreamConfig = getUpstream(profileName, `https://${req.url}/`, host);
const upstream = parseProxyConfig(upstreamConfig);
log('INFO', id, 'connect', { port, profile: profileName, target: req.url, upstream: upstreamConfig });
if (upstream.type === 'OFF') {
return clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
}
const upstreamSocket = net.connect(upstream.type === 'PROXY' ? upstream.port : portNum, upstream.type === 'PROXY' ? upstream.host : host, () => {
if (upstream.type === 'PROXY') {
if (!AUTH_HEADER) refreshAuth();
upstreamSocket.write(`CONNECT ${req.url} HTTP/1.1\r\nHost: ${req.url}\r\n`);
if (AUTH_HEADER) upstreamSocket.write(`Proxy-Authorization: ${AUTH_HEADER}\r\n`);
upstreamSocket.write('\r\n');
}
if (head && head.length > 0 && upstream.type === 'DIRECT') upstreamSocket.write(head);
});
let connected = false;
upstreamSocket.on('data', (data) => {
if (connected) return;
if (upstream.type === 'PROXY') {
const status = parseUpstreamStatus(data);
if (status.code === 200) {
connected = true;
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
const headerEnd = data.indexOf('\r\n\r\n');
if (headerEnd !== -1 && data.length > headerEnd + 4) clientSocket.write(data.subarray(headerEnd + 4));
upstreamSocket.pipe(clientSocket); clientSocket.pipe(upstreamSocket);
} else {
clientSocket.write(data); clientSocket.end(); upstreamSocket.end();
}
} else {
connected = true;
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
clientSocket.write(data);
upstreamSocket.pipe(clientSocket);
clientSocket.pipe(upstreamSocket);
}
});
if (upstream.type === 'DIRECT') {
upstreamSocket.on('connect', () => {
if (connected) return;
connected = true;
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
upstreamSocket.pipe(clientSocket);
clientSocket.pipe(upstreamSocket);
});
}
upstreamSocket.on('error', () => clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'));
clientSocket.on('error', () => upstreamSocket.end());
});
server.listen(port, LISTEN_HOST, () => {
log('INFO', 'startup', 'bridge listener active', { port, profile: profileName });
});
}
// 1. Dynamic bridge on 8888
createBridge(BASE_PORT, 'dynamic');
// 2. Static bridges on 8889+
const allProfiles = getAvailableProfiles();
allProfiles.forEach((name, i) => {
createBridge(BASE_PORT + 1 + i, name);
});
server.listen(8888, '127.0.0.1', () => {
console.log('Bridge active on http://127.0.0.1:8888 (Auth via Keyring)');
});
+99 -42
View File
@@ -1,48 +1,68 @@
#!/bin/bash
# Ensure the script is NOT run as root, so the user-level systemd service configures correctly
# --- 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
echo "Please run this script as your standard user, not as root."
echo "The script will prompt for sudo access automatically when needed."
exit 1
error "Please run this script as your standard user, not as root.\nThe script will prompt for sudo access automatically when needed."
fi
echo "=== Proxy Bridge Installer ==="
# 1. Verify files exist
if [ ! -f "bridge.js" ] || [ ! -f "setup.js" ]; then
echo "❌ Error: bridge.js and/or setup.js not found in the current directory."
echo "Please place this script in the same folder as your Node.js scripts."
exit 1
if ! command -v node >/dev/null 2>&1; then
error "Node.js is not installed. Please install Node.js first."
fi
# 2. Install dependencies (requires sudo)
echo "--> Installing required system packages (libsecret-tools)..."
sudo apt-get update
sudo apt-get install -y libsecret-tools
# 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."
# 3. Setup application directory (requires sudo)
echo "--> Creating /opt/proxy-bridge directory..."
sudo mkdir -p /opt/proxy-bridge
sudo chown -R $USER:$USER /opt/proxy-bridge
# 2. SYSTEM PACKAGES
info "Installing required system packages (libsecret-tools)..."
sudo apt-get update -qq
sudo apt-get install -y libsecret-tools -qq
# 4. Copy files
echo "--> Copying scripts to /opt/proxy-bridge..."
cp bridge.js /opt/proxy-bridge/
cp setup.js /opt/proxy-bridge/
# 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"
# 5. Setup User-Level systemd Service
echo "--> Configuring user-level systemd service..."
# 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/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=Dumb Pipe Proxy Bridge (Keyring Auth)
Description=Local Proxy Bridge (Keyring Auth)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/proxy-bridge/bridge.js
ExecStart=$(command -v node) $BRIDGE_DIR/bridge.js
Restart=on-failure
RestartSec=5
@@ -50,21 +70,58 @@ RestartSec=5
WantedBy=default.target
EOF
# 6. Enable the service
echo "--> Reloading systemd and enabling service..."
systemctl --user daemon-reload
systemctl --user enable proxy-bridge.service
systemctl --user restart proxy-bridge.service
echo ""
echo "✅ Installation Complete!"
echo "=================================================="
echo "Next Steps:"
echo "1. Run the interactive setup to store your proxy credentials:"
echo " node /opt/proxy-bridge/setup.js"
echo ""
echo "2. Start the background service:"
echo " systemctl --user start proxy-bridge.service"
echo ""
echo "3. Check the logs to ensure it's running smoothly:"
echo " journalctl --user -u proxy-bridge.service -f"
echo "=================================================="
# 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"
+445
View File
@@ -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);
});
+71 -24
View File
@@ -1,34 +1,81 @@
const readline = require('readline');
const { execSync } = require('child_process');
const { execFileSync } = require('child_process');
const fs = require('fs');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("=== Proxy Bridge Keyring Setup ===");
const CONFIG_DIR = '/opt/proxy-bridge';
const CONFIG_FILE = `${CONFIG_DIR}/config.json`;
const USER_FILE = `${CONFIG_DIR}/user.json`;
rl.question('Enter your corporate username: ', (user) => {
rl.question('Enter your corporate password: ', (pass) => {
try {
// Securely store the password in the Ubuntu Keyring using secret-tool
// We use standard input to pass the password so it doesn't appear in process lists
execSync(`secret-tool store --label="Proxy Bridge Credentials" service proxy-bridge account ${user}`, {
input: pass
});
// Store the username in a local config just so the bridge knows WHICH account to look up
require('fs').writeFileSync('/opt/proxy-bridge/user.json', JSON.stringify({ username: user }));
console.log("=== Proxy Bridge Configuration Setup ===");
console.log("\n✅ Credentials successfully stored in the system keyring.");
} catch (error) {
console.error("\n❌ Failed to store credentials in keyring:", error.message);
}
rl.close();
function ask(question, defaultValue = "") {
return new Promise((resolve) => {
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
rl.question(prompt, (answer) => {
resolve(answer || defaultValue);
});
});
// Hide typing for password (basic implementation)
rl._writeToOutput = function _writeToOutput(stringToWrite) {
if (rl.history.length === 0) rl.output.write("*");
};
});
}
async function run() {
try {
// 1. Upstream Proxy Configuration
console.log("\n--- Upstream Proxy (Corporate) ---");
const host = await ask("Corporate Proxy Hostname (e.g. proxy.company.com)");
const port = await ask("Corporate Proxy Port", "8080");
if (!host) {
console.error("❌ Hostname is required.");
process.exit(1);
}
// 2. Credentials
console.log("\n--- Credentials ---");
const user = await ask("Corporate Username");
// Setup password mask
const oldWrite = rl._writeToOutput;
rl._writeToOutput = function _writeToOutput(stringToWrite) {
if (rl.line.length > 0) rl.output.write("*");
else rl.output.write(stringToWrite);
};
const pass = await ask("Corporate Password");
rl._writeToOutput = oldWrite;
// Save Proxy Config
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ host, port }, null, 2));
// Save Username
fs.writeFileSync(USER_FILE, JSON.stringify({ username: user }));
// Store Password in Keyring
console.log("\n--> Storing password in system keyring...");
execFileSync('secret-tool', [
'store',
'--label=Proxy Bridge Credentials',
'service',
'proxy-bridge',
'account',
user,
], {
input: pass
});
console.log("\n✅ Configuration and credentials successfully stored.");
} catch (error) {
console.error("\n❌ Setup failed:", error.message);
process.exit(1);
} finally {
rl.close();
}
}
run();