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 @@ -4371,6 +4371,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
43 changes: 39 additions & 4 deletions lib/internal/http.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

const {
Array,
BigInt,
Date,
NumberParseInt,
Symbol,
Expand All @@ -16,7 +18,7 @@ const {

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

Expand Down Expand Up @@ -67,6 +69,35 @@ function ipToInt(ip) {
return result >>> 0;
}

function ipv6ToBigInt(ip) {
const cleanIp = ip.startsWith('[') ? ip.slice(1, -1) : ip;

const parts = cleanIp.split('::');
let left = parts[0];
let right = parts[1] || '';

if (parts.length === 2) {
const leftParts = left.split(':').filter((p) => p.length > 0);
const rightParts = right.split(':').filter((p) => p.length > 0);
const missingParts = 8 - leftParts.length - rightParts.length;

left = leftParts.join(':');
right = Array(missingParts).fill('0').join(':') + (right ? ':' + rightParts.join(':') : '');
}

const fullAddress = (left + (right ? ':' + right : '')).replace(/^:|:$/g, '');
const hexParts = fullAddress.split(':');

let result = BigInt(0);
for (const hexVal of hexParts) {
if (hexVal) {
result = (result << BigInt(16)) + BigInt('0x' + hexVal);
}
}

return result;
}

// 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 @@ -161,16 +192,20 @@ 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)) {
if (startIP && endIP && isIPv4(startIP) && isIPv4(endIP) && isIPv4(host)) {
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(host)) {
const hostInt = ipv6ToBigInt(host);
const startInt = ipv6ToBigInt(startIP);
const endInt = ipv6ToBigInt(endIP);
if (hostInt >= startInt && hostInt <= endInt) return false;
}
}

Expand Down
75 changes: 75 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,75 @@
// 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';

// 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');
}, 1));
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.
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 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 another IPv6 address within the range (::50)
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::1]:${server.address().port}/test`,
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
NO_PROXY: '::50-::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 (::200)
{
const { code, signal, stderr, stdout } = await runProxiedRequest({
NODE_USE_ENV_PROXY: 1,
REQUEST_URL: `http://[::200]:${server.address().port}/test`,
HTTP_PROXY: `http://[::1]:${proxy.address().port}`,
NO_PROXY: '::1-::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);
}

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