W
File size: 4,155 Bytes
2b64d42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import net from 'node:net';
import { lookup as dnsLookup } from 'node:dns';

function ipv4ToInt(ip) {
  const parts = ip.split('.').map(n => Number(n));
  if (parts.length !== 4 || parts.some(n => !Number.isInteger(n) || n < 0 || n > 255)) return null;
  return (((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]) >>> 0;
}

function ipv4InCidr(ip, base, bits) {
  const n = ipv4ToInt(ip);
  const b = ipv4ToInt(base);
  if (n == null || b == null) return false;
  const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
  return (n & mask) === (b & mask);
}

function expandIpv6(ip) {
  let input = ip.toLowerCase();
  const zone = input.indexOf('%');
  if (zone !== -1) input = input.slice(0, zone);
  if (input === '::') return Array(8).fill(0);
  const [leftRaw, rightRaw] = input.split('::');
  const left = leftRaw ? leftRaw.split(':').filter(Boolean) : [];
  const right = rightRaw ? rightRaw.split(':').filter(Boolean) : [];
  const parsePart = (part) => {
    if (part.includes('.')) {
      const n = ipv4ToInt(part);
      if (n == null) return [];
      return [(n >>> 16) & 0xffff, n & 0xffff];
    }
    return [parseInt(part || '0', 16)];
  };
  const leftNums = left.flatMap(parsePart);
  const rightNums = right.flatMap(parsePart);
  const missing = 8 - leftNums.length - rightNums.length;
  if (missing < 0) return null;
  return [...leftNums, ...Array(missing).fill(0), ...rightNums].map(n => Number.isFinite(n) ? n : 0);
}

function ipv6StartsWith(ip, prefix, bits) {
  const a = expandIpv6(ip);
  const p = expandIpv6(prefix);
  if (!a || !p) return false;
  let remaining = bits;
  for (let i = 0; i < 8 && remaining > 0; i++) {
    const take = Math.min(16, remaining);
    const mask = (0xffff << (16 - take)) & 0xffff;
    if ((a[i] & mask) !== (p[i] & mask)) return false;
    remaining -= take;
  }
  return true;
}

function mappedIpv4(ip) {
  const m = ip.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
  if (m) return m[1];
  const parts = expandIpv6(ip);
  if (!parts) return null;
  if (parts.slice(0, 5).every(n => n === 0) && parts[5] === 0xffff) {
    return `${parts[6] >>> 8}.${parts[6] & 255}.${parts[7] >>> 8}.${parts[7] & 255}`;
  }
  return null;
}

export function isPrivateIp(address) {
  if (!address) return false;
  const ip = String(address).replace(/^\[|\]$/g, '').toLowerCase();
  const mapped = mappedIpv4(ip);
  if (mapped) return isPrivateIp(mapped);
  const family = net.isIP(ip);
  if (family === 4) {
    return ipv4InCidr(ip, '0.0.0.0', 8)
      || ipv4InCidr(ip, '10.0.0.0', 8)
      || ipv4InCidr(ip, '100.64.0.0', 10)
      || ipv4InCidr(ip, '127.0.0.0', 8)
      || ipv4InCidr(ip, '169.254.0.0', 16)
      || ipv4InCidr(ip, '172.16.0.0', 12)
      || ipv4InCidr(ip, '192.168.0.0', 16);
  }
  if (family === 6) {
    return ip === '::' || ip === '::1'
      || ipv6StartsWith(ip, 'fc00::', 7)
      || ipv6StartsWith(ip, 'fe80::', 10);
  }
  return false;
}

export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) {
  const host = String(hostname || '').replace(/^\[|\]$/g, '');
  if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PROXY_PRIVATE_HOST');
  if (net.isIP(host)) {
    if (isPrivateIp(host)) throw new Error('ERR_PROXY_PRIVATE_IP');
    return [{ address: host, family: net.isIP(host) }];
  }
  const result = await new Promise((resolve, reject) => {
    lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs));
  });
  const addrs = Array.isArray(result) ? result : [result];
  for (const a of addrs) {
    if (isPrivateIp(a.address)) throw new Error('ERR_PROXY_PRIVATE_IP');
  }
  return addrs;
}

export async function validateHostFormat(hostname, lookupFn = dnsLookup) {
  const host = String(hostname || '').replace(/^\[|\]$/g, '');
  if (!host) throw new Error('ERR_INVALID_HOST');
  if (net.isIP(host)) {
    return [{ address: host, family: net.isIP(host) }];
  }
  const result = await new Promise((resolve, reject) => {
    lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs));
  });
  return Array.isArray(result) ? result : [result];
}