Skip to content

Commit ada34bd

Browse files
joyeecheungRafaelGSS
authored andcommitted
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 <matteo.collina@gmail.com>
1 parent 9de6943 commit ada34bd

File tree

6 files changed

+262
-0
lines changed

6 files changed

+262
-0
lines changed

doc/api/cli.md

Lines changed: 15 additions & 0 deletions
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

lib/internal/process/pre_execution.js

Lines changed: 16 additions & 0 deletions
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);

test/common/proxy-server.js

Lines changed: 100 additions & 0 deletions
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+
};

test/fixtures/fetch-and-log.mjs

Lines changed: 3 additions & 0 deletions
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);
Lines changed: 61 additions & 0 deletions
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());
Lines changed: 67 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy