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:
17
ts/core/routing/index.ts
Normal file
17
ts/core/routing/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Unified routing module
|
||||
* Provides all routing functionality in a centralized location
|
||||
*/
|
||||
|
||||
// Export all types
|
||||
export * from './types.js';
|
||||
|
||||
// Export all matchers
|
||||
export * from './matchers/index.js';
|
||||
|
||||
// Export specificity calculator
|
||||
export * from './specificity.js';
|
||||
|
||||
// Convenience re-exports
|
||||
export { matchers } from './matchers/index.js';
|
||||
export { RouteSpecificity } from './specificity.js';
|
119
ts/core/routing/matchers/domain.ts
Normal file
119
ts/core/routing/matchers/domain.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type { IMatcher, IDomainMatchOptions } from '../types.js';
|
||||
|
||||
/**
|
||||
* DomainMatcher provides comprehensive domain matching functionality
|
||||
* Supporting exact matches, wildcards, and case-insensitive matching
|
||||
*/
|
||||
export class DomainMatcher implements IMatcher<boolean, IDomainMatchOptions> {
|
||||
private static wildcardToRegex(pattern: string): RegExp {
|
||||
// Escape special regex characters except *
|
||||
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Replace * with regex equivalent
|
||||
const regexPattern = escaped.replace(/\*/g, '.*');
|
||||
return new RegExp(`^${regexPattern}$`, 'i');
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a domain pattern against a hostname
|
||||
* @param pattern The pattern to match (supports wildcards like *.example.com)
|
||||
* @param hostname The hostname to test
|
||||
* @param options Matching options
|
||||
* @returns true if the hostname matches the pattern
|
||||
*/
|
||||
static match(
|
||||
pattern: string,
|
||||
hostname: string,
|
||||
options: IDomainMatchOptions = {}
|
||||
): boolean {
|
||||
// Handle null/undefined cases
|
||||
if (!pattern || !hostname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize inputs
|
||||
const normalizedPattern = pattern.toLowerCase().trim();
|
||||
const normalizedHostname = hostname.toLowerCase().trim();
|
||||
|
||||
// Remove trailing dots (FQDN normalization)
|
||||
const cleanPattern = normalizedPattern.replace(/\.$/, '');
|
||||
const cleanHostname = normalizedHostname.replace(/\.$/, '');
|
||||
|
||||
// Exact match (most common case)
|
||||
if (cleanPattern === cleanHostname) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard matching
|
||||
if (options.allowWildcards !== false && cleanPattern.includes('*')) {
|
||||
const regex = this.wildcardToRegex(cleanPattern);
|
||||
return regex.test(cleanHostname);
|
||||
}
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a pattern contains wildcards
|
||||
*/
|
||||
static isWildcardPattern(pattern: string): boolean {
|
||||
return pattern.includes('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the specificity of a domain pattern
|
||||
* Higher values mean more specific patterns
|
||||
*/
|
||||
static calculateSpecificity(pattern: string): number {
|
||||
if (!pattern) return 0;
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Exact domains are most specific
|
||||
if (!pattern.includes('*')) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
// Count domain segments
|
||||
const segments = pattern.split('.');
|
||||
score += segments.length * 10;
|
||||
|
||||
// Penalize wildcards based on position
|
||||
if (pattern.startsWith('*')) {
|
||||
score -= 50; // Leading wildcard is very generic
|
||||
} else if (pattern.includes('*')) {
|
||||
score -= 20; // Wildcard elsewhere is less generic
|
||||
}
|
||||
|
||||
// Bonus for longer patterns
|
||||
score += pattern.length;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all matching patterns from a list
|
||||
* Returns patterns sorted by specificity (most specific first)
|
||||
*/
|
||||
static findAllMatches(
|
||||
patterns: string[],
|
||||
hostname: string,
|
||||
options: IDomainMatchOptions = {}
|
||||
): string[] {
|
||||
const matches = patterns.filter(pattern =>
|
||||
this.match(pattern, hostname, options)
|
||||
);
|
||||
|
||||
// Sort by specificity (highest first)
|
||||
return matches.sort((a, b) =>
|
||||
this.calculateSpecificity(b) - this.calculateSpecificity(a)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance method for interface compliance
|
||||
*/
|
||||
match(pattern: string, hostname: string, options?: IDomainMatchOptions): boolean {
|
||||
return DomainMatcher.match(pattern, hostname, options);
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
22
ts/core/routing/matchers/index.ts
Normal file
22
ts/core/routing/matchers/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Unified matching utilities for the routing system
|
||||
* All route matching logic should use these matchers for consistency
|
||||
*/
|
||||
|
||||
export * from './domain.js';
|
||||
export * from './path.js';
|
||||
export * from './ip.js';
|
||||
export * from './header.js';
|
||||
|
||||
// Re-export for convenience
|
||||
import { DomainMatcher } from './domain.js';
|
||||
import { PathMatcher } from './path.js';
|
||||
import { IpMatcher } from './ip.js';
|
||||
import { HeaderMatcher } from './header.js';
|
||||
|
||||
export const matchers = {
|
||||
domain: DomainMatcher,
|
||||
path: PathMatcher,
|
||||
ip: IpMatcher,
|
||||
header: HeaderMatcher
|
||||
} as const;
|
207
ts/core/routing/matchers/ip.ts
Normal file
207
ts/core/routing/matchers/ip.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import type { IMatcher, IIpMatchOptions } from '../types.js';
|
||||
|
||||
/**
|
||||
* IpMatcher provides comprehensive IP address matching functionality
|
||||
* Supporting exact matches, CIDR notation, ranges, and wildcards
|
||||
*/
|
||||
export class IpMatcher implements IMatcher<boolean, IIpMatchOptions> {
|
||||
/**
|
||||
* Check if a value is a valid IPv4 address
|
||||
*/
|
||||
static isValidIpv4(ip: string): boolean {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return false;
|
||||
|
||||
return parts.every(part => {
|
||||
const num = parseInt(part, 10);
|
||||
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a valid IPv6 address (simplified check)
|
||||
*/
|
||||
static isValidIpv6(ip: string): boolean {
|
||||
// Basic IPv6 validation - can be enhanced
|
||||
const ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|(([0-9a-fA-F]{1,4}:){1,7}|:):|(([0-9a-fA-F]{1,4}:){1,6}|::):[0-9a-fA-F]{1,4})$/;
|
||||
return ipv6Regex.test(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert IP address to numeric value for comparison
|
||||
*/
|
||||
private static ipToNumber(ip: string): number {
|
||||
const parts = ip.split('.');
|
||||
return parts.reduce((acc, part, index) => {
|
||||
return acc + (parseInt(part, 10) << (8 * (3 - index)));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP against a CIDR notation pattern
|
||||
*/
|
||||
static matchCidr(cidr: string, ip: string): boolean {
|
||||
const [range, bits] = cidr.split('/');
|
||||
if (!bits || !this.isValidIpv4(range) || !this.isValidIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rangeMask = parseInt(bits, 10);
|
||||
if (isNaN(rangeMask) || rangeMask < 0 || rangeMask > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rangeNum = this.ipToNumber(range);
|
||||
const ipNum = this.ipToNumber(ip);
|
||||
const mask = (-1 << (32 - rangeMask)) >>> 0;
|
||||
|
||||
return (rangeNum & mask) === (ipNum & mask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP against a wildcard pattern
|
||||
*/
|
||||
static matchWildcard(pattern: string, ip: string): boolean {
|
||||
if (!this.isValidIpv4(ip)) return false;
|
||||
|
||||
const patternParts = pattern.split('.');
|
||||
const ipParts = ip.split('.');
|
||||
|
||||
if (patternParts.length !== 4) return false;
|
||||
|
||||
return patternParts.every((part, index) => {
|
||||
if (part === '*') return true;
|
||||
return part === ipParts[index];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP against a range (e.g., "192.168.1.1-192.168.1.100")
|
||||
*/
|
||||
static matchRange(range: string, ip: string): boolean {
|
||||
const [start, end] = range.split('-').map(s => s.trim());
|
||||
|
||||
if (!start || !end || !this.isValidIpv4(start) || !this.isValidIpv4(end) || !this.isValidIpv4(ip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startNum = this.ipToNumber(start);
|
||||
const endNum = this.ipToNumber(end);
|
||||
const ipNum = this.ipToNumber(ip);
|
||||
|
||||
return ipNum >= startNum && ipNum <= endNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP pattern against an IP address
|
||||
* Supports multiple formats:
|
||||
* - Exact match: "192.168.1.1"
|
||||
* - CIDR: "192.168.1.0/24"
|
||||
* - Wildcard: "192.168.1.*"
|
||||
* - Range: "192.168.1.1-192.168.1.100"
|
||||
*/
|
||||
static match(
|
||||
pattern: string,
|
||||
ip: string,
|
||||
options: IIpMatchOptions = {}
|
||||
): boolean {
|
||||
// Handle null/undefined cases
|
||||
if (!pattern || !ip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize inputs
|
||||
const normalizedPattern = pattern.trim();
|
||||
const normalizedIp = ip.trim();
|
||||
|
||||
// Extract IPv4 from IPv6-mapped addresses (::ffff:192.168.1.1)
|
||||
const ipv4Match = normalizedIp.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/i);
|
||||
const testIp = ipv4Match ? ipv4Match[1] : normalizedIp;
|
||||
|
||||
// Exact match
|
||||
if (normalizedPattern === testIp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// CIDR notation
|
||||
if (options.allowCidr !== false && normalizedPattern.includes('/')) {
|
||||
return this.matchCidr(normalizedPattern, testIp);
|
||||
}
|
||||
|
||||
// Wildcard matching
|
||||
if (normalizedPattern.includes('*')) {
|
||||
return this.matchWildcard(normalizedPattern, testIp);
|
||||
}
|
||||
|
||||
// Range matching
|
||||
if (options.allowRanges !== false && normalizedPattern.includes('-')) {
|
||||
return this.matchRange(normalizedPattern, testIp);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP is authorized based on allow and block lists
|
||||
*/
|
||||
static isAuthorized(
|
||||
ip: string,
|
||||
allowList: string[] = [],
|
||||
blockList: string[] = []
|
||||
): boolean {
|
||||
// If IP is in block list, deny
|
||||
if (blockList.some(pattern => this.match(pattern, ip))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If allow list is empty, allow all (except blocked)
|
||||
if (allowList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If allow list exists, IP must match
|
||||
return allowList.some(pattern => this.match(pattern, ip));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the specificity of an IP pattern
|
||||
* Higher values mean more specific patterns
|
||||
*/
|
||||
static calculateSpecificity(pattern: string): number {
|
||||
if (!pattern) return 0;
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Exact IPs are most specific
|
||||
if (this.isValidIpv4(pattern) || this.isValidIpv6(pattern)) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
// CIDR notation
|
||||
if (pattern.includes('/')) {
|
||||
const [, bits] = pattern.split('/');
|
||||
const maskBits = parseInt(bits, 10);
|
||||
if (!isNaN(maskBits)) {
|
||||
score += maskBits; // Higher mask = more specific
|
||||
}
|
||||
}
|
||||
|
||||
// Wildcard patterns
|
||||
const wildcards = (pattern.match(/\*/g) || []).length;
|
||||
score -= wildcards * 20; // More wildcards = less specific
|
||||
|
||||
// Range patterns are somewhat specific
|
||||
if (pattern.includes('-')) {
|
||||
score += 30;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance method for interface compliance
|
||||
*/
|
||||
match(pattern: string, ip: string, options?: IIpMatchOptions): boolean {
|
||||
return IpMatcher.match(pattern, ip, options);
|
||||
}
|
||||
}
|
178
ts/core/routing/matchers/path.ts
Normal file
178
ts/core/routing/matchers/path.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import type { IMatcher, IPathMatchResult } from '../types.js';
|
||||
|
||||
/**
|
||||
* PathMatcher provides comprehensive path matching functionality
|
||||
* Supporting exact matches, wildcards, and parameter extraction
|
||||
*/
|
||||
export class PathMatcher implements IMatcher<IPathMatchResult> {
|
||||
/**
|
||||
* Convert a path pattern to a regex and extract parameter names
|
||||
* Supports:
|
||||
* - Exact paths: /api/users
|
||||
* - Wildcards: /api/*
|
||||
* - Parameters: /api/users/:id
|
||||
* - Mixed: /api/users/:id/*
|
||||
*/
|
||||
private static patternToRegex(pattern: string): {
|
||||
regex: RegExp;
|
||||
paramNames: string[]
|
||||
} {
|
||||
const paramNames: string[] = [];
|
||||
let regexPattern = pattern;
|
||||
|
||||
// Escape special regex characters except : and *
|
||||
regexPattern = regexPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Handle path parameters (:param)
|
||||
regexPattern = regexPattern.replace(/:(\w+)/g, (match, paramName) => {
|
||||
paramNames.push(paramName);
|
||||
return '([^/]+)'; // Match any non-slash characters
|
||||
});
|
||||
|
||||
// Handle wildcards
|
||||
regexPattern = regexPattern.replace(/\*/g, '(.*)');
|
||||
|
||||
// Ensure the pattern matches from start
|
||||
regexPattern = `^${regexPattern}`;
|
||||
|
||||
return {
|
||||
regex: new RegExp(regexPattern),
|
||||
paramNames
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Match a path pattern against a request path
|
||||
* @param pattern The pattern to match
|
||||
* @param path The request path to test
|
||||
* @returns Match result with params and remainder
|
||||
*/
|
||||
static match(pattern: string, path: string): IPathMatchResult {
|
||||
// Handle null/undefined cases
|
||||
if (!pattern || !path) {
|
||||
return { matches: false };
|
||||
}
|
||||
|
||||
// Normalize paths (remove trailing slashes unless it's just "/")
|
||||
const normalizedPattern = pattern === '/' ? '/' : pattern.replace(/\/$/, '');
|
||||
const normalizedPath = path === '/' ? '/' : path.replace(/\/$/, '');
|
||||
|
||||
// Exact match (most common case)
|
||||
if (normalizedPattern === normalizedPath) {
|
||||
return {
|
||||
matches: true,
|
||||
pathMatch: normalizedPath,
|
||||
pathRemainder: '',
|
||||
params: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Pattern matching (wildcards and parameters)
|
||||
const { regex, paramNames } = this.patternToRegex(normalizedPattern);
|
||||
const match = normalizedPath.match(regex);
|
||||
|
||||
if (!match) {
|
||||
return { matches: false };
|
||||
}
|
||||
|
||||
// Extract parameters
|
||||
const params: Record<string, string> = {};
|
||||
paramNames.forEach((name, index) => {
|
||||
params[name] = match[index + 1];
|
||||
});
|
||||
|
||||
// Calculate path match and remainder
|
||||
let pathMatch = match[0];
|
||||
let pathRemainder = normalizedPath.substring(pathMatch.length);
|
||||
|
||||
// Handle wildcard captures
|
||||
if (normalizedPattern.includes('*') && match.length > paramNames.length + 1) {
|
||||
const wildcardCapture = match[match.length - 1];
|
||||
if (wildcardCapture) {
|
||||
pathRemainder = wildcardCapture;
|
||||
pathMatch = normalizedPath.substring(0, normalizedPath.length - wildcardCapture.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up path match (remove trailing slash if present)
|
||||
if (pathMatch !== '/' && pathMatch.endsWith('/')) {
|
||||
pathMatch = pathMatch.slice(0, -1);
|
||||
}
|
||||
|
||||
return {
|
||||
matches: true,
|
||||
pathMatch,
|
||||
pathRemainder,
|
||||
params
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a pattern contains parameters or wildcards
|
||||
*/
|
||||
static isDynamicPattern(pattern: string): boolean {
|
||||
return pattern.includes(':') || pattern.includes('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the specificity of a path pattern
|
||||
* Higher values mean more specific patterns
|
||||
*/
|
||||
static calculateSpecificity(pattern: string): number {
|
||||
if (!pattern) return 0;
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Exact paths are most specific
|
||||
if (!this.isDynamicPattern(pattern)) {
|
||||
score += 100;
|
||||
}
|
||||
|
||||
// Count path segments
|
||||
const segments = pattern.split('/').filter(s => s.length > 0);
|
||||
score += segments.length * 10;
|
||||
|
||||
// Count static segments (more static = more specific)
|
||||
const staticSegments = segments.filter(s => !s.startsWith(':') && s !== '*');
|
||||
score += staticSegments.length * 20;
|
||||
|
||||
// Penalize wildcards and parameters
|
||||
const wildcards = (pattern.match(/\*/g) || []).length;
|
||||
const params = (pattern.match(/:/g) || []).length;
|
||||
score -= wildcards * 30; // Wildcards are very generic
|
||||
score -= params * 10; // Parameters are somewhat generic
|
||||
|
||||
// Bonus for longer patterns
|
||||
score += pattern.length;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all matching patterns from a list
|
||||
* Returns patterns sorted by specificity (most specific first)
|
||||
*/
|
||||
static findAllMatches(patterns: string[], path: string): Array<{
|
||||
pattern: string;
|
||||
result: IPathMatchResult;
|
||||
}> {
|
||||
const matches = patterns
|
||||
.map(pattern => ({
|
||||
pattern,
|
||||
result: this.match(pattern, path)
|
||||
}))
|
||||
.filter(({ result }) => result.matches);
|
||||
|
||||
// Sort by specificity (highest first)
|
||||
return matches.sort((a, b) =>
|
||||
this.calculateSpecificity(b.pattern) - this.calculateSpecificity(a.pattern)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance method for interface compliance
|
||||
*/
|
||||
match(pattern: string, path: string): IPathMatchResult {
|
||||
return PathMatcher.match(pattern, path);
|
||||
}
|
||||
}
|
141
ts/core/routing/specificity.ts
Normal file
141
ts/core/routing/specificity.ts
Normal file
@ -0,0 +1,141 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
49
ts/core/routing/types.ts
Normal file
49
ts/core/routing/types.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Core routing types used throughout the routing system
|
||||
*/
|
||||
|
||||
export interface IPathMatchResult {
|
||||
matches: boolean;
|
||||
params?: Record<string, string>;
|
||||
pathMatch?: string;
|
||||
pathRemainder?: string;
|
||||
}
|
||||
|
||||
export interface IRouteMatchResult {
|
||||
matches: boolean;
|
||||
score: number;
|
||||
specificity: number;
|
||||
matchedCriteria: string[];
|
||||
}
|
||||
|
||||
export interface IDomainMatchOptions {
|
||||
allowWildcards?: boolean;
|
||||
caseInsensitive?: boolean;
|
||||
}
|
||||
|
||||
export interface IIpMatchOptions {
|
||||
allowCidr?: boolean;
|
||||
allowRanges?: boolean;
|
||||
}
|
||||
|
||||
export interface IHeaderMatchOptions {
|
||||
caseInsensitive?: boolean;
|
||||
exactMatch?: boolean;
|
||||
}
|
||||
|
||||
export interface IRouteSpecificity {
|
||||
pathSpecificity: number;
|
||||
domainSpecificity: number;
|
||||
ipSpecificity: number;
|
||||
headerSpecificity: number;
|
||||
tlsSpecificity: number;
|
||||
totalScore: number;
|
||||
}
|
||||
|
||||
export interface IMatcher<T = any, O = any> {
|
||||
match(pattern: string, value: string, options?: O): T | boolean;
|
||||
}
|
||||
|
||||
export interface IAsyncMatcher<T = any, O = any> {
|
||||
match(pattern: string, value: string, options?: O): Promise<T | boolean>;
|
||||
}
|
@ -12,7 +12,6 @@ import {
|
||||
matchPath,
|
||||
matchIpPattern,
|
||||
matchIpCidr,
|
||||
ipToNumber,
|
||||
isIpAuthorized,
|
||||
calculateRouteSpecificity
|
||||
} from './route-utils.js';
|
||||
@ -343,13 +342,6 @@ export class SharedRouteManager extends plugins.EventEmitter {
|
||||
return matchIpCidr(cidr, ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IP address to a numeric value
|
||||
* @deprecated Use the ipToNumber function from route-utils.js instead
|
||||
*/
|
||||
private ipToNumber(ip: string): number {
|
||||
return ipToNumber(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the route configuration and return any warnings
|
||||
|
@ -1,34 +1,21 @@
|
||||
/**
|
||||
* Route matching utilities for SmartProxy components
|
||||
*
|
||||
* Contains shared logic for domain matching, path matching, and IP matching
|
||||
* to be used by different proxy components throughout the system.
|
||||
* This file provides utility functions that use the unified matchers
|
||||
* and additional route-specific utilities.
|
||||
*/
|
||||
|
||||
import { DomainMatcher, PathMatcher, IpMatcher, HeaderMatcher } from '../routing/matchers/index.js';
|
||||
import { RouteSpecificity } from '../routing/specificity.js';
|
||||
import type { IRouteSpecificity } from '../routing/types.js';
|
||||
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
||||
|
||||
/**
|
||||
* Match a domain pattern against a domain
|
||||
*
|
||||
* @param pattern Domain pattern with optional wildcards (e.g., "*.example.com")
|
||||
* @param domain Domain to match against the pattern
|
||||
* @returns Whether the domain matches the pattern
|
||||
* @deprecated Use DomainMatcher.match() directly
|
||||
*/
|
||||
export function matchDomain(pattern: string, domain: string): boolean {
|
||||
// Handle exact match (case-insensitive)
|
||||
if (pattern.toLowerCase() === domain.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle wildcard pattern
|
||||
if (pattern.includes('*')) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\./g, '\\.') // Escape dots
|
||||
.replace(/\*/g, '.*'); // Convert * to .*
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
||||
return regex.test(domain);
|
||||
}
|
||||
|
||||
return false;
|
||||
return DomainMatcher.match(pattern, domain);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,211 +42,50 @@ export function matchRouteDomain(domains: string | string[] | undefined, domain:
|
||||
|
||||
/**
|
||||
* Match a path pattern against a path
|
||||
*
|
||||
* @param pattern Path pattern with optional wildcards
|
||||
* @param path Path to match against the pattern
|
||||
* @returns Whether the path matches the pattern
|
||||
* @deprecated Use PathMatcher.match() directly
|
||||
*/
|
||||
export function matchPath(pattern: string, path: string): boolean {
|
||||
// Handle exact match
|
||||
if (pattern === path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle simple wildcard at the end (like /api/*)
|
||||
if (pattern.endsWith('*')) {
|
||||
const prefix = pattern.slice(0, -1);
|
||||
return path.startsWith(prefix);
|
||||
}
|
||||
|
||||
// Handle more complex wildcard patterns
|
||||
if (pattern.includes('*')) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\./g, '\\.') // Escape dots
|
||||
.replace(/\*/g, '.*') // Convert * to .*
|
||||
.replace(/\//g, '\\/'); // Escape slashes
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(path);
|
||||
}
|
||||
|
||||
return false;
|
||||
return PathMatcher.match(pattern, path).matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CIDR notation into subnet and mask bits
|
||||
*
|
||||
* @param cidr CIDR string (e.g., "192.168.1.0/24")
|
||||
* @returns Object with subnet and bits, or null if invalid
|
||||
*/
|
||||
export function parseCidr(cidr: string): { subnet: string; bits: number } | null {
|
||||
try {
|
||||
const [subnet, bitsStr] = cidr.split('/');
|
||||
const bits = parseInt(bitsStr, 10);
|
||||
|
||||
if (isNaN(bits) || bits < 0 || bits > 32) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { subnet, bits };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IP address to a numeric value
|
||||
*
|
||||
* @param ip IPv4 address string (e.g., "192.168.1.1")
|
||||
* @returns Numeric representation of the IP
|
||||
*/
|
||||
export function ipToNumber(ip: string): number {
|
||||
// Handle IPv6-mapped IPv4 addresses (::ffff:192.168.1.1)
|
||||
if (ip.startsWith('::ffff:')) {
|
||||
ip = ip.slice(7);
|
||||
}
|
||||
|
||||
const parts = ip.split('.').map(part => parseInt(part, 10));
|
||||
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
||||
}
|
||||
// Helper functions removed - use IpMatcher internal methods instead
|
||||
|
||||
/**
|
||||
* Match an IP against a CIDR pattern
|
||||
*
|
||||
* @param cidr CIDR pattern (e.g., "192.168.1.0/24")
|
||||
* @param ip IP to match against the pattern
|
||||
* @returns Whether the IP is in the CIDR range
|
||||
* @deprecated Use IpMatcher.matchCidr() directly
|
||||
*/
|
||||
export function matchIpCidr(cidr: string, ip: string): boolean {
|
||||
const parsed = parseCidr(cidr);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { subnet, bits } = parsed;
|
||||
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet;
|
||||
|
||||
// Convert IP addresses to numeric values
|
||||
const ipNum = ipToNumber(normalizedIp);
|
||||
const subnetNum = ipToNumber(normalizedSubnet);
|
||||
|
||||
// Calculate subnet mask
|
||||
const maskNum = ~(2 ** (32 - bits) - 1);
|
||||
|
||||
// Check if IP is in subnet
|
||||
return (ipNum & maskNum) === (subnetNum & maskNum);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return IpMatcher.matchCidr(cidr, ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP pattern against an IP
|
||||
*
|
||||
* @param pattern IP pattern (exact, CIDR, or with wildcards)
|
||||
* @param ip IP to match against the pattern
|
||||
* @returns Whether the IP matches the pattern
|
||||
* @deprecated Use IpMatcher.match() directly
|
||||
*/
|
||||
export function matchIpPattern(pattern: string, ip: string): boolean {
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||
|
||||
// Handle exact match with all variations
|
||||
if (pattern === ip || normalizedPattern === normalizedIp ||
|
||||
pattern === normalizedIp || normalizedPattern === ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle "all" wildcard
|
||||
if (pattern === '*' || normalizedPattern === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
||||
if (pattern.includes('/')) {
|
||||
return matchIpCidr(pattern, normalizedIp) ||
|
||||
(normalizedPattern !== pattern && matchIpCidr(normalizedPattern, normalizedIp));
|
||||
}
|
||||
|
||||
// Handle glob pattern (e.g., 192.168.1.*)
|
||||
if (pattern.includes('*')) {
|
||||
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
if (regex.test(ip) || regex.test(normalizedIp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If pattern was normalized, also test with normalized pattern
|
||||
if (normalizedPattern !== pattern) {
|
||||
const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||
const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`);
|
||||
return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return IpMatcher.match(pattern, ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an IP against allowed and blocked IP patterns
|
||||
*
|
||||
* @param ip IP to check
|
||||
* @param ipAllowList Array of allowed IP patterns
|
||||
* @param ipBlockList Array of blocked IP patterns
|
||||
* @returns Whether the IP is allowed
|
||||
* @deprecated Use IpMatcher.isAuthorized() directly
|
||||
*/
|
||||
export function isIpAuthorized(
|
||||
ip: string,
|
||||
ipAllowList: string[] = ['*'],
|
||||
ipBlockList: string[] = []
|
||||
): boolean {
|
||||
// Check blocked IPs first
|
||||
if (ipBlockList.length > 0) {
|
||||
for (const pattern of ipBlockList) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
return false; // IP is blocked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are allowed IPs, check them
|
||||
if (ipAllowList.length > 0) {
|
||||
// Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed
|
||||
if (ipAllowList.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const pattern of ipAllowList) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
return true; // IP is allowed
|
||||
}
|
||||
}
|
||||
return false; // IP not in allowed list
|
||||
}
|
||||
|
||||
// No allowed IPs specified, so IP is allowed by default
|
||||
return true;
|
||||
return IpMatcher.isAuthorized(ip, ipAllowList, ipBlockList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an HTTP header pattern against a header value
|
||||
*
|
||||
* @param pattern Expected header value (string or RegExp)
|
||||
* @param value Actual header value
|
||||
* @returns Whether the header matches the pattern
|
||||
* @deprecated Use HeaderMatcher.match() directly
|
||||
*/
|
||||
export function matchHeader(pattern: string | RegExp, value: string): boolean {
|
||||
if (typeof pattern === 'string') {
|
||||
return pattern === value;
|
||||
} else if (pattern instanceof RegExp) {
|
||||
return pattern.test(value);
|
||||
}
|
||||
return false;
|
||||
// Convert RegExp to string pattern for HeaderMatcher
|
||||
const stringPattern = pattern instanceof RegExp ? pattern.source : pattern;
|
||||
return HeaderMatcher.match(stringPattern, value, { exactMatch: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -268,6 +94,7 @@ export function matchHeader(pattern: string | RegExp, value: string): boolean {
|
||||
*
|
||||
* @param match Match criteria to evaluate
|
||||
* @returns Numeric specificity score
|
||||
* @deprecated Consider using RouteSpecificity.calculate() with full IRouteConfig
|
||||
*/
|
||||
export function calculateRouteSpecificity(match: {
|
||||
domains?: string | string[];
|
||||
@ -278,34 +105,37 @@ export function calculateRouteSpecificity(match: {
|
||||
}): number {
|
||||
let score = 0;
|
||||
|
||||
// Path is very specific
|
||||
// Path specificity using PathMatcher
|
||||
if (match.path) {
|
||||
// More specific if it doesn't use wildcards
|
||||
score += match.path.includes('*') ? 3 : 4;
|
||||
score += PathMatcher.calculateSpecificity(match.path);
|
||||
}
|
||||
|
||||
// Domain is next most specific
|
||||
// Domain specificity using DomainMatcher
|
||||
if (match.domains) {
|
||||
const domains = Array.isArray(match.domains) ? match.domains : [match.domains];
|
||||
// More domains or more specific domains (without wildcards) increase specificity
|
||||
score += domains.length;
|
||||
// Add bonus for exact domains (without wildcards)
|
||||
score += domains.some(d => !d.includes('*')) ? 1 : 0;
|
||||
// Use the highest specificity among all domains
|
||||
const domainScore = Math.max(...domains.map(d => DomainMatcher.calculateSpecificity(d)));
|
||||
score += domainScore;
|
||||
}
|
||||
|
||||
// Headers are quite specific
|
||||
// Headers specificity using HeaderMatcher
|
||||
if (match.headers) {
|
||||
score += Object.keys(match.headers).length * 2;
|
||||
const stringHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(match.headers)) {
|
||||
stringHeaders[key] = value instanceof RegExp ? value.source : value;
|
||||
}
|
||||
score += HeaderMatcher.calculateSpecificity(stringHeaders);
|
||||
}
|
||||
|
||||
// Client IP adds some specificity
|
||||
if (match.clientIp && match.clientIp.length > 0) {
|
||||
score += 1;
|
||||
// Use the first IP pattern for specificity
|
||||
score += IpMatcher.calculateSpecificity(match.clientIp[0]);
|
||||
}
|
||||
|
||||
// TLS version adds minimal specificity
|
||||
if (match.tlsVersion && match.tlsVersion.length > 0) {
|
||||
score += 1;
|
||||
score += match.tlsVersion.length * 10;
|
||||
}
|
||||
|
||||
return score;
|
||||
|
@ -1,9 +1,5 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import {
|
||||
matchIpPattern,
|
||||
ipToNumber,
|
||||
matchIpCidr
|
||||
} from './route-utils.js';
|
||||
import { IpMatcher } from '../routing/matchers/ip.js';
|
||||
|
||||
/**
|
||||
* Security utilities for IP validation, rate limiting,
|
||||
@ -90,7 +86,7 @@ export function isIPAuthorized(
|
||||
// First check if IP is blocked - blocked IPs take precedence
|
||||
if (blockedIPs.length > 0) {
|
||||
for (const pattern of blockedIPs) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
if (IpMatcher.match(pattern, ip)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -104,7 +100,7 @@ export function isIPAuthorized(
|
||||
// Then check if IP is allowed in the explicit allow list
|
||||
if (allowedIPs.length > 0) {
|
||||
for (const pattern of allowedIPs) {
|
||||
if (matchIpPattern(pattern, ip)) {
|
||||
if (IpMatcher.match(pattern, ip)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user