- 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.
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
|
import type { IRouteSpecificity } from './types.js';
|
|
import { DomainMatcher, PathMatcher, IpMatcher, HeaderMatcher } from './matchers/index.js';
|
|
|
|
/**
|
|
* Unified route specificity calculator
|
|
* Provides consistent specificity scoring across all routing components
|
|
*/
|
|
export class RouteSpecificity {
|
|
/**
|
|
* Calculate the total specificity score for a route
|
|
* Higher scores indicate more specific routes that should match first
|
|
*/
|
|
static calculate(route: IRouteConfig): IRouteSpecificity {
|
|
const specificity: IRouteSpecificity = {
|
|
pathSpecificity: 0,
|
|
domainSpecificity: 0,
|
|
ipSpecificity: 0,
|
|
headerSpecificity: 0,
|
|
tlsSpecificity: 0,
|
|
totalScore: 0
|
|
};
|
|
|
|
// Path specificity
|
|
if (route.match.path) {
|
|
specificity.pathSpecificity = PathMatcher.calculateSpecificity(route.match.path);
|
|
}
|
|
|
|
// Domain specificity
|
|
if (route.match.domains) {
|
|
const domains = Array.isArray(route.match.domains)
|
|
? route.match.domains
|
|
: [route.match.domains];
|
|
|
|
// Use the highest specificity among all domains
|
|
specificity.domainSpecificity = Math.max(
|
|
...domains.map(d => DomainMatcher.calculateSpecificity(d))
|
|
);
|
|
}
|
|
|
|
// IP specificity (clientIp is an array of IPs)
|
|
if (route.match.clientIp && route.match.clientIp.length > 0) {
|
|
// Use the first IP pattern for specificity calculation
|
|
specificity.ipSpecificity = IpMatcher.calculateSpecificity(route.match.clientIp[0]);
|
|
}
|
|
|
|
// Header specificity (convert RegExp values to strings)
|
|
if (route.match.headers) {
|
|
const stringHeaders: Record<string, string> = {};
|
|
for (const [key, value] of Object.entries(route.match.headers)) {
|
|
stringHeaders[key] = value instanceof RegExp ? value.source : value;
|
|
}
|
|
specificity.headerSpecificity = HeaderMatcher.calculateSpecificity(stringHeaders);
|
|
}
|
|
|
|
// TLS version specificity
|
|
if (route.match.tlsVersion && route.match.tlsVersion.length > 0) {
|
|
specificity.tlsSpecificity = route.match.tlsVersion.length * 10;
|
|
}
|
|
|
|
// Calculate total score with weights
|
|
specificity.totalScore =
|
|
specificity.pathSpecificity * 3 + // Path is most important
|
|
specificity.domainSpecificity * 2 + // Domain is second
|
|
specificity.ipSpecificity * 1.5 + // IP is moderately important
|
|
specificity.headerSpecificity * 1 + // Headers are less important
|
|
specificity.tlsSpecificity * 0.5; // TLS is least important
|
|
|
|
return specificity;
|
|
}
|
|
|
|
/**
|
|
* Compare two routes and determine which is more specific
|
|
* @returns positive if route1 is more specific, negative if route2 is more specific, 0 if equal
|
|
*/
|
|
static compare(route1: IRouteConfig, route2: IRouteConfig): number {
|
|
const spec1 = this.calculate(route1);
|
|
const spec2 = this.calculate(route2);
|
|
|
|
// First compare by total score
|
|
if (spec1.totalScore !== spec2.totalScore) {
|
|
return spec1.totalScore - spec2.totalScore;
|
|
}
|
|
|
|
// If total scores are equal, compare by individual components
|
|
// Path is most important tiebreaker
|
|
if (spec1.pathSpecificity !== spec2.pathSpecificity) {
|
|
return spec1.pathSpecificity - spec2.pathSpecificity;
|
|
}
|
|
|
|
// Then domain
|
|
if (spec1.domainSpecificity !== spec2.domainSpecificity) {
|
|
return spec1.domainSpecificity - spec2.domainSpecificity;
|
|
}
|
|
|
|
// Then IP
|
|
if (spec1.ipSpecificity !== spec2.ipSpecificity) {
|
|
return spec1.ipSpecificity - spec2.ipSpecificity;
|
|
}
|
|
|
|
// Then headers
|
|
if (spec1.headerSpecificity !== spec2.headerSpecificity) {
|
|
return spec1.headerSpecificity - spec2.headerSpecificity;
|
|
}
|
|
|
|
// Finally TLS
|
|
return spec1.tlsSpecificity - spec2.tlsSpecificity;
|
|
}
|
|
|
|
/**
|
|
* Sort routes by specificity (most specific first)
|
|
*/
|
|
static sort(routes: IRouteConfig[]): IRouteConfig[] {
|
|
return [...routes].sort((a, b) => this.compare(b, a));
|
|
}
|
|
|
|
/**
|
|
* Find the most specific route from a list
|
|
*/
|
|
static findMostSpecific(routes: IRouteConfig[]): IRouteConfig | null {
|
|
if (routes.length === 0) return null;
|
|
|
|
return routes.reduce((most, current) =>
|
|
this.compare(current, most) > 0 ? current : most
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a route has any matching criteria
|
|
*/
|
|
static hasMatchCriteria(route: IRouteConfig): boolean {
|
|
const match = route.match;
|
|
return !!(
|
|
match.domains ||
|
|
match.path ||
|
|
match.clientIp?.length ||
|
|
match.headers ||
|
|
match.tlsVersion?.length
|
|
);
|
|
}
|
|
} |