mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
When `options.path` passed to `http.request()` contains an absolute URL, `http.request` has been sending it directly as the request target in the HTTP 1.1 message. If the receiving server is a proxy, the proxy server typically forwards the request to the destination specified in the request target and ignores the Host header. This means eventually the request can be forwarded to a destination that is not consistent with `options.host`, depending on how the receiving server behaves. Mimatched Host header and request target also violates RFC 9112 Section 3.2, which we have been entirely leaving to the users to verify. This patch documents this behavior and warns that the user needs to ensure the `path`, `option` and `headers` conform to the RFC. If the receiving server is known to be a proxy server because the request is routed by Node.js' built-in HTTP proxy support, we now do a best-effort check to verify that the authority in `options.path` (if absolute), Host headers and `options.host` agree at request construction time. Node.js will give up on the require target rewriting and throw an error when they don't match at request construction. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: https://github.com/nodejs/node/pull/64108 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
260 lines
6.6 KiB
JavaScript
260 lines
6.6 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);
|
|
// Route based on the absolute-form request-target.
|
|
const url = new URL(req.url);
|
|
const options = {
|
|
hostname: url.hostname.startsWith('[') ? url.hostname.slice(1, -1) : url.hostname,
|
|
port: url.port || 80,
|
|
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,
|
|
};
|
|
};
|