Skip to content

Commit 5a7b7d2

Browse files
authored
http: support http proxy for fetch under NODE_USE_ENV_PROXY
When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY` environment variables during startup, and tunnels requests over the specified proxy. This currently only affects requests sent over `fetch()`. Support for other built-in `http` and `https` methods is under way. PR-URL: #57165 Refs: nodejs/undici#1650 Reviewed-By: Matteo Collina <[email protected]>
1 parent 02a985d commit 5a7b7d2

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

Diff for: doc/api/cli.md

+15
Original file line numberDiff line numberDiff line change
@@ -3545,6 +3545,21 @@ If `value` equals `'0'`, certificate validation is disabled for TLS connections.
35453545
This makes TLS, and HTTPS by extension, insecure. The use of this environment
35463546
variable is strongly discouraged.
35473547

3548+
### `NODE_USE_ENV_PROXY=1`
3549+
3550+
<!-- YAML
3551+
added: REPLACEME
3552+
-->
3553+
3554+
> Stability: 1.1 - Active Development
3555+
3556+
When enabled, Node.js parses the `HTTP_PROXY`, `HTTPS_PROXY` and `NO_PROXY`
3557+
environment variables during startup, and tunnels requests over the
3558+
specified proxy.
3559+
3560+
This currently only affects requests sent over `fetch()`. Support for other
3561+
built-in `http` and `https` methods is under way.
3562+
35483563
### `NODE_V8_COVERAGE=dir`
35493564

35503565
When set, Node.js will begin outputting [V8 JavaScript code coverage][] and

Diff for: lib/internal/process/pre_execution.js

+16
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ function prepareExecution(options) {
119119
initializeConfigFileSupport();
120120

121121
require('internal/dns/utils').initializeDns();
122+
setupHttpProxy();
122123

123124
if (isMainThread) {
124125
assert(internalBinding('worker').isMainThread);
@@ -154,6 +155,21 @@ function prepareExecution(options) {
154155
return mainEntry;
155156
}
156157

158+
function setupHttpProxy() {
159+
if (process.env.NODE_USE_ENV_PROXY &&
160+
(process.env.HTTP_PROXY || process.env.HTTPS_PROXY ||
161+
process.env.http_proxy || process.env.https_proxy)) {
162+
const { setGlobalDispatcher, EnvHttpProxyAgent } = require('internal/deps/undici/undici');
163+
const envHttpProxyAgent = new EnvHttpProxyAgent();
164+
setGlobalDispatcher(envHttpProxyAgent);
165+
// TODO(joyeecheung): This currently only affects fetch. Implement handling in the
166+
// http/https Agent constructor too.
167+
// TODO(joyeecheung): This is currently guarded with NODE_USE_ENV_PROXY. Investigate whether
168+
// it's possible to enable it by default without stepping on other existing libraries that
169+
// sets the global dispatcher or monkey patches the global agent.
170+
}
171+
}
172+
157173
function setupUserModules(forceDefaultLoader = false) {
158174
initializeCJSLoader();
159175
initializeESMLoader(forceDefaultLoader);

Diff for: test/common/proxy-server.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
'use strict';
2+
3+
const net = require('net');
4+
const http = require('http');
5+
const assert = require('assert');
6+
7+
function logRequest(logs, req) {
8+
logs.push({
9+
method: req.method,
10+
url: req.url,
11+
headers: { ...req.headers },
12+
});
13+
}
14+
15+
// This creates a minimal proxy server that logs the requests it gets
16+
// to an array before performing proxying.
17+
exports.createProxyServer = function() {
18+
const logs = [];
19+
20+
const proxy = http.createServer();
21+
proxy.on('request', (req, res) => {
22+
logRequest(logs, req);
23+
const [hostname, port] = req.headers.host.split(':');
24+
const targetPort = port || 80;
25+
26+
const options = {
27+
hostname: hostname,
28+
port: targetPort,
29+
path: req.url,
30+
method: req.method,
31+
headers: req.headers,
32+
};
33+
34+
const proxyReq = http.request(options, (proxyRes) => {
35+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
36+
proxyRes.pipe(res, { end: true });
37+
});
38+
39+
proxyReq.on('error', (err) => {
40+
logs.push({ error: err, source: 'proxy request' });
41+
res.writeHead(500);
42+
res.end('Proxy error: ' + err.message);
43+
});
44+
45+
req.pipe(proxyReq, { end: true });
46+
});
47+
48+
proxy.on('connect', (req, res, head) => {
49+
logRequest(logs, req);
50+
51+
const [hostname, port] = req.url.split(':');
52+
const proxyReq = net.connect(port, hostname, () => {
53+
res.write(
54+
'HTTP/1.1 200 Connection Established\r\n' +
55+
'Proxy-agent: Node.js-Proxy\r\n' +
56+
'\r\n',
57+
);
58+
proxyReq.write(head);
59+
res.pipe(proxyReq);
60+
proxyReq.pipe(res);
61+
});
62+
63+
proxyReq.on('error', (err) => {
64+
logs.push({ error: err, source: 'proxy request' });
65+
res.write('HTTP/1.1 500 Connection Error\r\n\r\n');
66+
res.end('Proxy error: ' + err.message);
67+
});
68+
});
69+
70+
proxy.on('error', (err) => {
71+
logs.push({ error: err, source: 'proxy server' });
72+
});
73+
74+
return { proxy, logs };
75+
};
76+
77+
exports.checkProxiedRequest = async function(envExtension, expectation) {
78+
const { spawnPromisified } = require('./');
79+
const fixtures = require('./fixtures');
80+
const { code, signal, stdout, stderr } = await spawnPromisified(
81+
process.execPath,
82+
[fixtures.path('fetch-and-log.mjs')], {
83+
env: {
84+
...process.env,
85+
...envExtension,
86+
},
87+
});
88+
89+
assert.deepStrictEqual({
90+
stderr: stderr.trim(),
91+
stdout: stdout.trim(),
92+
code,
93+
signal,
94+
}, {
95+
stderr: '',
96+
code: 0,
97+
signal: null,
98+
...expectation,
99+
});
100+
};

Diff for: test/fixtures/fetch-and-log.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const response = await fetch(process.env.FETCH_URL);
2+
const body = await response.text();
3+
console.log(body);

Diff for: test/parallel/test-http-proxy-fetch.js

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { once } = require('events');
6+
const http = require('http');
7+
const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server');
8+
9+
(async () => {
10+
// Start a server to process the final request.
11+
const server = http.createServer(common.mustCall((req, res) => {
12+
res.end('Hello world');
13+
}, 2));
14+
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
15+
server.listen(0);
16+
await once(server, 'listening');
17+
18+
// Start a minimal proxy server.
19+
const { proxy, logs } = createProxyServer();
20+
proxy.listen(0);
21+
await once(proxy, 'listening');
22+
23+
const serverHost = `localhost:${server.address().port}`;
24+
25+
// FIXME(undici:4083): undici currently always tunnels the request over
26+
// CONNECT, no matter it's HTTP traffic or not, which is different from e.g.
27+
// how curl behaves.
28+
const expectedLogs = [{
29+
method: 'CONNECT',
30+
url: serverHost,
31+
headers: {
32+
// FIXME(undici:4086): this should be keep-alive.
33+
connection: 'close',
34+
host: serverHost
35+
}
36+
}];
37+
38+
// Check upper-cased HTTPS_PROXY environment variable.
39+
await checkProxiedRequest({
40+
NODE_USE_ENV_PROXY: 1,
41+
FETCH_URL: `http://${serverHost}/test`,
42+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
43+
}, {
44+
stdout: 'Hello world',
45+
});
46+
assert.deepStrictEqual(logs, expectedLogs);
47+
48+
// Check lower-cased https_proxy environment variable.
49+
logs.splice(0, logs.length);
50+
await checkProxiedRequest({
51+
NODE_USE_ENV_PROXY: 1,
52+
FETCH_URL: `http://${serverHost}/test`,
53+
http_proxy: `http://localhost:${proxy.address().port}`,
54+
}, {
55+
stdout: 'Hello world',
56+
});
57+
assert.deepStrictEqual(logs, expectedLogs);
58+
59+
proxy.close();
60+
server.close();
61+
})().then(common.mustCall());

Diff for: test/parallel/test-https-proxy-fetch.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const fixtures = require('../common/fixtures');
8+
const assert = require('assert');
9+
const https = require('https');
10+
const { once } = require('events');
11+
const { createProxyServer, checkProxiedRequest } = require('../common/proxy-server');
12+
13+
(async () => {
14+
// Start a server to process the final request.
15+
const server = https.createServer({
16+
cert: fixtures.readKey('agent8-cert.pem'),
17+
key: fixtures.readKey('agent8-key.pem'),
18+
}, common.mustCall((req, res) => {
19+
res.end('Hello world');
20+
}, 2));
21+
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
22+
server.listen(0);
23+
await once(server, 'listening');
24+
25+
// Start a minimal proxy server.
26+
const { proxy, logs } = createProxyServer();
27+
proxy.listen(0);
28+
await once(proxy, 'listening');
29+
30+
const serverHost = `localhost:${server.address().port}`;
31+
32+
const expectedLogs = [{
33+
method: 'CONNECT',
34+
url: serverHost,
35+
headers: {
36+
// FIXME(undici:4086): this should be keep-alive.
37+
connection: 'close',
38+
host: serverHost
39+
}
40+
}];
41+
42+
// Check upper-cased HTTPS_PROXY environment variable.
43+
await checkProxiedRequest({
44+
NODE_USE_ENV_PROXY: 1,
45+
FETCH_URL: `https://${serverHost}/test`,
46+
HTTPS_PROXY: `http://localhost:${proxy.address().port}`,
47+
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
48+
}, {
49+
stdout: 'Hello world',
50+
});
51+
assert.deepStrictEqual(logs, expectedLogs);
52+
53+
// Check lower-cased https_proxy environment variable.
54+
logs.splice(0, logs.length);
55+
await checkProxiedRequest({
56+
NODE_USE_ENV_PROXY: 1,
57+
FETCH_URL: `https://${serverHost}/test`,
58+
https_proxy: `http://localhost:${proxy.address().port}`,
59+
NODE_EXTRA_CA_CERTS: fixtures.path('keys', 'fake-startcom-root-cert.pem'),
60+
}, {
61+
stdout: 'Hello world',
62+
});
63+
assert.deepStrictEqual(logs, expectedLogs);
64+
65+
proxy.close();
66+
server.close();
67+
})().then(common.mustCall());

0 commit comments

Comments
 (0)