Files
node/test/common/proxy-server.js
Joyee Cheung c0108bfc55 http: add http.setGlobalProxyFromEnv()
This adds an API to dynamically enable built-in proxy support
for all of fetch() and http.request()/https.request(), so
that users do not have to be aware of them all and configure them
one by one.

PR-URL: https://github.com/nodejs/node/pull/60953
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
2025-12-10 16:12:28 +00:00

262 lines
6.7 KiB
JavaScript

'use strict';
const net = require('net');
const http = require('http');
const assert = require('assert');
const { once } = require('events');
const fixtures = require('./fixtures');
function logRequest(logs, req) {
logs.push({
method: req.method,
url: req.url,
headers: { ...req.headers },
});
}
// This creates a minimal proxy server that logs the requests it gets
// to an array before performing proxying.
function createProxyServer(options = {}) {
const logs = [];
let proxy;
if (options.https) {
const common = require('../common');
if (!common.hasCrypto) {
common.skip('missing crypto');
}
proxy = require('https').createServer({
cert: require('./fixtures').readKey('agent9-cert.pem'),
key: require('./fixtures').readKey('agent9-key.pem'),
});
} else {
proxy = http.createServer();
}
proxy.on('request', (req, res) => {
logRequest(logs, req);
const { hostname, port } = new URL(`http://${req.headers.host}`);
const targetPort = port || 80;
const url = new URL(req.url);
const options = {
hostname: hostname.startsWith('[') ? hostname.slice(1, -1) : hostname,
port: targetPort,
path: url.pathname + url.search, // Convert back to relative URL.
method: req.method,
headers: {
...req.headers,
'connection': req.headers['proxy-connection'] || 'close',
},
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
proxyReq.on('error', (err) => {
logs.push({ error: err, source: 'proxy request' });
if (!res.headersSent) {
res.writeHead(500);
}
if (!res.writableEnded) {
res.end(`Proxy error ${err.code}: ${err.message}`);
}
});
res.on('error', (err) => {
logs.push({ error: err, source: 'client response for request' });
});
req.pipe(proxyReq, { end: true });
});
proxy.on('connect', (req, res, head) => {
logRequest(logs, req);
const { hostname, port } = new URL(`https://${req.url}`);
res.on('error', (err) => {
logs.push({ error: err, source: 'client response for connect' });
});
const normalizedHostname = hostname.startsWith('[') && hostname.endsWith(']') ?
hostname.slice(1, -1) : hostname;
const proxyReq = net.connect(port, normalizedHostname, () => {
res.write(
'HTTP/1.1 200 Connection Established\r\n' +
'Proxy-agent: Node.js-Proxy\r\n' +
'\r\n',
);
proxyReq.write(head);
res.pipe(proxyReq);
proxyReq.pipe(res);
});
proxyReq.on('error', (err) => {
logs.push({ error: err, source: 'proxy connect' });
// The proxy client might have already closed the connection
// when the upstream connection fails.
if (!res.writableEnded) {
res.write('HTTP/1.1 500 Connection Error\r\n\r\n');
res.end('Proxy error: ' + err.message);
}
});
});
proxy.on('error', (err) => {
logs.push({ error: err, source: 'proxy server' });
});
return { proxy, logs };
}
exports.createProxyServer = createProxyServer;
function spawnPromisified(...args) {
const { spawn } = require('child_process');
let stderr = '';
let stdout = '';
const child = spawn(...args);
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
console.error('[STDERR]', data);
stderr += data;
});
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
console.log('[STDOUT]', data);
stdout += data;
});
return new Promise((resolve, reject) => {
child.on('close', (code, signal) => {
console.log('[CLOSE]', code, signal);
resolve({
code,
signal,
stderr,
stdout,
});
});
child.on('error', (code, signal) => {
console.log('[ERROR]', code, signal);
reject({
code,
signal,
stderr,
stdout,
});
});
});
}
async function checkProxied(type, envExtension, expectation, cliArgsExtension = []) {
const script = type === 'fetch' ? fixtures.path('fetch-and-log.mjs') : fixtures.path('request-and-log.js');
const { code, signal, stdout, stderr } = await spawnPromisified(
process.execPath,
[...cliArgsExtension, script], {
env: {
NO_LOG_REQUEST: '1',
...process.env,
...envExtension,
},
});
assert.deepStrictEqual({
stderr: stderr.trim(),
stdout: stdout.trim(),
code,
signal,
}, {
stderr: '',
code: 0,
signal: null,
...expectation,
});
};
exports.checkProxiedFetch = async function(...args) {
return checkProxied('fetch', ...args);
};
exports.checkProxiedRequest = async function(...args) {
return checkProxied('request', ...args);
};
exports.runProxiedRequest = async function(envExtension, cliArgsExtension = []) {
const fixtures = require('./fixtures');
return spawnPromisified(
process.execPath,
[...cliArgsExtension, fixtures.path('request-and-log.js')], {
env: {
...process.env,
...envExtension,
},
});
};
exports.runProxiedPOST = async function(envExtension) {
const fixtures = require('./fixtures');
return spawnPromisified(
process.execPath,
[fixtures.path('post-resource-and-log.js')], {
env: {
...process.env,
...envExtension,
},
});
};
exports.startTestServers = async function(options = {}) {
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
let httpServer, httpsServer, httpEndpoint, httpsEndpoint;
if (options.httpsEndpoint) {
httpsServer = require('https').createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, (req, res) => {
res.end('Hello world');
});
httpsServer.listen(0);
await once(httpsServer, 'listening');
const { port } = httpsServer.address();
httpsEndpoint = {
serverHost: `localhost:${port}`,
requestUrl: `https://localhost:${port}/test`,
};
}
if (options.httpEndpoint) {
httpServer = http.createServer((req, res) => {
res.end('Hello world');
});
httpServer.listen(0);
await once(httpServer, 'listening');
const { port } = httpServer.address();
httpEndpoint = {
serverHost: `localhost:${port}`,
requestUrl: `http://localhost:${port}/test`,
};
}
return {
proxyLogs: logs,
shutdown() {
if (httpServer) {
httpServer.close();
}
if (httpsServer) {
httpsServer.close();
}
proxy.close();
},
proxyUrl: `http://localhost:${proxy.address().port}`,
httpEndpoint,
httpsEndpoint,
};
};