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