| 1 | const LOOPBACK_HOSTNAMES = new Set(['localhost']);
|
|---|
| 2 |
|
|---|
| 3 | const isIPv4Loopback = (host) => {
|
|---|
| 4 | const parts = host.split('.');
|
|---|
| 5 | if (parts.length !== 4) return false;
|
|---|
| 6 | if (parts[0] !== '127') return false;
|
|---|
| 7 | return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
|
|---|
| 8 | };
|
|---|
| 9 |
|
|---|
| 10 | const isIPv6Loopback = (host) => {
|
|---|
| 11 | // Collapse all-zero groups: any form of ::1 / 0:0:...:0:1
|
|---|
| 12 | // First, strip any leading "::" by normalising with Set lookup of common forms,
|
|---|
| 13 | // then fall back to structural check.
|
|---|
| 14 | if (host === '::1') return true;
|
|---|
| 15 |
|
|---|
| 16 | // Check IPv4-mapped IPv6 loopback: ::ffff:<v4-loopback> or ::ffff:<hex-v4-loopback>
|
|---|
| 17 | // Node's URL parser normalises ::ffff:127.0.0.1 → ::ffff:7f00:1
|
|---|
| 18 | const v4MappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|---|
| 19 | if (v4MappedDotted) return isIPv4Loopback(v4MappedDotted[1]);
|
|---|
| 20 |
|
|---|
| 21 | const v4MappedHex = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|---|
| 22 | if (v4MappedHex) {
|
|---|
| 23 | const high = parseInt(v4MappedHex[1], 16);
|
|---|
| 24 | // High 16 bits must start with 127 (0x7f) — i.e. 0x7f00..0x7fff
|
|---|
| 25 | return high >= 0x7f00 && high <= 0x7fff;
|
|---|
| 26 | }
|
|---|
| 27 |
|
|---|
| 28 | // Full-form ::1 variants: any number of zero groups followed by trailing 1
|
|---|
| 29 | // e.g. 0:0:0:0:0:0:0:1, 0000:...:0001
|
|---|
| 30 | const groups = host.split(':');
|
|---|
| 31 | if (groups.length === 8) {
|
|---|
| 32 | for (let i = 0; i < 7; i++) {
|
|---|
| 33 | if (!/^0+$/.test(groups[i])) return false;
|
|---|
| 34 | }
|
|---|
| 35 | return /^0*1$/.test(groups[7]);
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | return false;
|
|---|
| 39 | };
|
|---|
| 40 |
|
|---|
| 41 | const isLoopback = (host) => {
|
|---|
| 42 | if (!host) return false;
|
|---|
| 43 | if (LOOPBACK_HOSTNAMES.has(host)) return true;
|
|---|
| 44 | if (isIPv4Loopback(host)) return true;
|
|---|
| 45 | return isIPv6Loopback(host);
|
|---|
| 46 | };
|
|---|
| 47 |
|
|---|
| 48 | const DEFAULT_PORTS = {
|
|---|
| 49 | http: 80,
|
|---|
| 50 | https: 443,
|
|---|
| 51 | ws: 80,
|
|---|
| 52 | wss: 443,
|
|---|
| 53 | ftp: 21,
|
|---|
| 54 | };
|
|---|
| 55 |
|
|---|
| 56 | const parseNoProxyEntry = (entry) => {
|
|---|
| 57 | let entryHost = entry;
|
|---|
| 58 | let entryPort = 0;
|
|---|
| 59 |
|
|---|
| 60 | if (entryHost.charAt(0) === '[') {
|
|---|
| 61 | const bracketIndex = entryHost.indexOf(']');
|
|---|
| 62 |
|
|---|
| 63 | if (bracketIndex !== -1) {
|
|---|
| 64 | const host = entryHost.slice(1, bracketIndex);
|
|---|
| 65 | const rest = entryHost.slice(bracketIndex + 1);
|
|---|
| 66 |
|
|---|
| 67 | if (rest.charAt(0) === ':' && /^\d+$/.test(rest.slice(1))) {
|
|---|
| 68 | entryPort = Number.parseInt(rest.slice(1), 10);
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | return [host, entryPort];
|
|---|
| 72 | }
|
|---|
| 73 | }
|
|---|
| 74 |
|
|---|
| 75 | const firstColon = entryHost.indexOf(':');
|
|---|
| 76 | const lastColon = entryHost.lastIndexOf(':');
|
|---|
| 77 |
|
|---|
| 78 | if (
|
|---|
| 79 | firstColon !== -1 &&
|
|---|
| 80 | firstColon === lastColon &&
|
|---|
| 81 | /^\d+$/.test(entryHost.slice(lastColon + 1))
|
|---|
| 82 | ) {
|
|---|
| 83 | entryPort = Number.parseInt(entryHost.slice(lastColon + 1), 10);
|
|---|
| 84 | entryHost = entryHost.slice(0, lastColon);
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | return [entryHost, entryPort];
|
|---|
| 88 | };
|
|---|
| 89 |
|
|---|
| 90 | // Convert IPv4-mapped IPv6 (::ffff:0:0/96 prefix) to IPv4 dotted form so both
|
|---|
| 91 | // sides of a NO_PROXY comparison see the same canonical address. Without this,
|
|---|
| 92 | // `NO_PROXY=192.168.1.5` would not match a request to `http://[::ffff:192.168.1.5]/`
|
|---|
| 93 | // (Node's URL parser normalises that to `[::ffff:c0a8:105]`), and vice-versa,
|
|---|
| 94 | // allowing the proxy-bypass policy to be circumvented by using the alternate
|
|---|
| 95 | // representation. Returns the input unchanged when not IPv4-mapped.
|
|---|
| 96 | const IPV4_MAPPED_DOTTED_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:(\d+\.\d+\.\d+\.\d+)$/i;
|
|---|
| 97 | const IPV4_MAPPED_HEX_RE = /^(?:::|(?:0{1,4}:){1,4}:|(?:0{1,4}:){5})ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;
|
|---|
| 98 |
|
|---|
| 99 | const unmapIPv4MappedIPv6 = (host) => {
|
|---|
| 100 | if (typeof host !== 'string' || host.indexOf(':') === -1) return host;
|
|---|
| 101 |
|
|---|
| 102 | const dotted = host.match(IPV4_MAPPED_DOTTED_RE);
|
|---|
| 103 | if (dotted) return dotted[1];
|
|---|
| 104 |
|
|---|
| 105 | const hex = host.match(IPV4_MAPPED_HEX_RE);
|
|---|
| 106 | if (hex) {
|
|---|
| 107 | const high = parseInt(hex[1], 16);
|
|---|
| 108 | const low = parseInt(hex[2], 16);
|
|---|
| 109 | return `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`;
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | return host;
|
|---|
| 113 | };
|
|---|
| 114 |
|
|---|
| 115 | const normalizeNoProxyHost = (hostname) => {
|
|---|
| 116 | if (!hostname) {
|
|---|
| 117 | return hostname;
|
|---|
| 118 | }
|
|---|
| 119 |
|
|---|
| 120 | if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') {
|
|---|
| 121 | hostname = hostname.slice(1, -1);
|
|---|
| 122 | }
|
|---|
| 123 |
|
|---|
| 124 | return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));
|
|---|
| 125 | };
|
|---|
| 126 |
|
|---|
| 127 | export default function shouldBypassProxy(location) {
|
|---|
| 128 | let parsed;
|
|---|
| 129 |
|
|---|
| 130 | try {
|
|---|
| 131 | parsed = new URL(location);
|
|---|
| 132 | } catch (_err) {
|
|---|
| 133 | return false;
|
|---|
| 134 | }
|
|---|
| 135 |
|
|---|
| 136 | const noProxy = (process.env.no_proxy || process.env.NO_PROXY || '').toLowerCase();
|
|---|
| 137 |
|
|---|
| 138 | if (!noProxy) {
|
|---|
| 139 | return false;
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | if (noProxy === '*') {
|
|---|
| 143 | return true;
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | const port =
|
|---|
| 147 | Number.parseInt(parsed.port, 10) || DEFAULT_PORTS[parsed.protocol.split(':', 1)[0]] || 0;
|
|---|
| 148 |
|
|---|
| 149 | const hostname = normalizeNoProxyHost(parsed.hostname.toLowerCase());
|
|---|
| 150 |
|
|---|
| 151 | return noProxy.split(/[\s,]+/).some((entry) => {
|
|---|
| 152 | if (!entry) {
|
|---|
| 153 | return false;
|
|---|
| 154 | }
|
|---|
| 155 |
|
|---|
| 156 | let [entryHost, entryPort] = parseNoProxyEntry(entry);
|
|---|
| 157 |
|
|---|
| 158 | entryHost = normalizeNoProxyHost(entryHost);
|
|---|
| 159 |
|
|---|
| 160 | if (!entryHost) {
|
|---|
| 161 | return false;
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | if (entryPort && entryPort !== port) {
|
|---|
| 165 | return false;
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | if (entryHost.charAt(0) === '*') {
|
|---|
| 169 | entryHost = entryHost.slice(1);
|
|---|
| 170 | }
|
|---|
| 171 |
|
|---|
| 172 | if (entryHost.charAt(0) === '.') {
|
|---|
| 173 | return hostname.endsWith(entryHost);
|
|---|
| 174 | }
|
|---|
| 175 |
|
|---|
| 176 | return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));
|
|---|
| 177 | });
|
|---|
| 178 | }
|
|---|