Skip to content

Commit ef676a3

Browse files
http: normalize hosts for no_proxy
Normalize hostnames before comparing them with NO_PROXY entries so trailing dots, bracketed IPv6 literals, and IPv4-mapped loopback addresses are handled consistently. Fixes: #66853 Assisted-by: Codex Signed-off-by: Gabriel <hey@gabfon.com>
1 parent 697ecce commit ef676a3

2 files changed

Lines changed: 114 additions & 3 deletions

File tree

‎lib/internal/http.js‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const {
1919

2020
const { URL } = require('internal/url');
2121
const { Buffer } = require('buffer');
22-
const { isIPv4 } = require('internal/net');
22+
const { isIPv4, isIPv6 } = require('internal/net');
23+
const { canonicalizeIP } = internalBinding('cares_wrap');
2324
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
2425
let utcCache;
2526
let utcHeaderCache;
@@ -75,6 +76,30 @@ function ipToInt(ip) {
7576
return result >>> 0;
7677
}
7778

79+
function normalizeProxyHost(hostname) {
80+
let host = hostname.toLowerCase();
81+
82+
if (host[0] === '[' && host[host.length - 1] === ']') {
83+
host = host.slice(1, -1);
84+
}
85+
86+
if (host.endsWith('.')) {
87+
host = host.slice(0, -1);
88+
}
89+
90+
if (isIPv6(host)) {
91+
host = canonicalizeIP(host);
92+
if (host.startsWith('::ffff:')) {
93+
const ipv4 = host.slice(7);
94+
if (isIPv4(ipv4)) {
95+
host = ipv4;
96+
}
97+
}
98+
}
99+
100+
return host;
101+
}
102+
78103
// There are two factors in play when proxying the request:
79104
// 1. What the request protocol is, that is, whether users are sending it via
80105
// http.request or https.request, or whether they are sending
@@ -137,7 +162,7 @@ class ProxyConfig {
137162
this.auth = `Basic ${Buffer.from(auth).toString('base64')}`;
138163
}
139164
if (noProxyList) {
140-
this.bypassList = noProxyList.split(',').map((entry) => entry.trim().toLowerCase());
165+
this.bypassList = noProxyList.split(',').map((entry) => normalizeProxyHost(entry.trim()));
141166
} else {
142167
this.bypassList = []; // No bypass list provided.
143168
}
@@ -159,7 +184,7 @@ class ProxyConfig {
159184
return true; // No bypass list, always use the proxy.
160185
}
161186

162-
const host = hostname.toLowerCase();
187+
const host = normalizeProxyHost(hostname);
163188
const hostWithPort = port ? `${host}:${port}` : host;
164189

165190
for (let i = 0; i < bypassList.length; i++) {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// This tests that NO_PROXY matches alternate loopback host spellings.
2+
3+
import * as common from '../common/index.mjs';
4+
import assert from 'node:assert';
5+
import { once } from 'events';
6+
import http from 'node:http';
7+
import { runProxiedRequest } from '../common/proxy-server.js';
8+
9+
if (!common.hasIPv6) {
10+
common.skip('missing IPv6 support');
11+
}
12+
13+
const server4 = http.createServer(common.mustCall((req, res) => {
14+
res.writeHead(200, { 'Content-Type': 'text/plain' });
15+
res.end('Hello World\n');
16+
}, 2));
17+
server4.listen(0, '127.0.0.1');
18+
await once(server4, 'listening');
19+
20+
const server6 = http.createServer(common.mustCall((req, res) => {
21+
res.writeHead(200, { 'Content-Type': 'text/plain' });
22+
res.end('Hello World\n');
23+
}, 1));
24+
server6.listen(0, '::1');
25+
await once(server6, 'listening');
26+
27+
// Start a proxy server that should NOT be used.
28+
const proxy = http.createServer();
29+
proxy.on('request', common.mustNotCall());
30+
proxy.on('connect', common.mustNotCall());
31+
proxy.listen(0);
32+
await once(proxy, 'listening');
33+
34+
// A trailing dot in a DNS name should match the dotless NO_PROXY entry.
35+
{
36+
const { code, signal, stderr, stdout } = await runProxiedRequest({
37+
NODE_USE_ENV_PROXY: 1,
38+
REQUEST_URL: `http://localhost.:${server4.address().port}/test`,
39+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
40+
NO_PROXY: 'localhost',
41+
RESOLVE_TO_LOCALHOST: 'localhost.',
42+
});
43+
44+
assert.match(stdout, /Status Code: 200/);
45+
assert.match(stdout, /Hello World/);
46+
assert.match(stdout, /Resolving lookup for localhost\./);
47+
assert.strictEqual(stderr.trim(), '');
48+
assert.strictEqual(code, 0);
49+
assert.strictEqual(signal, null);
50+
}
51+
52+
// Bracketed IPv6 URL hosts should match unbracketed NO_PROXY entries.
53+
{
54+
const { code, signal, stderr, stdout } = await runProxiedRequest({
55+
NODE_USE_ENV_PROXY: 1,
56+
REQUEST_URL: `http://[::1]:${server6.address().port}/test`,
57+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
58+
NO_PROXY: '::1',
59+
});
60+
61+
assert.match(stdout, /Status Code: 200/);
62+
assert.match(stdout, /Hello World/);
63+
assert.strictEqual(stderr.trim(), '');
64+
assert.strictEqual(code, 0);
65+
assert.strictEqual(signal, null);
66+
}
67+
68+
// IPv4-mapped IPv6 loopback should match its IPv4 NO_PROXY entry.
69+
{
70+
const { code, signal, stderr, stdout } = await runProxiedRequest({
71+
NODE_USE_ENV_PROXY: 1,
72+
REQUEST_URL: `http://[::ffff:7f00:1]:${server4.address().port}/test`,
73+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
74+
NO_PROXY: 'localhost,127.0.0.1,::1',
75+
});
76+
77+
assert.match(stdout, /Status Code: 200/);
78+
assert.match(stdout, /Hello World/);
79+
assert.strictEqual(stderr.trim(), '');
80+
assert.strictEqual(code, 0);
81+
assert.strictEqual(signal, null);
82+
}
83+
84+
proxy.close();
85+
server6.close();
86+
server4.close();

0 commit comments

Comments
 (0)