- 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.
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
import type { IMatcher, IDomainMatchOptions } from '../types.js';
|
|
|
|
/**
|
|
* DomainMatcher provides comprehensive domain matching functionality
|
|
* Supporting exact matches, wildcards, and case-insensitive matching
|
|
*/
|
|
export class DomainMatcher implements IMatcher<boolean, IDomainMatchOptions> {
|
|
private static wildcardToRegex(pattern: string): RegExp {
|
|
// Escape special regex characters except *
|
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
// Replace * with regex equivalent
|
|
const regexPattern = escaped.replace(/\*/g, '.*');
|
|
return new RegExp(`^${regexPattern}$`, 'i');
|
|
}
|
|
|
|
/**
|
|
* Match a domain pattern against a hostname
|
|
* @param pattern The pattern to match (supports wildcards like *.example.com)
|
|
* @param hostname The hostname to test
|
|
* @param options Matching options
|
|
* @returns true if the hostname matches the pattern
|
|
*/
|
|
static match(
|
|
pattern: string,
|
|
hostname: string,
|
|
options: IDomainMatchOptions = {}
|
|
): boolean {
|
|
// Handle null/undefined cases
|
|
if (!pattern || !hostname) {
|
|
return false;
|
|
}
|
|
|
|
// Normalize inputs
|
|
const normalizedPattern = pattern.toLowerCase().trim();
|
|
const normalizedHostname = hostname.toLowerCase().trim();
|
|
|
|
// Remove trailing dots (FQDN normalization)
|
|
const cleanPattern = normalizedPattern.replace(/\.$/, '');
|
|
const cleanHostname = normalizedHostname.replace(/\.$/, '');
|
|
|
|
// Exact match (most common case)
|
|
if (cleanPattern === cleanHostname) {
|
|
return true;
|
|
}
|
|
|
|
// Wildcard matching
|
|
if (options.allowWildcards !== false && cleanPattern.includes('*')) {
|
|
const regex = this.wildcardToRegex(cleanPattern);
|
|
return regex.test(cleanHostname);
|
|
}
|
|
|
|
// No match
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a pattern contains wildcards
|
|
*/
|
|
static isWildcardPattern(pattern: string): boolean {
|
|
return pattern.includes('*');
|
|
}
|
|
|
|
/**
|
|
* Calculate the specificity of a domain pattern
|
|
* Higher values mean more specific patterns
|
|
*/
|
|
static calculateSpecificity(pattern: string): number {
|
|
if (!pattern) return 0;
|
|
|
|
let score = 0;
|
|
|
|
// Exact domains are most specific
|
|
if (!pattern.includes('*')) {
|
|
score += 100;
|
|
}
|
|
|
|
// Count domain segments
|
|
const segments = pattern.split('.');
|
|
score += segments.length * 10;
|
|
|
|
// Penalize wildcards based on position
|
|
if (pattern.startsWith('*')) {
|
|
score -= 50; // Leading wildcard is very generic
|
|
} else if (pattern.includes('*')) {
|
|
score -= 20; // Wildcard elsewhere is less generic
|
|
}
|
|
|
|
// Bonus for longer patterns
|
|
score += pattern.length;
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Find all matching patterns from a list
|
|
* Returns patterns sorted by specificity (most specific first)
|
|
*/
|
|
static findAllMatches(
|
|
patterns: string[],
|
|
hostname: string,
|
|
options: IDomainMatchOptions = {}
|
|
): string[] {
|
|
const matches = patterns.filter(pattern =>
|
|
this.match(pattern, hostname, options)
|
|
);
|
|
|
|
// Sort by specificity (highest first)
|
|
return matches.sort((a, b) =>
|
|
this.calculateSpecificity(b) - this.calculateSpecificity(a)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Instance method for interface compliance
|
|
*/
|
|
match(pattern: string, hostname: string, options?: IDomainMatchOptions): boolean {
|
|
return DomainMatcher.match(pattern, hostname, options);
|
|
}
|
|
} |