- Introduced a centralized routing module with comprehensive matchers for domains, headers, IPs, and paths. - Added DomainMatcher for domain pattern matching with support for wildcards and specificity calculation. - Implemented HeaderMatcher for HTTP header matching, including exact matches and pattern support. - Developed IpMatcher for IP address matching, supporting CIDR notation, ranges, and wildcards. - Created PathMatcher for path matching with parameter extraction and wildcard support. - Established RouteSpecificity class to calculate and compare route specificity scores. - Enhanced HttpRouter to utilize the new matching system, supporting both modern and legacy route configurations. - Added detailed logging and error handling for routing operations.
207 lines
5.6 KiB
TypeScript
207 lines
5.6 KiB
TypeScript
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<boolean, IIpMatchOptions> {
|
|
/**
|
|
* 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);
|
|
}
|
|
} |