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; // Check each pattern for (const pattern of patterns) { // Handle CIDR notation if (pattern.includes('/')) { if (this.matchCIDR(ip, pattern)) { return true; } continue; } // Handle range notation if (pattern.includes('-') && !pattern.includes('*')) { if (this.matchIPRange(ip, pattern)) { return true; } continue; } // Expand shorthand patterns for glob matching let expandedPattern = pattern; if (pattern.includes('*') && !pattern.includes(':')) { const parts = pattern.split('.'); while (parts.length < 4) { parts.push('*'); } expandedPattern = parts.join('.'); } // Normalize and check with minimatch const normalizedPatterns = this.normalizeIP(expandedPattern); for (const ipVariant of normalizedIPVariants) { for (const normalizedPattern of normalizedPatterns) { if (plugins.minimatch(ipVariant, normalizedPattern)) { return true; } } } } return false; } /** * 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); } /** * Check if an IP matches a CIDR notation * * @param ip The IP address to check * @param cidr The CIDR notation (e.g., "192.168.1.0/24") * @returns true if IP is within the CIDR range */ private static matchCIDR(ip: string, cidr: string): boolean { if (!cidr.includes('/')) return false; const [networkAddr, prefixStr] = cidr.split('/'); const prefix = parseInt(prefixStr, 10); // Handle IPv4-mapped IPv6 in the IP being checked let checkIP = ip; if (checkIP.startsWith('::ffff:')) { checkIP = checkIP.slice(7); } // Handle IPv6 CIDR if (networkAddr.includes(':')) { // TODO: Implement IPv6 CIDR matching return false; } // IPv4 CIDR matching if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false; if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(networkAddr)) return false; if (isNaN(prefix) || prefix < 0 || prefix > 32) return false; const ipParts = checkIP.split('.').map(Number); const netParts = networkAddr.split('.').map(Number); // Validate IP parts for (const part of [...ipParts, ...netParts]) { if (part < 0 || part > 255) return false; } // Convert to 32-bit integers const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3]; const netNum = (netParts[0] << 24) | (netParts[1] << 16) | (netParts[2] << 8) | netParts[3]; // Create mask const mask = (-1 << (32 - prefix)) >>> 0; // Check if IP is in network range return (ipNum & mask) === (netNum & mask); } /** * Check if an IP matches a range notation * * @param ip The IP address to check * @param range The range notation (e.g., "192.168.1.1-192.168.1.100") * @returns true if IP is within the range */ private static matchIPRange(ip: string, range: string): boolean { if (!range.includes('-')) return false; const [startIP, endIP] = range.split('-').map(s => s.trim()); // Handle IPv4-mapped IPv6 in the IP being checked let checkIP = ip; if (checkIP.startsWith('::ffff:')) { checkIP = checkIP.slice(7); } // Only handle IPv4 for now if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(checkIP)) return false; if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(startIP)) return false; if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(endIP)) return false; const ipParts = checkIP.split('.').map(Number); const startParts = startIP.split('.').map(Number); const endParts = endIP.split('.').map(Number); // Validate parts for (const part of [...ipParts, ...startParts, ...endParts]) { if (part < 0 || part > 255) return false; } // Convert to 32-bit integers for comparison const ipNum = (ipParts[0] << 24) | (ipParts[1] << 16) | (ipParts[2] << 8) | ipParts[3]; const startNum = (startParts[0] << 24) | (startParts[1] << 16) | (startParts[2] << 8) | startParts[3]; const endNum = (endParts[0] << 24) | (endParts[1] << 16) | (endParts[2] << 8) | endParts[3]; // Convert to unsigned for proper comparison const ipUnsigned = ipNum >>> 0; const startUnsigned = startNum >>> 0; const endUnsigned = endNum >>> 0; return ipUnsigned >= startUnsigned && ipUnsigned <= endUnsigned; } /** * 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; } }