175 lines
5.6 KiB
TypeScript
175 lines
5.6 KiB
TypeScript
|
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;
|
||
|
}
|
||
|
}
|