node/test/common/proxy-server.js
Joyee Cheung 81af7b93c5
http,https: handle IPv6 with proxies
This simplifies the proxy configuration handling code,
 adds tests to make sure the proxy support works with IPv6
and throws correct errors for invalid proxy IPs.
Drive-by: remove useless properties from ProxyConfig

PR-URL: https://github.com/nodejs/node/pull/59894
Refs: https://github.com/nodejs/node/issues/57872
Reviewed-By: Aditi Singh <aditisingh1400@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
2025-09-22 23:19:26 +00:00

198 lines
5.0 KiB
JavaScript

'use strict';
const net = require('net');
const http = require('http');
const assert = require('assert');
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.
exports.createProxyServer = function(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 };
};
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,
});
});
});
}
exports.checkProxiedFetch = async function(envExtension, expectation, cliArgsExtension = []) {
const fixtures = require('./fixtures');
const { code, signal, stdout, stderr } = await spawnPromisified(
process.execPath,
[...cliArgsExtension, fixtures.path('fetch-and-log.mjs')], {
env: {
...process.env,
...envExtension,
},
});
assert.deepStrictEqual({
stderr: stderr.trim(),
stdout: stdout.trim(),
code,
signal,
}, {
stderr: '',
code: 0,
signal: null,
...expectation,
});
};
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,
},
});
};