119 lines
3.3 KiB
TypeScript
Raw Normal View History

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