import type { IMatcher, IIpMatchOptions } from '../types.js'; /** * IpMatcher provides comprehensive IP address matching functionality * Supporting exact matches, CIDR notation, ranges, and wildcards */ export class IpMatcher implements IMatcher { /** * Check if a value is a valid IPv4 address */ static isValidIpv4(ip: string): boolean { const parts = ip.split('.'); if (parts.length !== 4) return false; return parts.every(part => { const num = parseInt(part, 10); return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString(); }); } /** * Check if a value is a valid IPv6 address (simplified check) */ static isValidIpv6(ip: string): boolean { // Basic IPv6 validation - can be enhanced const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){1,7}|:):|(([0-9a-fA-F]{1,4}:){1,6}|::):[0-9a-fA-F]{1,4})$/; return ipv6Regex.test(ip); } /** * Convert IP address to numeric value for comparison */ private static ipToNumber(ip: string): number { const parts = ip.split('.'); return parts.reduce((acc, part, index) => { return acc + (parseInt(part, 10) << (8 * (3 - index))); }, 0); } /** * Match an IP against a CIDR notation pattern */ static matchCidr(cidr: string, ip: string): boolean { const [range, bits] = cidr.split('/'); if (!bits || !this.isValidIpv4(range) || !this.isValidIpv4(ip)) { return false; } const rangeMask = parseInt(bits, 10); if (isNaN(rangeMask) || rangeMask < 0 || rangeMask > 32) { return false; } const rangeNum = this.ipToNumber(range); const ipNum = this.ipToNumber(ip); const mask = (-1 << (32 - rangeMask)) >>> 0; return (rangeNum & mask) === (ipNum & mask); } /** * Match an IP against a wildcard pattern */ static matchWildcard(pattern: string, ip: string): boolean { if (!this.isValidIpv4(ip)) return false; const patternParts = pattern.split('.'); const ipParts = ip.split('.'); if (patternParts.length !== 4) return false; return patternParts.every((part, index) => { if (part === '*') return true; return part === ipParts[index]; }); } /** * Match an IP against a range (e.g., "192.168.1.1-192.168.1.100") */ static matchRange(range: string, ip: string): boolean { const [start, end] = range.split('-').map(s => s.trim()); if (!start || !end || !this.isValidIpv4(start) || !this.isValidIpv4(end) || !this.isValidIpv4(ip)) { return false; } const startNum = this.ipToNumber(start); const endNum = this.ipToNumber(end); const ipNum = this.ipToNumber(ip); return ipNum >= startNum && ipNum <= endNum; } /** * Match an IP pattern against an IP address * Supports multiple formats: * - Exact match: "192.168.1.1" * - CIDR: "192.168.1.0/24" * - Wildcard: "192.168.1.*" * - Range: "192.168.1.1-192.168.1.100" */ static match( pattern: string, ip: string, options: IIpMatchOptions = {} ): boolean { // Handle null/undefined cases if (!pattern || !ip) { return false; } // Normalize inputs const normalizedPattern = pattern.trim(); const normalizedIp = ip.trim(); // Extract IPv4 from IPv6-mapped addresses (::ffff:192.168.1.1) const ipv4Match = normalizedIp.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/i); const testIp = ipv4Match ? ipv4Match[1] : normalizedIp; // Exact match if (normalizedPattern === testIp) { return true; } // CIDR notation if (options.allowCidr !== false && normalizedPattern.includes('/')) { return this.matchCidr(normalizedPattern, testIp); } // Wildcard matching if (normalizedPattern.includes('*')) { return this.matchWildcard(normalizedPattern, testIp); } // Range matching if (options.allowRanges !== false && normalizedPattern.includes('-')) { return this.matchRange(normalizedPattern, testIp); } return false; } /** * Check if an IP is authorized based on allow and block lists */ static isAuthorized( ip: string, allowList: string[] = [], blockList: string[] = [] ): boolean { // If IP is in block list, deny if (blockList.some(pattern => this.match(pattern, ip))) { return false; } // If allow list is empty, allow all (except blocked) if (allowList.length === 0) { return true; } // If allow list exists, IP must match return allowList.some(pattern => this.match(pattern, ip)); } /** * Calculate the specificity of an IP pattern * Higher values mean more specific patterns */ static calculateSpecificity(pattern: string): number { if (!pattern) return 0; let score = 0; // Exact IPs are most specific if (this.isValidIpv4(pattern) || this.isValidIpv6(pattern)) { score += 100; } // CIDR notation if (pattern.includes('/')) { const [, bits] = pattern.split('/'); const maskBits = parseInt(bits, 10); if (!isNaN(maskBits)) { score += maskBits; // Higher mask = more specific } } // Wildcard patterns const wildcards = (pattern.match(/\*/g) || []).length; score -= wildcards * 20; // More wildcards = less specific // Range patterns are somewhat specific if (pattern.includes('-')) { score += 30; } return score; } /** * Instance method for interface compliance */ match(pattern: string, ip: string, options?: IIpMatchOptions): boolean { return IpMatcher.match(pattern, ip, options); } }