120 lines
3.8 KiB
TypeScript
Raw Normal View History

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