import * as plugins from '../../plugins.js'; /** * Utility class for IP address operations */ export class IpUtils { /** * Check if the IP matches any of the glob patterns * * This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization. * It's used to implement IP filtering based on security configurations. * * @param ip - The IP address to check * @param patterns - Array of glob patterns * @returns true if IP matches any pattern, false otherwise */ public static isGlobIPMatch(ip: string, patterns: string[]): boolean { if (!ip || !patterns || patterns.length === 0) return false; // Normalize the IP being checked const normalizedIPVariants = this.normalizeIP(ip); if (normalizedIPVariants.length === 0) return false; // Normalize the pattern IPs for consistent comparison const expandedPatterns = patterns.flatMap(pattern => this.normalizeIP(pattern)); // Check for any match between normalized IP variants and patterns return normalizedIPVariants.some((ipVariant) => expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern)) ); } /** * Normalize IP addresses for consistent comparison * * @param ip The IP address to normalize * @returns Array of normalized IP forms */ public static normalizeIP(ip: string): string[] { if (!ip) return []; // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) if (ip.startsWith('::ffff:')) { const ipv4 = ip.slice(7); return [ip, ipv4]; } // Handle IPv4 addresses by also checking IPv4-mapped form if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) { return [ip, `::ffff:${ip}`]; } return [ip]; } /** * Check if an IP is authorized using security rules * * @param ip - The IP address to check * @param allowedIPs - Array of allowed IP patterns * @param blockedIPs - Array of blocked IP patterns * @returns true if IP is authorized, false if blocked */ public static isIPAuthorized(ip: string, allowedIPs: string[] = [], blockedIPs: string[] = []): boolean { // Skip IP validation if no rules are defined if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) { return true; } // First check if IP is blocked - blocked IPs take precedence if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) { return false; } // Then check if IP is allowed (if no allowed IPs are specified, all non-blocked IPs are allowed) return allowedIPs.length === 0 || this.isGlobIPMatch(ip, allowedIPs); } /** * Check if an IP address is a private network address * * @param ip The IP address to check * @returns true if the IP is a private network address, false otherwise */ public static isPrivateIP(ip: string): boolean { if (!ip) return false; // Handle IPv4-mapped IPv6 addresses if (ip.startsWith('::ffff:')) { ip = ip.slice(7); } // Check IPv4 private ranges if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) { const parts = ip.split('.').map(Number); // Check common private ranges // 10.0.0.0/8 if (parts[0] === 10) return true; // 172.16.0.0/12 if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 192.168.0.0/16 if (parts[0] === 192 && parts[1] === 168) return true; // 127.0.0.0/8 (localhost) if (parts[0] === 127) return true; return false; } // IPv6 local addresses return ip === '::1' || ip.startsWith('fc00:') || ip.startsWith('fd00:') || ip.startsWith('fe80:'); } /** * Check if an IP address is a public network address * * @param ip The IP address to check * @returns true if the IP is a public network address, false otherwise */ public static isPublicIP(ip: string): boolean { return !this.isPrivateIP(ip); } /** * Convert a subnet CIDR to an IP range for filtering * * @param cidr The CIDR notation (e.g., "192.168.1.0/24") * @returns Array of glob patterns that match the CIDR range */ public static cidrToGlobPatterns(cidr: string): string[] { if (!cidr || !cidr.includes('/')) return []; const [ipPart, prefixPart] = cidr.split('/'); const prefix = parseInt(prefixPart, 10); if (isNaN(prefix) || prefix < 0 || prefix > 32) return []; // For IPv4 only for now if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ipPart)) return []; const ipParts = ipPart.split('.').map(Number); const fullMask = Math.pow(2, 32 - prefix) - 1; // Convert IP to a numeric value const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3]; // Calculate network address (IP & ~fullMask) const networkNum = ipNum & ~fullMask; // For large ranges, return wildcard patterns if (prefix <= 8) { return [`${(networkNum >>> 24) & 255}.*.*.*`]; } else if (prefix <= 16) { return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.*.*`]; } else if (prefix <= 24) { return [`${(networkNum >>> 24) & 255}.${(networkNum >>> 16) & 255}.${(networkNum >>> 8) & 255}.*`]; } // For small ranges, create individual IP patterns const patterns = []; const maxAddresses = Math.min(256, Math.pow(2, 32 - prefix)); for (let i = 0; i < maxAddresses; i++) { const currentIpNum = networkNum + i; patterns.push( `${(currentIpNum >>> 24) & 255}.${(currentIpNum >>> 16) & 255}.${(currentIpNum >>> 8) & 255}.${currentIpNum & 255}` ); } return patterns; } }