| 1 | /** |
| 2 | * NO_PROXY 风格的内网直连匹配:解析逗号分隔的绕过列表, |
| 3 | * 支持域名(含子域名)、IP、IPv4 CIDR 网段与端口限定。 |
| 4 | */ |
| 5 | |
| 6 | export interface NoProxyEntry { |
| 7 | hostname: string |
| 8 | port: number | null |
| 9 | cidr: { address: number; bits: number } | null |
| 10 | } |
| 11 | |
| 12 | const parseIpv4 = (value: string): number | null => { |
| 13 | const parts = value.split('.') |
| 14 | if (parts.length !== 4) return null |
| 15 | let result = 0 |
| 16 | for (const part of parts) { |
| 17 | if (!/^\d{1,3}$/.test(part)) return null |
| 18 | const octet = Number(part) |
| 19 | if (octet > 255) return null |
| 20 | result = result * 256 + octet |
| 21 | } |
| 22 | return result |
| 23 | } |
| 24 | |
| 25 | const parseCidr = (value: string): { address: number; bits: number } | null => { |
| 26 | const [rawAddress, rawBits] = value.split('/') |
| 27 | if (rawAddress === undefined) return null |
| 28 | const address = parseIpv4(rawAddress) |
| 29 | if (address === null) return null |
| 30 | const bits = rawBits === undefined ? 32 : Number(rawBits) |
| 31 | if (!Number.isInteger(bits) || bits < 0 || bits > 32) return null |
| 32 | return { address, bits } |
| 33 | } |
| 34 | |
| 35 | const inCidr = (host: number, cidr: { address: number; bits: number }): boolean => { |
| 36 | if (cidr.bits === 0) return true |
| 37 | const mask = cidr.bits === 32 ? 0xffffffff : ~(0xffffffff >>> cidr.bits) |
| 38 | return (host & mask) >>> 0 === (cidr.address & mask) >>> 0 |
| 39 | } |
| 40 | |
| 41 | export const parseNoProxyEntries = (value: string | null | undefined): NoProxyEntry[] => { |
| 42 | if (typeof value !== 'string') return [] |
| 43 | return value |
| 44 | .split(/[,\s]+/) |
| 45 | .map((item) => item.trim()) |
| 46 | // 通配符 * 由 containsNoProxyWildcard 单独处理,不进入逐条匹配 |
| 47 | .filter((item) => item.length > 0 && item !== '*') |
| 48 | .map<NoProxyEntry>((item) => { |
| 49 | const lower = item.toLowerCase() |
| 50 | const portMatch = lower.match(/^(.+):(\d+)$/) |
| 51 | const rawHost = (portMatch ? portMatch[1] : lower).replace(/^\*\./, '').replace(/^\./, '') |
| 52 | if (!rawHost) return { hostname: '', port: null, cidr: null } |
| 53 | return { |
| 54 | hostname: rawHost, |
| 55 | port: portMatch ? Number(portMatch[2]) : null, |
| 56 | cidr: rawHost.includes('/') ? parseCidr(rawHost) : null |
| 57 | } |
| 58 | }) |
| 59 | .filter((entry) => entry.hostname.length > 0) |
| 60 | } |
| 61 | |
| 62 | export const matchesNoProxy = ( |
| 63 | entries: NoProxyEntry[], |
| 64 | hostname: string, |
| 65 | port: number |
| 66 | ): boolean => { |
| 67 | const host = hostname.toLowerCase().replace(/^\[|\]$/g, '') |
| 68 | if (!host) return false |
| 69 | for (const entry of entries) { |
| 70 | if (entry.port !== null && entry.port !== port) continue |
| 71 | if (entry.cidr) { |
| 72 | const hostAddress = parseIpv4(host) |
| 73 | if (hostAddress !== null && inCidr(hostAddress, entry.cidr)) return true |
| 74 | continue |
| 75 | } |
| 76 | if (host === entry.hostname) return true |
| 77 | if (host.endsWith(`.${entry.hostname}`)) return true |
| 78 | } |
| 79 | return false |
| 80 | } |
| 81 | |
| 82 | export const containsNoProxyWildcard = (value: string | null | undefined): boolean => { |
| 83 | if (typeof value !== 'string') return false |
| 84 | return value |
| 85 | .split(/[,\s]+/) |
| 86 | .map((item) => item.trim()) |
| 87 | .some((item) => item === '*') |
| 88 | } |
| 89 |