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.
This commit is contained in:
120
ts/core/routing/matchers/header.ts
Normal file
120
ts/core/routing/matchers/header.ts
Normal file
@ -0,0 +1,120 @@
|
||||
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user