| // 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 logSet = new Set(logs); |
| for (const log of logSet) { |
| if (log.source === 'proxy connect' && log.error?.code === 'EPIPE') { |
| // There can be a race from eagerly shutting down the servers and severing |
| // two pipes at the same time but for the purpose of this test, we only |
| // care about whether the requests are initiated from the client as expected, |
| // not how the upstream/proxy servers behave. Ignore EPIPE errors from them.. |
| // Refs: https://github.com/nodejs/node/issues/59741 |
| console.log('Ignoring EPIPE error from proxy connect', log.error); |
| logSet.delete(log); |
| } |
| } |
| assert.deepStrictEqual(logSet, expectedProxyLogs); |
| })); |
| } |
| })); |
| })).on('error', common.mustNotCall()).end(); |
| } |