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