mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 05:38:47 +02:00

This patch implements proxy support for HTTP and HTTPS clients and agents in the `http` and `https` built-ins`. When NODE_USE_ENV_PROXY is set to 1, the default global agent would parse the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy settings from the environment variables, and proxy the requests sent through the built-in http/https client accordingly. To support this, `http.Agent` and `https.Agent` now accept a few new options: - `proxyEnv`: when it's an object, the agent would read and parse the HTTP_PROXY/http_proxy, HTTPS_PROXY/https_proxy, NO_PROXY/no_proxy properties from it, and apply them based on the protocol it uses to send requests. This option allows custom agents to reuse built-in proxy support by composing options. Global agents set this to `process.env` when NODE_USE_ENV_PROXY is 1. - `defaultPort` and `protocol`: these allow setting of the default port and protocol of the agents. We also need these when configuring proxy settings and deciding whether a request should be proxied. Implementation-wise, this adds a `ProxyConfig` internal class to handle parsing and application of proxy configurations. The configuration is parsed during agent construction. When requests are made, the `createConnection()` methods on the agents would check whether the request should be proxied. If yes, they either connect to the proxy server (in the case of HTTP reqeusts) or establish a tunnel (in the case of HTTPS requests) through either a TCP socket (if the proxy uses HTTP) or a TLS socket (if the proxy uses HTTPS). When proxying HTTPS requests through a tunnel, the connection listener is invoked after the tunnel is established. Tunnel establishment uses the timeout of the request options, if there is one. Otherwise it uses the timeout of the agent. If an error is encountered during tunnel establishment, an ERR_PROXY_TUNNEL would be emitted on the returned socket. If the proxy server sends a errored status code, the error would contain an `statusCode` property. If the error is caused by timeout, the error would contain a `proxyTunnelTimeout` property. This implementation honors the built-in socket pool and socket limits. Pooled sockets are still keyed by request endpoints, they are just connected to the proxy server instead, and the persistence of the connection can be maintained as long as the proxy server respects connection/proxy-connection or persist by default (HTTP/1.1) PR-URL: https://github.com/nodejs/node/pull/58980 Refs: https://github.com/nodejs/node/issues/57872 Refs: https://github.com/nodejs/node/issues/8381 Refs: https://github.com/nodejs/node/issues/15620 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
84 lines
2.9 KiB
JavaScript
84 lines
2.9 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) => {
|
|
requests.add(`https://localhost:${server.address().port}${req.url}`);
|
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
res.end(`Response for ${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 = testCases.length;
|
|
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, (res) => {
|
|
res.on('error', common.mustNotCall());
|
|
res.setEncoding('utf8');
|
|
res.on('data', () => {});
|
|
res.on('end', common.mustCall(() => {
|
|
console.log(`#${counter--} eneded response for: ${inspect(url)}`);
|
|
// Finished all test cases.
|
|
if (counter === 0) {
|
|
proxy.close();
|
|
server.close();
|
|
assert.deepStrictEqual(requests, expectedUrls);
|
|
assert.deepStrictEqual(new Set(logs), expectedProxyLogs);
|
|
}
|
|
}));
|
|
}).on('error', common.mustNotCall()).end();
|
|
}
|