Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -4620,6 +4620,8 @@ The `NO_PROXY` environment variable supports several formats:
* `*.example.com` - Wildcard domain match
* `192.168.1.100` - Exact IP address match
* `192.168.1.1-192.168.1.100` - IP address range
* `::1` or `[::1]` - Exact IPv6 address match
* `::1-::100` - IPv6 address range
* `example.com:8080` - Hostname with specific port

Multiple entries should be separated by commas.
Expand Down
34 changes: 26 additions & 8 deletions lib/internal/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const {

const { URL } = require('internal/url');
const { Buffer } = require('buffer');
const { isIPv4 } = require('internal/net');
const { BlockList } = require('internal/blocklist');
const { isIPv4, isIPv6 } = require('internal/net');
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
let utcCache;

Expand Down Expand Up @@ -70,6 +71,11 @@ function ipToInt(ip) {
return result >>> 0;
}

function removeIPv6Brackets(host) {
return host[0] === '[' && host[host.length - 1] === ']' ?
host.slice(1, -1) : host;
}

// There are two factors in play when proxying the request:
// 1. What the request protocol is, that is, whether users are sending it via
// http.request or https.request, or whether they are sending
Expand Down Expand Up @@ -155,13 +161,18 @@ class ProxyConfig {
}

const host = hostname.toLowerCase();
const hostWithPort = port ? `${host}:${port}` : host;
const normalizedHost = removeIPv6Brackets(host);
const hostWithPort = port ?
(isIPv6(normalizedHost) ? `[${normalizedHost}]:${port}` : `${host}:${port}`) :
host;

for (let i = 0; i < bypassList.length; i++) {
const entry = bypassList[i];
const normalizedEntry = removeIPv6Brackets(entry);

if (entry === '*') return false; // * bypasses all hosts.
if (entry === host || entry === hostWithPort) return false; // Matching host and host:port
// Matching host and host:port.
if (entry === host || entry === hostWithPort || normalizedEntry === normalizedHost) return false;

// Follow curl's behavior: strip leading dot before matching suffixes.
if (entry[0] === '.') {
Expand All @@ -173,16 +184,23 @@ class ProxyConfig {
if (entry.startsWith('*.') && host.endsWith(entry.substring(1))) return false;

// Handle IP ranges (simple format like 192.168.1.0-192.168.1.255)
// TODO(joyeecheung): support IPv6.
if (entry.includes('-') && isIPv4(host)) {
if (entry.includes('-')) {
let { 0: startIP, 1: endIP } = entry.split('-');
startIP = startIP.trim();
endIP = endIP.trim();
if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP)) {
startIP = removeIPv6Brackets(startIP.trim());
endIP = removeIPv6Brackets(endIP.trim());
if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP) && isIPv4(normalizedHost)) {
const hostInt = ipToInt(host);
const startInt = ipToInt(startIP);
const endInt = ipToInt(endIP);
if (hostInt >= startInt && hostInt <= endInt) return false;
} else if (startIP && endIP && isIPv6(startIP) && isIPv6(endIP) && isIPv6(normalizedHost)) {
const blockList = new BlockList();
try {
blockList.addRange(startIP, endIP, 'ipv6');
} catch {
continue;
}
if (blockList.check(normalizedHost, 'ipv6')) return false;
}
}

Expand Down
116 changes: 116 additions & 0 deletions test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// This tests that NO_PROXY environment variable supports IPv6 ranges.

import * as common from '../common/index.mjs';
import assert from 'node:assert';
import { once } from 'events';
import http from 'node:http';
import { runProxiedRequest } from '../common/proxy-server.js';

if (!common.hasIPv6) {
common.skip('missing IPv6 support');
}

// Start a server to process the final request.
const server = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello IPv6\n');
}, 3));
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
server.listen(0, '::1');
await once(server, 'listening');

// Start a proxy server that should be used only when NO_PROXY does not match.
const proxy = http.createServer(common.mustCall((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Proxied Hello IPv6\n');
}, 2));
proxy.listen(0, '::1');
await once(proxy, 'listening');

// Test NO_PROXY with a bracketed exact IPv6 address.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: '[::1]',
});

// The request should succeed and bypass proxy
assert.match(stdout, /Status Code: 200/);
assert.match(stdout, /Hello IPv6/);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
}

// Test NO_PROXY with an IPv6 host and port.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: `[::1]:${server.address().port}`,
});

// The request should succeed and bypass proxy
assert.match(stdout, /Status Code: 200/);
assert.match(stdout, /Hello IPv6/);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
}

// Test NO_PROXY with IPv6 range (::1-::100 includes ::1).
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: '::1-::100',
});

// The request should succeed and bypass proxy
assert.match(stdout, /Status Code: 200/);
assert.match(stdout, /Hello IPv6/);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
}

// Test NO_PROXY with an IPv6 address outside the range.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: '::50-::100',
});

// The request should be proxied
assert.match(stdout, /Status Code: 200/);
assert.match(stdout, /Proxied Hello IPv6/);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
}

// Test NO_PROXY with an invalid IPv6 range.
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: '::100-::1',
});

// The request should be proxied
assert.match(stdout, /Status Code: 200/);
assert.match(stdout, /Proxied Hello IPv6/);
assert.strictEqual(stderr.trim(), '');
assert.strictEqual(code, 0);
assert.strictEqual(signal, null);
}

proxy.close();
server.close();
Loading