diff --git a/doc/api/http.md b/doc/api/http.md index d7392b2f054bf2..81775c91ec4988 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -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. diff --git a/lib/internal/http.js b/lib/internal/http.js index aa3ec354dabf4a..79a3fe44ff0f55 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -1,6 +1,8 @@ 'use strict'; const { + Array, + BigInt, Date, NumberParseInt, Symbol, @@ -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; @@ -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 @@ -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; } } diff --git a/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs b/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs new file mode 100644 index 00000000000000..015ab30bd4f156 --- /dev/null +++ b/test/client-proxy/test-http-proxy-request-no-proxy-ipv6.mjs @@ -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();