- 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.
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import type { IMatcher, IHeaderMatchOptions } from '../types.js';
|
|
|
|
/**
|
|
* HeaderMatcher provides HTTP header matching functionality
|
|
* Supporting exact matches, patterns, and case-insensitive matching
|
|
*/
|
|
export class HeaderMatcher implements IMatcher<boolean, IHeaderMatchOptions> {
|
|
/**
|
|
* Match a header value against a pattern
|
|
* @param pattern The pattern to match
|
|
* @param value The header value to test
|
|
* @param options Matching options
|
|
* @returns true if the value matches the pattern
|
|
*/
|
|
static match(
|
|
pattern: string,
|
|
value: string | undefined,
|
|
options: IHeaderMatchOptions = {}
|
|
): boolean {
|
|
// Handle missing header
|
|
if (value === undefined || value === null) {
|
|
return pattern === '' || pattern === null || pattern === undefined;
|
|
}
|
|
|
|
// Convert to string and normalize
|
|
const normalizedPattern = String(pattern);
|
|
const normalizedValue = String(value);
|
|
|
|
// Apply case sensitivity
|
|
const comparePattern = options.caseInsensitive !== false
|
|
? normalizedPattern.toLowerCase()
|
|
: normalizedPattern;
|
|
const compareValue = options.caseInsensitive !== false
|
|
? normalizedValue.toLowerCase()
|
|
: normalizedValue;
|
|
|
|
// Exact match
|
|
if (options.exactMatch !== false) {
|
|
return comparePattern === compareValue;
|
|
}
|
|
|
|
// Pattern matching (simple wildcard support)
|
|
if (comparePattern.includes('*')) {
|
|
const regex = new RegExp(
|
|
'^' + comparePattern.replace(/\*/g, '.*') + '$',
|
|
options.caseInsensitive !== false ? 'i' : ''
|
|
);
|
|
return regex.test(normalizedValue);
|
|
}
|
|
|
|
// Contains match (if not exact match mode)
|
|
return compareValue.includes(comparePattern);
|
|
}
|
|
|
|
/**
|
|
* Match multiple headers against a set of required headers
|
|
* @param requiredHeaders Headers that must match
|
|
* @param actualHeaders Actual request headers
|
|
* @param options Matching options
|
|
* @returns true if all required headers match
|
|
*/
|
|
static matchAll(
|
|
requiredHeaders: Record<string, string>,
|
|
actualHeaders: Record<string, string | string[] | undefined>,
|
|
options: IHeaderMatchOptions = {}
|
|
): boolean {
|
|
for (const [name, pattern] of Object.entries(requiredHeaders)) {
|
|
const headerName = options.caseInsensitive !== false
|
|
? name.toLowerCase()
|
|
: name;
|
|
|
|
// Find the actual header (case-insensitive search if needed)
|
|
let actualValue: string | undefined;
|
|
if (options.caseInsensitive !== false) {
|
|
const actualKey = Object.keys(actualHeaders).find(
|
|
key => key.toLowerCase() === headerName
|
|
);
|
|
const rawValue = actualKey ? actualHeaders[actualKey] : undefined;
|
|
// Handle array values (multiple headers with same name)
|
|
actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue;
|
|
} else {
|
|
const rawValue = actualHeaders[name];
|
|
// Handle array values (multiple headers with same name)
|
|
actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue;
|
|
}
|
|
|
|
// Check if this header matches
|
|
if (!this.match(pattern, actualValue, options)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Calculate the specificity of header requirements
|
|
* More headers = more specific
|
|
*/
|
|
static calculateSpecificity(headers: Record<string, string>): number {
|
|
const count = Object.keys(headers).length;
|
|
let score = count * 10;
|
|
|
|
// Bonus for headers without wildcards (more specific)
|
|
for (const value of Object.values(headers)) {
|
|
if (!value.includes('*')) {
|
|
score += 5;
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Instance method for interface compliance
|
|
*/
|
|
match(pattern: string, value: string, options?: IHeaderMatchOptions): boolean {
|
|
return HeaderMatcher.match(pattern, value, options);
|
|
}
|
|
} |