fix(proxy): port HTTPS-upstream TLS-wrap from deployed bridge; unify CONNECT establishment
- parseProxyConfig handles PAC 'HTTPS host:port' (tls.connect + CONNECT inside TLS) in both the CONNECT and plain-HTTP handlers — ports live fix d8b9db2 into the repo - CONNECT: split tunnel establishment into establishDirect / sendConnectToProxy, removing the duplicate DIRECT 'connect' listener that raced onUpstreamData - env-overridable PROFILE_FILE / REMOTES_FILE / PAC_DIR (test hermeticity) - tests: +DIRECT echo exactly-once, +HTTPS upstream via local PAC dir + self-signed TLS stub (no internet needed); 8/8 pass
This commit is contained in:
1 parent
f434d58ef9
commit
0b8c5001fc
5 files changed
+531
-48
No files matched your search
+108
-48
@@ -1,5 +1,7 @@
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const { execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
@@ -8,8 +10,10 @@ 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 PROFILE_FILE = process.env.PROXY_BRIDGE_PROFILE_FILE || '/opt/proxy-bridge/profile.json';
|
||||
const OVERRIDES_FILE = process.env.PROXY_BRIDGE_OVERRIDES_FILE || '/opt/proxy-bridge/overrides.json';
|
||||
const REMOTES_FILE = process.env.PROXY_BRIDGE_REMOTES_FILE || '/opt/proxy-bridge/remotes.json';
|
||||
const PAC_DIR = process.env.PROXY_BRIDGE_PAC_DIR || path.join(os.homedir(), '.mozilla');
|
||||
const LISTEN_HOST = process.env.PROXY_BRIDGE_LISTEN_HOST || '127.0.0.1';
|
||||
const BASE_PORT = Number(process.env.PROXY_BRIDGE_BASE_PORT || 8888);
|
||||
|
||||
@@ -120,7 +124,7 @@ function createPacSandbox() {
|
||||
|
||||
function loadPacFile(profileName) {
|
||||
try {
|
||||
const pacPath = path.join(os.homedir(), '.mozilla', `${profileName}.pac`);
|
||||
const pacPath = path.join(PAC_DIR, `${profileName}.pac`);
|
||||
if (!fs.existsSync(pacPath)) return null;
|
||||
const script = fs.readFileSync(pacPath, 'utf8');
|
||||
const context = createPacSandbox();
|
||||
@@ -137,7 +141,7 @@ function getAvailableProfiles() {
|
||||
if (UPSTREAM_ENABLED) profiles.push('internet');
|
||||
profiles.push('direct', 'off');
|
||||
|
||||
const pacDir = path.join(os.homedir(), '.mozilla');
|
||||
const pacDir = PAC_DIR;
|
||||
if (fs.existsSync(pacDir)) {
|
||||
fs.readdirSync(pacDir).forEach(f => {
|
||||
if (f.endsWith('.pac')) profiles.push(f.slice(0, -4));
|
||||
@@ -270,6 +274,10 @@ function parseProxyConfig(config) {
|
||||
const [host, port] = first.substring(6).split(':');
|
||||
return { type: 'PROXY', host, port: Number(port) };
|
||||
}
|
||||
if (first.startsWith('HTTPS ')) {
|
||||
const [host, port] = first.substring(6).split(':');
|
||||
return { type: 'HTTPS', host, port: Number(port) };
|
||||
}
|
||||
return { type: first === 'OFF' ? 'OFF' : 'DIRECT' };
|
||||
}
|
||||
|
||||
@@ -303,23 +311,35 @@ function createBridge(port, profileName) {
|
||||
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)
|
||||
host: (upstream.type === 'PROXY' || upstream.type === 'HTTPS') ? upstream.host : urlObj.hostname,
|
||||
port: (upstream.type === 'PROXY' || upstream.type === 'HTTPS') ? upstream.port : (urlObj.port || 80),
|
||||
path: (upstream.type === 'PROXY' || upstream.type === 'HTTPS') ? urlObj.href : (urlObj.pathname + urlObj.search)
|
||||
};
|
||||
if (upstream.type === 'PROXY') {
|
||||
if (upstream.type === 'PROXY' || upstream.type === 'HTTPS') {
|
||||
if (!AUTH_HEADER) refreshAuth();
|
||||
if (AUTH_HEADER) options.headers['Proxy-Authorization'] = AUTH_HEADER;
|
||||
}
|
||||
|
||||
const upstreamReq = http.request(options, (upstreamRes) => {
|
||||
const requestModule = upstream.type === 'HTTPS' ? https : http;
|
||||
const upstreamReq = requestModule.request(options, (upstreamRes) => {
|
||||
clientRes.writeHead(upstreamRes.statusCode, sanitizeProxyHeaders(upstreamRes.headers));
|
||||
upstreamRes.pipe(clientRes);
|
||||
upstreamRes.on('error', (err) => {
|
||||
clientRes.destroy(err);
|
||||
});
|
||||
upstreamRes.on('close', () => {
|
||||
if (!upstreamRes.complete) {
|
||||
clientRes.destroy(new Error('Upstream response truncated'));
|
||||
}
|
||||
});
|
||||
});
|
||||
upstreamReq.on('error', (e) => {
|
||||
if (!clientRes.headersSent) clientRes.writeHead(502);
|
||||
clientRes.end('Bad Gateway\n');
|
||||
});
|
||||
clientReq.on('close', () => {
|
||||
if (!clientReq.complete) upstreamReq.destroy();
|
||||
});
|
||||
clientReq.pipe(upstreamReq);
|
||||
});
|
||||
|
||||
@@ -330,10 +350,17 @@ function createBridge(port, profileName) {
|
||||
const { config: upstreamConfig, overrideProfile } = getUpstream(profileName, `https://${req.url}/`, host);
|
||||
const upstream = parseProxyConfig(upstreamConfig);
|
||||
let upstreamSocket;
|
||||
let connected = false;
|
||||
|
||||
clientSocket.setKeepAlive(true, 10000);
|
||||
clientSocket.setNoDelay(true);
|
||||
|
||||
clientSocket.on('error', () => {
|
||||
if (upstreamSocket) upstreamSocket.destroy();
|
||||
});
|
||||
clientSocket.on('close', () => {
|
||||
if (upstreamSocket) upstreamSocket.destroy();
|
||||
});
|
||||
|
||||
log('INFO', id, 'connect', { port, profile: profileName, override: overrideProfile, target: req.url, upstream: upstreamConfig });
|
||||
|
||||
@@ -341,49 +368,82 @@ function createBridge(port, profileName) {
|
||||
return clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
const establishDirect = () => {
|
||||
upstreamSocket.setKeepAlive(true, 10000);
|
||||
upstreamSocket.setNoDelay(true);
|
||||
connected = true;
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
if (head && head.length > 0) upstreamSocket.write(head);
|
||||
upstreamSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(upstreamSocket);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
const sendConnectToProxy = () => {
|
||||
upstreamSocket.setKeepAlive(true, 10000);
|
||||
upstreamSocket.setNoDelay(true);
|
||||
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 (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);
|
||||
});
|
||||
if (upstream.type === 'HTTPS') {
|
||||
upstreamSocket = tls.connect(
|
||||
{ host: upstream.host, port: upstream.port, servername: upstream.host, rejectUnauthorized: false },
|
||||
sendConnectToProxy
|
||||
);
|
||||
} else if (upstream.type === 'PROXY') {
|
||||
upstreamSocket = net.connect(upstream.port, upstream.host, sendConnectToProxy);
|
||||
} else { // DIRECT
|
||||
upstreamSocket = net.connect(portNum, host, establishDirect);
|
||||
}
|
||||
upstreamSocket.on('error', () => clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'));
|
||||
|
||||
upstreamSocket.on('error', () => {
|
||||
if (!connected) {
|
||||
try { clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'); } catch {}
|
||||
} else {
|
||||
clientSocket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
upstreamSocket.on('close', () => {
|
||||
if (!connected) {
|
||||
try { clientSocket.end('HTTP/1.1 502 Bad Gateway\r\n\r\n'); } catch {}
|
||||
} else {
|
||||
clientSocket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
let connectBuffer = Buffer.alloc(0);
|
||||
function onUpstreamData(data) {
|
||||
if (connected) return;
|
||||
if (upstream.type === 'PROXY' || upstream.type === 'HTTPS') {
|
||||
connectBuffer = Buffer.concat([connectBuffer, data]);
|
||||
const headerEnd = connectBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd === -1) return;
|
||||
|
||||
connected = true;
|
||||
upstreamSocket.removeListener('data', onUpstreamData);
|
||||
|
||||
const status = parseUpstreamStatus(connectBuffer);
|
||||
if (status.code === 200) {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
const remaining = connectBuffer.subarray(headerEnd + 4);
|
||||
if (remaining.length > 0) clientSocket.write(remaining);
|
||||
if (head && head.length > 0) upstreamSocket.write(head);
|
||||
upstreamSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(upstreamSocket);
|
||||
} else {
|
||||
clientSocket.write(connectBuffer);
|
||||
clientSocket.end();
|
||||
upstreamSocket.end();
|
||||
}
|
||||
}
|
||||
// DIRECT is established in the socket's connect callback (establishDirect);
|
||||
// nothing to do here — piped streams carry the bytes from here on.
|
||||
}
|
||||
|
||||
upstreamSocket.on('data', onUpstreamData);
|
||||
});
|
||||
|
||||
server.listen(port, LISTEN_HOST, () => {
|
||||
|
||||
Reference in new issue
Block a user