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

6 files changed

Lines changed: 262 additions & 0 deletions

File tree

‎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
returnmainEntry;
155156
}
156157

158+
functionsetupHttpProxy(){
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+
constenvHttpProxyAgent=newEnvHttpProxyAgent();
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
functionsetupUserModules(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+
constnet=require('net');
4+
consthttp=require('http');
5+
constassert=require('assert');
6+
7+
functionlogRequest(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+
constlogs=[];
19+
20+
constproxy=http.createServer();
21+
proxy.on('request',(req,res)=>{
22+
logRequest(logs,req);
23+
const[hostname,port]=req.headers.host.split(':');
24+
consttargetPort=port||80;
25+
26+
constoptions={
27+
hostname: hostname,
28+
port: targetPort,
29+
path: req.url,
30+
method: req.method,
31+
headers: req.headers,
32+
};
33+
34+
constproxyReq=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+
constproxyReq=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=asyncfunction(envExtension,expectation){
78+
const{ spawnPromisified }=require('./');
79+
constfixtures=require('./fixtures');
80+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(
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+
constresponse=awaitfetch(process.env.FETCH_URL);
2+
constbody=awaitresponse.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+
constcommon=require('../common');
4+
constassert=require('assert');
5+
const{ once }=require('events');
6+
consthttp=require('http');
7+
const{ createProxyServer, checkProxiedRequest }=require('../common/proxy-server');
8+
9+
(async()=>{
10+
// Start a server to process the final request.
11+
constserver=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+
awaitonce(server,'listening');
17+
18+
// Start a minimal proxy server.
19+
const{ proxy, logs }=createProxyServer();
20+
proxy.listen(0);
21+
awaitonce(proxy,'listening');
22+
23+
constserverHost=`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+
constexpectedLogs=[{
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+
awaitcheckProxiedRequest({
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+
awaitcheckProxiedRequest({
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+
constcommon=require('../common');
4+
if(!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
constfixtures=require('../common/fixtures');
8+
constassert=require('assert');
9+
consthttps=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+
constserver=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+
awaitonce(server,'listening');
24+
25+
// Start a minimal proxy server.
26+
const{ proxy, logs }=createProxyServer();
27+
proxy.listen(0);
28+
awaitonce(proxy,'listening');
29+
30+
constserverHost=`localhost:${server.address().port}`;
31+
32+
constexpectedLogs=[{
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+
awaitcheckProxiedRequest({
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+
awaitcheckProxiedRequest({
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)