mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
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>
262 lines
6.7 KiB
JavaScript
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,
|
|
};
|
|
};
|