2025-05-10 00:01:02 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
import type {
|
|
|
|
IRouteConfig,
|
|
|
|
IRouteMatch,
|
|
|
|
IRouteAction,
|
|
|
|
TPortRange
|
|
|
|
} from './models/route-types.js';
|
|
|
|
import type {
|
|
|
|
ISmartProxyOptions,
|
2025-05-10 07:34:35 +00:00
|
|
|
IRoutedSmartProxyOptions
|
2025-05-10 00:01:02 +00:00
|
|
|
} from './models/interfaces.js';
|
|
|
|
import {
|
|
|
|
isRoutedOptions,
|
|
|
|
isLegacyOptions
|
|
|
|
} from './models/interfaces.js';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Result of route matching
|
|
|
|
*/
|
|
|
|
export interface IRouteMatchResult {
|
|
|
|
route: IRouteConfig;
|
|
|
|
// Additional match parameters (path, query, etc.)
|
|
|
|
params?: Record<string, string>;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The RouteManager handles all routing decisions based on connections and attributes
|
|
|
|
*/
|
|
|
|
export class RouteManager extends plugins.EventEmitter {
|
|
|
|
private routes: IRouteConfig[] = [];
|
|
|
|
private portMap: Map<number, IRouteConfig[]> = new Map();
|
|
|
|
private options: IRoutedSmartProxyOptions;
|
|
|
|
|
|
|
|
constructor(options: ISmartProxyOptions) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
// We no longer support legacy options, always use provided options
|
|
|
|
this.options = options;
|
|
|
|
|
|
|
|
// Initialize routes from either source
|
|
|
|
this.updateRoutes(this.options.routes);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update routes with new configuration
|
|
|
|
*/
|
|
|
|
public updateRoutes(routes: IRouteConfig[] = []): void {
|
|
|
|
// Sort routes by priority (higher first)
|
|
|
|
this.routes = [...(routes || [])].sort((a, b) => {
|
|
|
|
const priorityA = a.priority ?? 0;
|
|
|
|
const priorityB = b.priority ?? 0;
|
|
|
|
return priorityB - priorityA;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Rebuild port mapping for fast lookups
|
|
|
|
this.rebuildPortMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rebuild the port mapping for fast lookups
|
|
|
|
*/
|
|
|
|
private rebuildPortMap(): void {
|
|
|
|
this.portMap.clear();
|
|
|
|
|
|
|
|
for (const route of this.routes) {
|
|
|
|
const ports = this.expandPortRange(route.match.ports);
|
|
|
|
|
|
|
|
for (const port of ports) {
|
|
|
|
if (!this.portMap.has(port)) {
|
|
|
|
this.portMap.set(port, []);
|
|
|
|
}
|
|
|
|
this.portMap.get(port)!.push(route);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Expand a port range specification into an array of individual ports
|
|
|
|
*/
|
|
|
|
private expandPortRange(portRange: TPortRange): number[] {
|
|
|
|
if (typeof portRange === 'number') {
|
|
|
|
return [portRange];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(portRange)) {
|
|
|
|
// Handle array of port objects or numbers
|
|
|
|
return portRange.flatMap(item => {
|
|
|
|
if (typeof item === 'number') {
|
|
|
|
return [item];
|
|
|
|
} else if (typeof item === 'object' && 'from' in item && 'to' in item) {
|
|
|
|
// Handle port range object
|
|
|
|
const ports: number[] = [];
|
|
|
|
for (let p = item.from; p <= item.to; p++) {
|
|
|
|
ports.push(p);
|
|
|
|
}
|
|
|
|
return ports;
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all ports that should be listened on
|
|
|
|
*/
|
|
|
|
public getListeningPorts(): number[] {
|
|
|
|
return Array.from(this.portMap.keys());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all routes for a given port
|
|
|
|
*/
|
|
|
|
public getRoutesForPort(port: number): IRouteConfig[] {
|
|
|
|
return this.portMap.get(port) || [];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test if a pattern matches a domain using glob matching
|
|
|
|
*/
|
|
|
|
private matchDomain(pattern: string, domain: string): boolean {
|
|
|
|
// Convert glob pattern to regex
|
|
|
|
const regexPattern = pattern
|
|
|
|
.replace(/\./g, '\\.') // Escape dots
|
|
|
|
.replace(/\*/g, '.*'); // Convert * to .*
|
|
|
|
|
|
|
|
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
|
|
return regex.test(domain);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Match a domain against all patterns in a route
|
|
|
|
*/
|
|
|
|
private matchRouteDomain(route: IRouteConfig, domain: string): boolean {
|
|
|
|
if (!route.match.domains) {
|
|
|
|
// If no domains specified, match all domains
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const patterns = Array.isArray(route.match.domains)
|
|
|
|
? route.match.domains
|
|
|
|
: [route.match.domains];
|
|
|
|
|
|
|
|
return patterns.some(pattern => this.matchDomain(pattern, domain));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a client IP is allowed by a route's security settings
|
|
|
|
*/
|
|
|
|
private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean {
|
|
|
|
const security = route.action.security;
|
|
|
|
|
|
|
|
if (!security) {
|
|
|
|
return true; // No security settings means allowed
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check blocked IPs first
|
|
|
|
if (security.blockedIps && security.blockedIps.length > 0) {
|
|
|
|
for (const pattern of security.blockedIps) {
|
|
|
|
if (this.matchIpPattern(pattern, clientIp)) {
|
|
|
|
return false; // IP is blocked
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there are allowed IPs, check them
|
|
|
|
if (security.allowedIps && security.allowedIps.length > 0) {
|
|
|
|
for (const pattern of security.allowedIps) {
|
|
|
|
if (this.matchIpPattern(pattern, clientIp)) {
|
|
|
|
return true; // IP is allowed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false; // IP not in allowed list
|
|
|
|
}
|
|
|
|
|
|
|
|
// No allowed IPs specified, so IP is allowed
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Match an IP against a pattern
|
|
|
|
*/
|
|
|
|
private matchIpPattern(pattern: string, ip: string): boolean {
|
|
|
|
// Handle exact match
|
|
|
|
if (pattern === ip) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
|
|
|
if (pattern.includes('/')) {
|
|
|
|
return this.matchIpCidr(pattern, ip);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle glob pattern (e.g., 192.168.1.*)
|
|
|
|
if (pattern.includes('*')) {
|
|
|
|
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
|
|
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
|
|
return regex.test(ip);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Match an IP against a CIDR pattern
|
|
|
|
*/
|
|
|
|
private matchIpCidr(cidr: string, ip: string): boolean {
|
|
|
|
try {
|
|
|
|
// In a real implementation, you'd use a proper IP library
|
|
|
|
// This is a simplified implementation
|
|
|
|
const [subnet, bits] = cidr.split('/');
|
|
|
|
const mask = parseInt(bits, 10);
|
|
|
|
|
|
|
|
// Convert IP addresses to numeric values
|
|
|
|
const ipNum = this.ipToNumber(ip);
|
|
|
|
const subnetNum = this.ipToNumber(subnet);
|
|
|
|
|
|
|
|
// Calculate subnet mask
|
|
|
|
const maskNum = ~(2 ** (32 - mask) - 1);
|
|
|
|
|
|
|
|
// Check if IP is in subnet
|
|
|
|
return (ipNum & maskNum) === (subnetNum & maskNum);
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert an IP address to a numeric value
|
|
|
|
*/
|
|
|
|
private ipToNumber(ip: string): number {
|
|
|
|
const parts = ip.split('.').map(part => parseInt(part, 10));
|
|
|
|
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find the matching route for a connection
|
|
|
|
*/
|
|
|
|
public findMatchingRoute(options: {
|
|
|
|
port: number;
|
|
|
|
domain?: string;
|
|
|
|
clientIp: string;
|
|
|
|
path?: string;
|
|
|
|
tlsVersion?: string;
|
|
|
|
}): IRouteMatchResult | null {
|
|
|
|
const { port, domain, clientIp, path, tlsVersion } = options;
|
|
|
|
|
|
|
|
// Get all routes for this port
|
|
|
|
const routesForPort = this.getRoutesForPort(port);
|
|
|
|
|
|
|
|
// Find the first matching route based on priority order
|
|
|
|
for (const route of routesForPort) {
|
|
|
|
// Check domain match if specified
|
|
|
|
if (domain && !this.matchRouteDomain(route, domain)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check path match if specified in both route and request
|
|
|
|
if (path && route.match.path) {
|
|
|
|
if (!this.matchPath(route.match.path, path)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check client IP match
|
|
|
|
if (route.match.clientIp && !route.match.clientIp.some(pattern =>
|
|
|
|
this.matchIpPattern(pattern, clientIp))) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check TLS version match
|
|
|
|
if (tlsVersion && route.match.tlsVersion &&
|
|
|
|
!route.match.tlsVersion.includes(tlsVersion)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check security settings
|
|
|
|
if (!this.isClientIpAllowed(route, clientIp)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// All checks passed, this route matches
|
|
|
|
return { route };
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Match a path against a pattern
|
|
|
|
*/
|
|
|
|
private matchPath(pattern: string, path: string): boolean {
|
|
|
|
// Convert the glob pattern to a regex
|
|
|
|
const regexPattern = pattern
|
|
|
|
.replace(/\./g, '\\.') // Escape dots
|
|
|
|
.replace(/\*/g, '.*') // Convert * to .*
|
|
|
|
.replace(/\//g, '\\/'); // Escape slashes
|
|
|
|
|
|
|
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
|
|
return regex.test(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert a domain config to routes
|
|
|
|
* (For backward compatibility with code that still uses domainConfigs)
|
|
|
|
*/
|
|
|
|
public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] {
|
|
|
|
const routes: IRouteConfig[] = [];
|
|
|
|
const { domains, forwarding } = domainConfig;
|
|
|
|
|
|
|
|
// Determine the action based on forwarding type
|
|
|
|
let action: IRouteAction = {
|
|
|
|
type: 'forward',
|
|
|
|
target: {
|
|
|
|
host: forwarding.target.host,
|
|
|
|
port: forwarding.target.port
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Set TLS mode based on forwarding type
|
|
|
|
switch (forwarding.type) {
|
|
|
|
case 'http-only':
|
|
|
|
// No TLS settings needed
|
|
|
|
break;
|
|
|
|
case 'https-passthrough':
|
|
|
|
action.tls = { mode: 'passthrough' };
|
|
|
|
break;
|
|
|
|
case 'https-terminate-to-http':
|
|
|
|
action.tls = {
|
|
|
|
mode: 'terminate',
|
|
|
|
certificate: forwarding.https?.customCert ? {
|
|
|
|
key: forwarding.https.customCert.key,
|
|
|
|
cert: forwarding.https.customCert.cert
|
|
|
|
} : 'auto'
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'https-terminate-to-https':
|
|
|
|
action.tls = {
|
|
|
|
mode: 'terminate-and-reencrypt',
|
|
|
|
certificate: forwarding.https?.customCert ? {
|
|
|
|
key: forwarding.https.customCert.key,
|
|
|
|
cert: forwarding.https.customCert.cert
|
|
|
|
} : 'auto'
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add security settings if present
|
|
|
|
if (forwarding.security) {
|
|
|
|
action.security = {
|
|
|
|
allowedIps: forwarding.security.allowedIps,
|
|
|
|
blockedIps: forwarding.security.blockedIps,
|
|
|
|
maxConnections: forwarding.security.maxConnections
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add advanced settings if present
|
|
|
|
if (forwarding.advanced) {
|
|
|
|
action.advanced = {
|
|
|
|
timeout: forwarding.advanced.timeout,
|
|
|
|
headers: forwarding.advanced.headers,
|
|
|
|
keepAlive: forwarding.advanced.keepAlive
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Determine which port to use based on forwarding type
|
|
|
|
const defaultPort = forwarding.type.startsWith('https') ? 443 : 80;
|
|
|
|
|
|
|
|
// Add the main route
|
|
|
|
routes.push({
|
|
|
|
match: {
|
|
|
|
ports: defaultPort,
|
|
|
|
domains
|
|
|
|
},
|
|
|
|
action,
|
|
|
|
name: `Route for ${domains.join(', ')}`
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add HTTP redirect if needed
|
|
|
|
if (forwarding.http?.redirectToHttps) {
|
|
|
|
routes.push({
|
|
|
|
match: {
|
|
|
|
ports: 80,
|
|
|
|
domains
|
|
|
|
},
|
|
|
|
action: {
|
|
|
|
type: 'redirect',
|
|
|
|
redirect: {
|
|
|
|
to: 'https://{domain}{path}',
|
|
|
|
status: 301
|
|
|
|
}
|
|
|
|
},
|
|
|
|
name: `HTTP Redirect for ${domains.join(', ')}`,
|
|
|
|
priority: 100 // Higher priority for redirects
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add port ranges if specified
|
|
|
|
if (forwarding.advanced?.portRanges) {
|
|
|
|
for (const range of forwarding.advanced.portRanges) {
|
|
|
|
routes.push({
|
|
|
|
match: {
|
|
|
|
ports: [{ from: range.from, to: range.to }],
|
|
|
|
domains
|
|
|
|
},
|
|
|
|
action,
|
|
|
|
name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}`
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return routes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update routes based on domain configs
|
|
|
|
* (For backward compatibility with code that still uses domainConfigs)
|
|
|
|
*/
|
|
|
|
public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void {
|
|
|
|
const routes: IRouteConfig[] = [];
|
|
|
|
|
|
|
|
// Convert each domain config to routes
|
|
|
|
for (const config of domainConfigs) {
|
|
|
|
routes.push(...this.domainConfigToRoutes(config));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Merge with existing routes that aren't derived from domain configs
|
|
|
|
const nonDomainRoutes = this.routes.filter(r =>
|
|
|
|
!r.name || !r.name.includes('for '));
|
|
|
|
|
|
|
|
this.updateRoutes([...nonDomainRoutes, ...routes]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate the route configuration and return any warnings
|
|
|
|
*/
|
|
|
|
public validateConfiguration(): string[] {
|
|
|
|
const warnings: string[] = [];
|
|
|
|
const duplicatePorts = new Map<number, number>();
|
|
|
|
|
|
|
|
// Check for routes with the same exact match criteria
|
|
|
|
for (let i = 0; i < this.routes.length; i++) {
|
|
|
|
for (let j = i + 1; j < this.routes.length; j++) {
|
|
|
|
const route1 = this.routes[i];
|
|
|
|
const route2 = this.routes[j];
|
|
|
|
|
|
|
|
// Check if route match criteria are the same
|
|
|
|
if (this.areMatchesSimilar(route1.match, route2.match)) {
|
|
|
|
warnings.push(
|
|
|
|
`Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` +
|
|
|
|
`The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for routes that may never be matched due to priority
|
|
|
|
for (let i = 0; i < this.routes.length; i++) {
|
|
|
|
const route = this.routes[i];
|
|
|
|
const higherPriorityRoutes = this.routes.filter(r =>
|
|
|
|
(r.priority || 0) > (route.priority || 0));
|
|
|
|
|
|
|
|
for (const higherRoute of higherPriorityRoutes) {
|
|
|
|
if (this.isRouteShadowed(route, higherRoute)) {
|
|
|
|
warnings.push(
|
|
|
|
`Route "${route.name || i}" may never be matched because it is shadowed by ` +
|
|
|
|
`higher priority route "${higherRoute.name || 'unnamed'}"`
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return warnings;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if two route matches are similar (potential conflict)
|
|
|
|
*/
|
|
|
|
private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean {
|
|
|
|
// Check port overlap
|
|
|
|
const ports1 = new Set(this.expandPortRange(match1.ports));
|
|
|
|
const ports2 = new Set(this.expandPortRange(match2.ports));
|
|
|
|
|
|
|
|
let havePortOverlap = false;
|
|
|
|
for (const port of ports1) {
|
|
|
|
if (ports2.has(port)) {
|
|
|
|
havePortOverlap = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!havePortOverlap) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check domain overlap
|
|
|
|
if (match1.domains && match2.domains) {
|
|
|
|
const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains];
|
|
|
|
const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains];
|
|
|
|
|
|
|
|
// Check if any domain pattern from match1 could match any from match2
|
|
|
|
let haveDomainOverlap = false;
|
|
|
|
for (const domain1 of domains1) {
|
|
|
|
for (const domain2 of domains2) {
|
|
|
|
if (domain1 === domain2 ||
|
|
|
|
(domain1.includes('*') || domain2.includes('*'))) {
|
|
|
|
haveDomainOverlap = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (haveDomainOverlap) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!haveDomainOverlap) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else if (match1.domains || match2.domains) {
|
|
|
|
// One has domains, the other doesn't - they could overlap
|
|
|
|
// The one with domains is more specific, so it's not exactly a conflict
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check path overlap
|
|
|
|
if (match1.path && match2.path) {
|
|
|
|
// This is a simplified check - in a real implementation,
|
|
|
|
// you'd need to check if the path patterns could match the same paths
|
|
|
|
return match1.path === match2.path ||
|
|
|
|
match1.path.includes('*') ||
|
|
|
|
match2.path.includes('*');
|
|
|
|
} else if (match1.path || match2.path) {
|
|
|
|
// One has a path, the other doesn't
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we get here, the matches have significant overlap
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a route is completely shadowed by a higher priority route
|
|
|
|
*/
|
|
|
|
private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean {
|
|
|
|
// If they don't have similar match criteria, no shadowing occurs
|
|
|
|
if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If higher priority route has more specific criteria, no shadowing
|
|
|
|
if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If higher priority route is equally or less specific but has higher priority,
|
|
|
|
// it shadows the lower priority route
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if route1 is more specific than route2
|
|
|
|
*/
|
|
|
|
private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean {
|
|
|
|
// Check if match1 has more specific criteria
|
|
|
|
let match1Points = 0;
|
|
|
|
let match2Points = 0;
|
|
|
|
|
|
|
|
// Path is the most specific
|
|
|
|
if (match1.path) match1Points += 3;
|
|
|
|
if (match2.path) match2Points += 3;
|
|
|
|
|
|
|
|
// Domain is next most specific
|
|
|
|
if (match1.domains) match1Points += 2;
|
|
|
|
if (match2.domains) match2Points += 2;
|
|
|
|
|
|
|
|
// Client IP and TLS version are least specific
|
|
|
|
if (match1.clientIp) match1Points += 1;
|
|
|
|
if (match2.clientIp) match2Points += 1;
|
|
|
|
|
|
|
|
if (match1.tlsVersion) match1Points += 1;
|
|
|
|
if (match2.tlsVersion) match2Points += 1;
|
|
|
|
|
|
|
|
return match1Points > match2Points;
|
|
|
|
}
|
|
|
|
}
|