Files
gists/scripts/proxy/bridge.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

296 lines
11 KiB
JavaScript

const http = require('http');
const net = require('net');
const { execFileSync } = require('child_process');
const fs = require('fs');
const vm = require('vm');
const path = require('path');
const os = require('os');
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);
let INTERNET_PROXY_HOST, INTERNET_PROXY_PORT;
try {
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
INTERNET_PROXY_HOST = config.host;
INTERNET_PROXY_PORT = Number(config.port);
} catch (e) {
INTERNET_PROXY_HOST = 'PROXY_HOST_PLACEHOLDER';
INTERNET_PROXY_PORT = 8080;
}
let requestCounter = 0;
let dynamicProfile = 'internet';
const pacContexts = new Map(); // profileName -> vmContext
function now() {
return new Date().toISOString();
}
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}` : ''}`);
}
function nextId(prefix) {
requestCounter += 1;
return `${prefix}-${requestCounter}`;
}
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));
}
});
}
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);
});