@push.rocks/smartproxy 19.5.19 → 19.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/core/models/index.d.ts +2 -0
- package/dist_ts/core/models/index.js +3 -1
- package/dist_ts/core/models/socket-types.d.ts +14 -0
- package/dist_ts/core/models/socket-types.js +15 -0
- package/dist_ts/core/models/wrapped-socket.d.ts +34 -0
- package/dist_ts/core/models/wrapped-socket.js +82 -0
- package/dist_ts/core/routing/index.d.ts +11 -0
- package/dist_ts/core/routing/index.js +17 -0
- package/dist_ts/core/routing/matchers/domain.d.ts +34 -0
- package/dist_ts/core/routing/matchers/domain.js +91 -0
- package/dist_ts/core/routing/matchers/header.d.ts +32 -0
- package/dist_ts/core/routing/matchers/header.js +94 -0
- package/dist_ts/core/routing/matchers/index.d.ts +18 -0
- package/dist_ts/core/routing/matchers/index.js +20 -0
- package/dist_ts/core/routing/matchers/ip.d.ts +53 -0
- package/dist_ts/core/routing/matchers/ip.js +169 -0
- package/dist_ts/core/routing/matchers/path.d.ts +44 -0
- package/dist_ts/core/routing/matchers/path.js +148 -0
- package/dist_ts/core/routing/route-manager.d.ts +88 -0
- package/dist_ts/core/routing/route-manager.js +342 -0
- package/dist_ts/core/routing/route-utils.d.ts +28 -0
- package/dist_ts/core/routing/route-utils.js +67 -0
- package/dist_ts/core/routing/specificity.d.ts +30 -0
- package/dist_ts/core/routing/specificity.js +115 -0
- package/dist_ts/core/routing/types.d.ts +41 -0
- package/dist_ts/core/routing/types.js +5 -0
- package/dist_ts/core/utils/index.d.ts +0 -2
- package/dist_ts/core/utils/index.js +1 -3
- package/dist_ts/core/utils/route-manager.d.ts +0 -30
- package/dist_ts/core/utils/route-manager.js +6 -47
- package/dist_ts/core/utils/route-utils.d.ts +2 -68
- package/dist_ts/core/utils/route-utils.js +21 -218
- package/dist_ts/core/utils/security-utils.js +4 -4
- package/dist_ts/index.d.ts +2 -5
- package/dist_ts/index.js +5 -11
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +0 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +15 -60
- package/dist_ts/proxies/http-proxy/models/types.d.ts +0 -90
- package/dist_ts/proxies/http-proxy/models/types.js +1 -242
- package/dist_ts/proxies/http-proxy/request-handler.d.ts +3 -5
- package/dist_ts/proxies/http-proxy/request-handler.js +20 -171
- package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +2 -5
- package/dist_ts/proxies/http-proxy/websocket-handler.js +15 -23
- package/dist_ts/proxies/index.d.ts +2 -2
- package/dist_ts/proxies/index.js +4 -3
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +3 -1
- package/dist_ts/proxies/smart-proxy/connection-manager.js +15 -7
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +2 -1
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -2
- package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/index.js +2 -2
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +6 -2
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +48 -25
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +15 -4
- package/dist_ts/proxies/smart-proxy/utils/route-utils.js +10 -43
- package/dist_ts/routing/router/http-router.d.ts +89 -0
- package/dist_ts/routing/router/http-router.js +205 -0
- package/dist_ts/routing/router/index.d.ts +2 -5
- package/dist_ts/routing/router/index.js +3 -4
- package/package.json +1 -1
- package/readme.delete.md +187 -0
- package/readme.hints.md +189 -1
- package/readme.plan.md +621 -0
- package/readme.routing.md +341 -0
- package/ts/core/models/index.ts +2 -0
- package/ts/core/models/socket-types.ts +21 -0
- package/ts/core/models/wrapped-socket.ts +99 -0
- package/ts/core/routing/index.ts +21 -0
- package/ts/core/routing/matchers/domain.ts +119 -0
- package/ts/core/routing/matchers/header.ts +120 -0
- package/ts/core/routing/matchers/index.ts +22 -0
- package/ts/core/routing/matchers/ip.ts +207 -0
- package/ts/core/routing/matchers/path.ts +184 -0
- package/ts/core/{utils → routing}/route-manager.ts +7 -57
- package/ts/core/routing/route-utils.ts +88 -0
- package/ts/core/routing/specificity.ts +141 -0
- package/ts/core/routing/types.ts +49 -0
- package/ts/core/utils/index.ts +0 -2
- package/ts/core/utils/security-utils.ts +3 -7
- package/ts/index.ts +4 -14
- package/ts/proxies/http-proxy/http-proxy.ts +13 -68
- package/ts/proxies/http-proxy/models/types.ts +0 -324
- package/ts/proxies/http-proxy/request-handler.ts +15 -186
- package/ts/proxies/http-proxy/websocket-handler.ts +15 -26
- package/ts/proxies/index.ts +3 -2
- package/ts/proxies/smart-proxy/connection-manager.ts +15 -7
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +6 -2
- package/ts/proxies/smart-proxy/index.ts +1 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +8 -2
- package/ts/proxies/smart-proxy/route-connection-handler.ts +58 -30
- package/ts/proxies/smart-proxy/smart-proxy.ts +15 -3
- package/ts/proxies/smart-proxy/utils/route-utils.ts +11 -49
- package/ts/routing/router/http-router.ts +266 -0
- package/ts/routing/router/index.ts +3 -8
- package/readme.problems.md +0 -170
- package/ts/core/utils/route-utils.ts +0 -312
- package/ts/proxies/smart-proxy/route-manager.ts +0 -554
- package/ts/routing/router/proxy-router.ts +0 -437
- package/ts/routing/router/route-router.ts +0 -482
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { IMatcher, IHeaderMatchOptions } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HeaderMatcher provides HTTP header matching functionality
|
|
5
|
+
* Supporting exact matches, patterns, and case-insensitive matching
|
|
6
|
+
*/
|
|
7
|
+
export class HeaderMatcher implements IMatcher<boolean, IHeaderMatchOptions> {
|
|
8
|
+
/**
|
|
9
|
+
* Match a header value against a pattern
|
|
10
|
+
* @param pattern The pattern to match
|
|
11
|
+
* @param value The header value to test
|
|
12
|
+
* @param options Matching options
|
|
13
|
+
* @returns true if the value matches the pattern
|
|
14
|
+
*/
|
|
15
|
+
static match(
|
|
16
|
+
pattern: string,
|
|
17
|
+
value: string | undefined,
|
|
18
|
+
options: IHeaderMatchOptions = {}
|
|
19
|
+
): boolean {
|
|
20
|
+
// Handle missing header
|
|
21
|
+
if (value === undefined || value === null) {
|
|
22
|
+
return pattern === '' || pattern === null || pattern === undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Convert to string and normalize
|
|
26
|
+
const normalizedPattern = String(pattern);
|
|
27
|
+
const normalizedValue = String(value);
|
|
28
|
+
|
|
29
|
+
// Apply case sensitivity
|
|
30
|
+
const comparePattern = options.caseInsensitive !== false
|
|
31
|
+
? normalizedPattern.toLowerCase()
|
|
32
|
+
: normalizedPattern;
|
|
33
|
+
const compareValue = options.caseInsensitive !== false
|
|
34
|
+
? normalizedValue.toLowerCase()
|
|
35
|
+
: normalizedValue;
|
|
36
|
+
|
|
37
|
+
// Exact match
|
|
38
|
+
if (options.exactMatch !== false) {
|
|
39
|
+
return comparePattern === compareValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pattern matching (simple wildcard support)
|
|
43
|
+
if (comparePattern.includes('*')) {
|
|
44
|
+
const regex = new RegExp(
|
|
45
|
+
'^' + comparePattern.replace(/\*/g, '.*') + '$',
|
|
46
|
+
options.caseInsensitive !== false ? 'i' : ''
|
|
47
|
+
);
|
|
48
|
+
return regex.test(normalizedValue);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Contains match (if not exact match mode)
|
|
52
|
+
return compareValue.includes(comparePattern);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match multiple headers against a set of required headers
|
|
57
|
+
* @param requiredHeaders Headers that must match
|
|
58
|
+
* @param actualHeaders Actual request headers
|
|
59
|
+
* @param options Matching options
|
|
60
|
+
* @returns true if all required headers match
|
|
61
|
+
*/
|
|
62
|
+
static matchAll(
|
|
63
|
+
requiredHeaders: Record<string, string>,
|
|
64
|
+
actualHeaders: Record<string, string | string[] | undefined>,
|
|
65
|
+
options: IHeaderMatchOptions = {}
|
|
66
|
+
): boolean {
|
|
67
|
+
for (const [name, pattern] of Object.entries(requiredHeaders)) {
|
|
68
|
+
const headerName = options.caseInsensitive !== false
|
|
69
|
+
? name.toLowerCase()
|
|
70
|
+
: name;
|
|
71
|
+
|
|
72
|
+
// Find the actual header (case-insensitive search if needed)
|
|
73
|
+
let actualValue: string | undefined;
|
|
74
|
+
if (options.caseInsensitive !== false) {
|
|
75
|
+
const actualKey = Object.keys(actualHeaders).find(
|
|
76
|
+
key => key.toLowerCase() === headerName
|
|
77
|
+
);
|
|
78
|
+
const rawValue = actualKey ? actualHeaders[actualKey] : undefined;
|
|
79
|
+
// Handle array values (multiple headers with same name)
|
|
80
|
+
actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue;
|
|
81
|
+
} else {
|
|
82
|
+
const rawValue = actualHeaders[name];
|
|
83
|
+
// Handle array values (multiple headers with same name)
|
|
84
|
+
actualValue = Array.isArray(rawValue) ? rawValue.join(', ') : rawValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if this header matches
|
|
88
|
+
if (!this.match(pattern, actualValue, options)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Calculate the specificity of header requirements
|
|
98
|
+
* More headers = more specific
|
|
99
|
+
*/
|
|
100
|
+
static calculateSpecificity(headers: Record<string, string>): number {
|
|
101
|
+
const count = Object.keys(headers).length;
|
|
102
|
+
let score = count * 10;
|
|
103
|
+
|
|
104
|
+
// Bonus for headers without wildcards (more specific)
|
|
105
|
+
for (const value of Object.values(headers)) {
|
|
106
|
+
if (!value.includes('*')) {
|
|
107
|
+
score += 5;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return score;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Instance method for interface compliance
|
|
116
|
+
*/
|
|
117
|
+
match(pattern: string, value: string, options?: IHeaderMatchOptions): boolean {
|
|
118
|
+
return HeaderMatcher.match(pattern, value, options);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified matching utilities for the routing system
|
|
3
|
+
* All route matching logic should use these matchers for consistency
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './domain.js';
|
|
7
|
+
export * from './path.js';
|
|
8
|
+
export * from './ip.js';
|
|
9
|
+
export * from './header.js';
|
|
10
|
+
|
|
11
|
+
// Re-export for convenience
|
|
12
|
+
import { DomainMatcher } from './domain.js';
|
|
13
|
+
import { PathMatcher } from './path.js';
|
|
14
|
+
import { IpMatcher } from './ip.js';
|
|
15
|
+
import { HeaderMatcher } from './header.js';
|
|
16
|
+
|
|
17
|
+
export const matchers = {
|
|
18
|
+
domain: DomainMatcher,
|
|
19
|
+
path: PathMatcher,
|
|
20
|
+
ip: IpMatcher,
|
|
21
|
+
header: HeaderMatcher
|
|
22
|
+
} as const;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { IMatcher, IIpMatchOptions } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IpMatcher provides comprehensive IP address matching functionality
|
|
5
|
+
* Supporting exact matches, CIDR notation, ranges, and wildcards
|
|
6
|
+
*/
|
|
7
|
+
export class IpMatcher implements IMatcher<boolean, IIpMatchOptions> {
|
|
8
|
+
/**
|
|
9
|
+
* Check if a value is a valid IPv4 address
|
|
10
|
+
*/
|
|
11
|
+
static isValidIpv4(ip: string): boolean {
|
|
12
|
+
const parts = ip.split('.');
|
|
13
|
+
if (parts.length !== 4) return false;
|
|
14
|
+
|
|
15
|
+
return parts.every(part => {
|
|
16
|
+
const num = parseInt(part, 10);
|
|
17
|
+
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a value is a valid IPv6 address (simplified check)
|
|
23
|
+
*/
|
|
24
|
+
static isValidIpv6(ip: string): boolean {
|
|
25
|
+
// Basic IPv6 validation - can be enhanced
|
|
26
|
+
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})$/;
|
|
27
|
+
return ipv6Regex.test(ip);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert IP address to numeric value for comparison
|
|
32
|
+
*/
|
|
33
|
+
private static ipToNumber(ip: string): number {
|
|
34
|
+
const parts = ip.split('.');
|
|
35
|
+
return parts.reduce((acc, part, index) => {
|
|
36
|
+
return acc + (parseInt(part, 10) << (8 * (3 - index)));
|
|
37
|
+
}, 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Match an IP against a CIDR notation pattern
|
|
42
|
+
*/
|
|
43
|
+
static matchCidr(cidr: string, ip: string): boolean {
|
|
44
|
+
const [range, bits] = cidr.split('/');
|
|
45
|
+
if (!bits || !this.isValidIpv4(range) || !this.isValidIpv4(ip)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rangeMask = parseInt(bits, 10);
|
|
50
|
+
if (isNaN(rangeMask) || rangeMask < 0 || rangeMask > 32) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rangeNum = this.ipToNumber(range);
|
|
55
|
+
const ipNum = this.ipToNumber(ip);
|
|
56
|
+
const mask = (-1 << (32 - rangeMask)) >>> 0;
|
|
57
|
+
|
|
58
|
+
return (rangeNum & mask) === (ipNum & mask);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Match an IP against a wildcard pattern
|
|
63
|
+
*/
|
|
64
|
+
static matchWildcard(pattern: string, ip: string): boolean {
|
|
65
|
+
if (!this.isValidIpv4(ip)) return false;
|
|
66
|
+
|
|
67
|
+
const patternParts = pattern.split('.');
|
|
68
|
+
const ipParts = ip.split('.');
|
|
69
|
+
|
|
70
|
+
if (patternParts.length !== 4) return false;
|
|
71
|
+
|
|
72
|
+
return patternParts.every((part, index) => {
|
|
73
|
+
if (part === '*') return true;
|
|
74
|
+
return part === ipParts[index];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Match an IP against a range (e.g., "192.168.1.1-192.168.1.100")
|
|
80
|
+
*/
|
|
81
|
+
static matchRange(range: string, ip: string): boolean {
|
|
82
|
+
const [start, end] = range.split('-').map(s => s.trim());
|
|
83
|
+
|
|
84
|
+
if (!start || !end || !this.isValidIpv4(start) || !this.isValidIpv4(end) || !this.isValidIpv4(ip)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const startNum = this.ipToNumber(start);
|
|
89
|
+
const endNum = this.ipToNumber(end);
|
|
90
|
+
const ipNum = this.ipToNumber(ip);
|
|
91
|
+
|
|
92
|
+
return ipNum >= startNum && ipNum <= endNum;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Match an IP pattern against an IP address
|
|
97
|
+
* Supports multiple formats:
|
|
98
|
+
* - Exact match: "192.168.1.1"
|
|
99
|
+
* - CIDR: "192.168.1.0/24"
|
|
100
|
+
* - Wildcard: "192.168.1.*"
|
|
101
|
+
* - Range: "192.168.1.1-192.168.1.100"
|
|
102
|
+
*/
|
|
103
|
+
static match(
|
|
104
|
+
pattern: string,
|
|
105
|
+
ip: string,
|
|
106
|
+
options: IIpMatchOptions = {}
|
|
107
|
+
): boolean {
|
|
108
|
+
// Handle null/undefined cases
|
|
109
|
+
if (!pattern || !ip) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Normalize inputs
|
|
114
|
+
const normalizedPattern = pattern.trim();
|
|
115
|
+
const normalizedIp = ip.trim();
|
|
116
|
+
|
|
117
|
+
// Extract IPv4 from IPv6-mapped addresses (::ffff:192.168.1.1)
|
|
118
|
+
const ipv4Match = normalizedIp.match(/::ffff:(\d+\.\d+\.\d+\.\d+)/i);
|
|
119
|
+
const testIp = ipv4Match ? ipv4Match[1] : normalizedIp;
|
|
120
|
+
|
|
121
|
+
// Exact match
|
|
122
|
+
if (normalizedPattern === testIp) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// CIDR notation
|
|
127
|
+
if (options.allowCidr !== false && normalizedPattern.includes('/')) {
|
|
128
|
+
return this.matchCidr(normalizedPattern, testIp);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Wildcard matching
|
|
132
|
+
if (normalizedPattern.includes('*')) {
|
|
133
|
+
return this.matchWildcard(normalizedPattern, testIp);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Range matching
|
|
137
|
+
if (options.allowRanges !== false && normalizedPattern.includes('-')) {
|
|
138
|
+
return this.matchRange(normalizedPattern, testIp);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if an IP is authorized based on allow and block lists
|
|
146
|
+
*/
|
|
147
|
+
static isAuthorized(
|
|
148
|
+
ip: string,
|
|
149
|
+
allowList: string[] = [],
|
|
150
|
+
blockList: string[] = []
|
|
151
|
+
): boolean {
|
|
152
|
+
// If IP is in block list, deny
|
|
153
|
+
if (blockList.some(pattern => this.match(pattern, ip))) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If allow list is empty, allow all (except blocked)
|
|
158
|
+
if (allowList.length === 0) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If allow list exists, IP must match
|
|
163
|
+
return allowList.some(pattern => this.match(pattern, ip));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Calculate the specificity of an IP pattern
|
|
168
|
+
* Higher values mean more specific patterns
|
|
169
|
+
*/
|
|
170
|
+
static calculateSpecificity(pattern: string): number {
|
|
171
|
+
if (!pattern) return 0;
|
|
172
|
+
|
|
173
|
+
let score = 0;
|
|
174
|
+
|
|
175
|
+
// Exact IPs are most specific
|
|
176
|
+
if (this.isValidIpv4(pattern) || this.isValidIpv6(pattern)) {
|
|
177
|
+
score += 100;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// CIDR notation
|
|
181
|
+
if (pattern.includes('/')) {
|
|
182
|
+
const [, bits] = pattern.split('/');
|
|
183
|
+
const maskBits = parseInt(bits, 10);
|
|
184
|
+
if (!isNaN(maskBits)) {
|
|
185
|
+
score += maskBits; // Higher mask = more specific
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Wildcard patterns
|
|
190
|
+
const wildcards = (pattern.match(/\*/g) || []).length;
|
|
191
|
+
score -= wildcards * 20; // More wildcards = less specific
|
|
192
|
+
|
|
193
|
+
// Range patterns are somewhat specific
|
|
194
|
+
if (pattern.includes('-')) {
|
|
195
|
+
score += 30;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return score;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Instance method for interface compliance
|
|
203
|
+
*/
|
|
204
|
+
match(pattern: string, ip: string, options?: IIpMatchOptions): boolean {
|
|
205
|
+
return IpMatcher.match(pattern, ip, options);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { IMatcher, IPathMatchResult } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PathMatcher provides comprehensive path matching functionality
|
|
5
|
+
* Supporting exact matches, wildcards, and parameter extraction
|
|
6
|
+
*/
|
|
7
|
+
export class PathMatcher implements IMatcher<IPathMatchResult> {
|
|
8
|
+
/**
|
|
9
|
+
* Convert a path pattern to a regex and extract parameter names
|
|
10
|
+
* Supports:
|
|
11
|
+
* - Exact paths: /api/users
|
|
12
|
+
* - Wildcards: /api/*
|
|
13
|
+
* - Parameters: /api/users/:id
|
|
14
|
+
* - Mixed: /api/users/:id/*
|
|
15
|
+
*/
|
|
16
|
+
private static patternToRegex(pattern: string): {
|
|
17
|
+
regex: RegExp;
|
|
18
|
+
paramNames: string[]
|
|
19
|
+
} {
|
|
20
|
+
const paramNames: string[] = [];
|
|
21
|
+
let regexPattern = pattern;
|
|
22
|
+
|
|
23
|
+
// Escape special regex characters except : and *
|
|
24
|
+
regexPattern = regexPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
+
|
|
26
|
+
// Handle path parameters (:param)
|
|
27
|
+
regexPattern = regexPattern.replace(/:(\w+)/g, (match, paramName) => {
|
|
28
|
+
paramNames.push(paramName);
|
|
29
|
+
return '([^/]+)'; // Match any non-slash characters
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Handle wildcards
|
|
33
|
+
regexPattern = regexPattern.replace(/\*/g, '(.*)');
|
|
34
|
+
|
|
35
|
+
// Ensure the pattern matches from start
|
|
36
|
+
regexPattern = `^${regexPattern}`;
|
|
37
|
+
|
|
38
|
+
// If pattern doesn't end with wildcard, ensure it matches to end
|
|
39
|
+
// But only for patterns that don't have parameters or wildcards
|
|
40
|
+
if (!pattern.includes('*') && !pattern.includes(':') && !pattern.endsWith('/')) {
|
|
41
|
+
regexPattern = `${regexPattern}$`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
regex: new RegExp(regexPattern),
|
|
46
|
+
paramNames
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Match a path pattern against a request path
|
|
52
|
+
* @param pattern The pattern to match
|
|
53
|
+
* @param path The request path to test
|
|
54
|
+
* @returns Match result with params and remainder
|
|
55
|
+
*/
|
|
56
|
+
static match(pattern: string, path: string): IPathMatchResult {
|
|
57
|
+
// Handle null/undefined cases
|
|
58
|
+
if (!pattern || !path) {
|
|
59
|
+
return { matches: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Normalize paths (remove trailing slashes unless it's just "/")
|
|
63
|
+
const normalizedPattern = pattern === '/' ? '/' : pattern.replace(/\/$/, '');
|
|
64
|
+
const normalizedPath = path === '/' ? '/' : path.replace(/\/$/, '');
|
|
65
|
+
|
|
66
|
+
// Exact match (most common case)
|
|
67
|
+
if (normalizedPattern === normalizedPath) {
|
|
68
|
+
return {
|
|
69
|
+
matches: true,
|
|
70
|
+
pathMatch: normalizedPath,
|
|
71
|
+
pathRemainder: '',
|
|
72
|
+
params: {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Pattern matching (wildcards and parameters)
|
|
77
|
+
const { regex, paramNames } = this.patternToRegex(normalizedPattern);
|
|
78
|
+
const match = normalizedPath.match(regex);
|
|
79
|
+
|
|
80
|
+
if (!match) {
|
|
81
|
+
return { matches: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract parameters
|
|
85
|
+
const params: Record<string, string> = {};
|
|
86
|
+
paramNames.forEach((name, index) => {
|
|
87
|
+
params[name] = match[index + 1];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Calculate path match and remainder
|
|
91
|
+
let pathMatch = match[0];
|
|
92
|
+
let pathRemainder = normalizedPath.substring(pathMatch.length);
|
|
93
|
+
|
|
94
|
+
// Handle wildcard captures
|
|
95
|
+
if (normalizedPattern.includes('*') && match.length > paramNames.length + 1) {
|
|
96
|
+
const wildcardCapture = match[match.length - 1];
|
|
97
|
+
if (wildcardCapture) {
|
|
98
|
+
pathRemainder = wildcardCapture;
|
|
99
|
+
pathMatch = normalizedPath.substring(0, normalizedPath.length - wildcardCapture.length);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clean up path match (remove trailing slash if present)
|
|
104
|
+
if (pathMatch !== '/' && pathMatch.endsWith('/')) {
|
|
105
|
+
pathMatch = pathMatch.slice(0, -1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
matches: true,
|
|
110
|
+
pathMatch,
|
|
111
|
+
pathRemainder,
|
|
112
|
+
params
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a pattern contains parameters or wildcards
|
|
118
|
+
*/
|
|
119
|
+
static isDynamicPattern(pattern: string): boolean {
|
|
120
|
+
return pattern.includes(':') || pattern.includes('*');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Calculate the specificity of a path pattern
|
|
125
|
+
* Higher values mean more specific patterns
|
|
126
|
+
*/
|
|
127
|
+
static calculateSpecificity(pattern: string): number {
|
|
128
|
+
if (!pattern) return 0;
|
|
129
|
+
|
|
130
|
+
let score = 0;
|
|
131
|
+
|
|
132
|
+
// Exact paths are most specific
|
|
133
|
+
if (!this.isDynamicPattern(pattern)) {
|
|
134
|
+
score += 100;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Count path segments
|
|
138
|
+
const segments = pattern.split('/').filter(s => s.length > 0);
|
|
139
|
+
score += segments.length * 10;
|
|
140
|
+
|
|
141
|
+
// Count static segments (more static = more specific)
|
|
142
|
+
const staticSegments = segments.filter(s => !s.startsWith(':') && s !== '*');
|
|
143
|
+
score += staticSegments.length * 20;
|
|
144
|
+
|
|
145
|
+
// Penalize wildcards and parameters
|
|
146
|
+
const wildcards = (pattern.match(/\*/g) || []).length;
|
|
147
|
+
const params = (pattern.match(/:/g) || []).length;
|
|
148
|
+
score -= wildcards * 30; // Wildcards are very generic
|
|
149
|
+
score -= params * 10; // Parameters are somewhat generic
|
|
150
|
+
|
|
151
|
+
// Bonus for longer patterns
|
|
152
|
+
score += pattern.length;
|
|
153
|
+
|
|
154
|
+
return score;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find all matching patterns from a list
|
|
159
|
+
* Returns patterns sorted by specificity (most specific first)
|
|
160
|
+
*/
|
|
161
|
+
static findAllMatches(patterns: string[], path: string): Array<{
|
|
162
|
+
pattern: string;
|
|
163
|
+
result: IPathMatchResult;
|
|
164
|
+
}> {
|
|
165
|
+
const matches = patterns
|
|
166
|
+
.map(pattern => ({
|
|
167
|
+
pattern,
|
|
168
|
+
result: this.match(pattern, path)
|
|
169
|
+
}))
|
|
170
|
+
.filter(({ result }) => result.matches);
|
|
171
|
+
|
|
172
|
+
// Sort by specificity (highest first)
|
|
173
|
+
return matches.sort((a, b) =>
|
|
174
|
+
this.calculateSpecificity(b.pattern) - this.calculateSpecificity(a.pattern)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Instance method for interface compliance
|
|
180
|
+
*/
|
|
181
|
+
match(pattern: string, path: string): IPathMatchResult {
|
|
182
|
+
return PathMatcher.match(pattern, path);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -7,20 +7,15 @@ import type {
|
|
|
7
7
|
IRouteContext
|
|
8
8
|
} from '../../proxies/smart-proxy/models/route-types.js';
|
|
9
9
|
import {
|
|
10
|
-
matchDomain,
|
|
11
10
|
matchRouteDomain,
|
|
12
|
-
matchPath,
|
|
13
|
-
matchIpPattern,
|
|
14
|
-
matchIpCidr,
|
|
15
|
-
ipToNumber,
|
|
16
|
-
isIpAuthorized,
|
|
17
11
|
calculateRouteSpecificity
|
|
18
12
|
} from './route-utils.js';
|
|
13
|
+
import { DomainMatcher, PathMatcher, IpMatcher } from './matchers/index.js';
|
|
19
14
|
|
|
20
15
|
/**
|
|
21
|
-
* Result of route
|
|
16
|
+
* Result of route lookup
|
|
22
17
|
*/
|
|
23
|
-
export interface
|
|
18
|
+
export interface IRouteLookupResult {
|
|
24
19
|
route: IRouteConfig;
|
|
25
20
|
// Additional match parameters (path, query, etc.)
|
|
26
21
|
params?: Record<string, string>;
|
|
@@ -219,7 +214,7 @@ export class SharedRouteManager extends plugins.EventEmitter {
|
|
|
219
214
|
/**
|
|
220
215
|
* Find the matching route for a connection
|
|
221
216
|
*/
|
|
222
|
-
public findMatchingRoute(context: IRouteContext):
|
|
217
|
+
public findMatchingRoute(context: IRouteContext): IRouteLookupResult | null {
|
|
223
218
|
// Get routes for this port if using port-based filtering
|
|
224
219
|
const routesToCheck = context.port
|
|
225
220
|
? (this.portMap.get(context.port) || [])
|
|
@@ -258,21 +253,21 @@ export class SharedRouteManager extends plugins.EventEmitter {
|
|
|
258
253
|
? route.match.domains
|
|
259
254
|
: [route.match.domains];
|
|
260
255
|
|
|
261
|
-
if (!domains.some(domainPattern =>
|
|
256
|
+
if (!domains.some(domainPattern => DomainMatcher.match(domainPattern, context.domain!))) {
|
|
262
257
|
return false;
|
|
263
258
|
}
|
|
264
259
|
}
|
|
265
260
|
|
|
266
261
|
// Check path match if specified
|
|
267
262
|
if (route.match.path && context.path) {
|
|
268
|
-
if (!
|
|
263
|
+
if (!PathMatcher.match(route.match.path, context.path).matches) {
|
|
269
264
|
return false;
|
|
270
265
|
}
|
|
271
266
|
}
|
|
272
267
|
|
|
273
268
|
// Check client IP match if specified
|
|
274
269
|
if (route.match.clientIp && context.clientIp) {
|
|
275
|
-
if (!route.match.clientIp.some(ip =>
|
|
270
|
+
if (!route.match.clientIp.some(ip => IpMatcher.match(ip, context.clientIp))) {
|
|
276
271
|
return false;
|
|
277
272
|
}
|
|
278
273
|
}
|
|
@@ -311,45 +306,7 @@ export class SharedRouteManager extends plugins.EventEmitter {
|
|
|
311
306
|
return true;
|
|
312
307
|
}
|
|
313
308
|
|
|
314
|
-
/**
|
|
315
|
-
* Match a domain pattern against a domain
|
|
316
|
-
* @deprecated Use the matchDomain function from route-utils.js instead
|
|
317
|
-
*/
|
|
318
|
-
public matchDomain(pattern: string, domain: string): boolean {
|
|
319
|
-
return matchDomain(pattern, domain);
|
|
320
|
-
}
|
|
321
309
|
|
|
322
|
-
/**
|
|
323
|
-
* Match a path pattern against a path
|
|
324
|
-
* @deprecated Use the matchPath function from route-utils.js instead
|
|
325
|
-
*/
|
|
326
|
-
public matchPath(pattern: string, path: string): boolean {
|
|
327
|
-
return matchPath(pattern, path);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Match an IP pattern against a pattern
|
|
332
|
-
* @deprecated Use the matchIpPattern function from route-utils.js instead
|
|
333
|
-
*/
|
|
334
|
-
public matchIpPattern(pattern: string, ip: string): boolean {
|
|
335
|
-
return matchIpPattern(pattern, ip);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Match an IP against a CIDR pattern
|
|
340
|
-
* @deprecated Use the matchIpCidr function from route-utils.js instead
|
|
341
|
-
*/
|
|
342
|
-
public matchIpCidr(cidr: string, ip: string): boolean {
|
|
343
|
-
return matchIpCidr(cidr, ip);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Convert an IP address to a numeric value
|
|
348
|
-
* @deprecated Use the ipToNumber function from route-utils.js instead
|
|
349
|
-
*/
|
|
350
|
-
private ipToNumber(ip: string): number {
|
|
351
|
-
return ipToNumber(ip);
|
|
352
|
-
}
|
|
353
310
|
|
|
354
311
|
/**
|
|
355
312
|
* Validate the route configuration and return any warnings
|
|
@@ -479,11 +436,4 @@ export class SharedRouteManager extends plugins.EventEmitter {
|
|
|
479
436
|
return true;
|
|
480
437
|
}
|
|
481
438
|
|
|
482
|
-
/**
|
|
483
|
-
* Check if route1 is more specific than route2
|
|
484
|
-
* @deprecated Use the calculateRouteSpecificity function from route-utils.js instead
|
|
485
|
-
*/
|
|
486
|
-
private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean {
|
|
487
|
-
return calculateRouteSpecificity(match1) > calculateRouteSpecificity(match2);
|
|
488
|
-
}
|
|
489
439
|
}
|