Philipp Kunz 54ffbadb86 feat(routing): Implement unified routing and matching system
- 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.
2025-06-02 03:57:52 +00:00

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);
}
}