Files
node/test/client-proxy/test-https-proxy-request-invalid-char-in-url.mjs
T
Shelley Vohr 29d39e10ad test: ignore tunnel resets in proxy invalid-char-in-url test
test-https-proxy-request-invalid-char-in-url is the only client-proxy
test that asserts the proxy logged no socket errors at all. Once the
last response has been read the client destroys its tunnel, and if the
proxy is still relaying the upstream's TLS close_notify at that point
the client answers with a reset, which the proxy records as ECONNRESET
on the CONNECT socket. That has been failing the test on macOS even
though every request was routed to the sanitized URL. Keep asserting
on other errors but leave connection resets out.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65780
Fixes: https://github.com/nodejs/node/issues/64226
Refs: https://github.com/nodejs/node/issues/59146
Refs: https://github.com/nodejs/node/issues/54534
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2026-09-06 08:43:49 +00:00

96 lines
3.7 KiB
JavaScript

// This tests that invalid hosts or ports with carriage return or newline characters
// in HTTPS request urls are stripped away before being sent to the server.
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import fixtures from '../common/fixtures.js';
import { once } from 'events';
import { inspect } from 'node:util';
import { createProxyServer } from '../common/proxy-server.js';
if (!common.hasCrypto) {
common.skip('missing crypto');
}
// https must be dynamically imported so that builds without crypto support
// can skip it.
const { default: https } = await import('node:https');
const requests = new Set();
const server = https.createServer({
cert: fixtures.readKey('agent8-cert.pem'),
key: fixtures.readKey('agent8-key.pem'),
}, (req, res) => {
console.log(`[Upstream server] responding to request for ${inspect(req.url)}`);
requests.add(`https://localhost:${server.address().port}${req.url}`);
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Response for ${inspect(req.url)}`);
});
server.listen(0);
await once(server, 'listening');
server.on('error', common.mustNotCall());
const port = server.address().port.toString();
const testCases = [
{ host: 'local\rhost', port: port, path: '/carriage-return-in-host' },
{ host: 'local\nhost', port: port, path: '/newline-in-host' },
{ host: 'local\r\nhost', port: port, path: '/crlf-in-host' },
{ host: 'localhost', port: port.substring(0, 1) + '\r' + port.substring(1), path: '/carriage-return-in-port' },
{ host: 'localhost', port: port.substring(0, 1) + '\n' + port.substring(1), path: '/newline-in-port' },
{ host: 'localhost', port: port.substring(0, 1) + '\r\n' + port.substring(1), path: '/crlf-in-port' },
];
// Start a minimal proxy server
const { proxy, logs } = createProxyServer();
proxy.listen(0);
await once(proxy, 'listening');
https.globalAgent = new https.Agent({
ca: fixtures.readKey('fake-startcom-root-cert.pem'),
proxyEnv: {
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
},
});
const severHost = `localhost:${server.address().port}`;
let counter = 0;
const expectedUrls = new Set();
const expectedProxyLogs = new Set();
for (const testCase of testCases) {
const url = `https://${testCase.host}:${testCase.port}${testCase.path}`;
// The invalid characters should all be stripped away before being sent.
expectedUrls.add(url.replaceAll(/\r|\n/g, ''));
expectedProxyLogs.add({
method: 'CONNECT',
url: severHost,
headers: { host: severHost },
});
https.request(url, common.mustCall((res) => {
res.on('error', common.mustNotCall());
res.setEncoding('utf8');
res.on('data', (data) => {
console.log(`[Proxy client] Received response from server for ${inspect(url)}: ${data.toString()}`);
});
res.on('close', common.mustCall(() => {
console.log(`[Proxy client] #${++counter} closed request for: ${inspect(url)}`);
// Finished all test cases.
if (counter === testCases.length) {
setImmediate(common.mustCall(() => {
console.log('All requests completed, shutting down.');
proxy.close();
server.close();
assert.deepStrictEqual(requests, expectedUrls);
const requestLogs = logs.filter((log) => !('error' in log));
// The client may reset a tunnel while the proxy is still relaying
// the upstream's TLS shutdown; that says nothing about the URLs.
const errors = logs.filter((log) => 'error' in log && log.error.code !== 'ECONNRESET');
assert.deepStrictEqual(new Set(requestLogs), expectedProxyLogs);
assert.deepStrictEqual(errors, []);
}));
}
}));
})).on('error', common.mustNotCall()).end();
}