@push.rocks/smartproxy 16.0.2 → 16.0.3

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.
Files changed (115) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/core/models/index.d.ts +2 -0
  3. package/dist_ts/core/models/index.js +3 -1
  4. package/dist_ts/core/models/route-context.d.ts +62 -0
  5. package/dist_ts/core/models/route-context.js +43 -0
  6. package/dist_ts/core/models/socket-augmentation.d.ts +12 -0
  7. package/dist_ts/core/models/socket-augmentation.js +18 -0
  8. package/dist_ts/core/utils/event-system.d.ts +200 -0
  9. package/dist_ts/core/utils/event-system.js +224 -0
  10. package/dist_ts/core/utils/index.d.ts +7 -0
  11. package/dist_ts/core/utils/index.js +8 -1
  12. package/dist_ts/core/utils/route-manager.d.ts +118 -0
  13. package/dist_ts/core/utils/route-manager.js +383 -0
  14. package/dist_ts/core/utils/route-utils.d.ts +94 -0
  15. package/dist_ts/core/utils/route-utils.js +264 -0
  16. package/dist_ts/core/utils/security-utils.d.ts +111 -0
  17. package/dist_ts/core/utils/security-utils.js +212 -0
  18. package/dist_ts/core/utils/shared-security-manager.d.ts +110 -0
  19. package/dist_ts/core/utils/shared-security-manager.js +252 -0
  20. package/dist_ts/core/utils/template-utils.d.ts +37 -0
  21. package/dist_ts/core/utils/template-utils.js +104 -0
  22. package/dist_ts/core/utils/websocket-utils.d.ts +23 -0
  23. package/dist_ts/core/utils/websocket-utils.js +86 -0
  24. package/dist_ts/http/router/index.d.ts +5 -1
  25. package/dist_ts/http/router/index.js +4 -2
  26. package/dist_ts/http/router/route-router.d.ts +108 -0
  27. package/dist_ts/http/router/route-router.js +393 -0
  28. package/dist_ts/index.d.ts +8 -2
  29. package/dist_ts/index.js +10 -3
  30. package/dist_ts/proxies/index.d.ts +7 -2
  31. package/dist_ts/proxies/index.js +10 -4
  32. package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +21 -0
  33. package/dist_ts/proxies/network-proxy/certificate-manager.js +92 -1
  34. package/dist_ts/proxies/network-proxy/context-creator.d.ts +34 -0
  35. package/dist_ts/proxies/network-proxy/context-creator.js +108 -0
  36. package/dist_ts/proxies/network-proxy/function-cache.d.ts +90 -0
  37. package/dist_ts/proxies/network-proxy/function-cache.js +198 -0
  38. package/dist_ts/proxies/network-proxy/http-request-handler.d.ts +40 -0
  39. package/dist_ts/proxies/network-proxy/http-request-handler.js +256 -0
  40. package/dist_ts/proxies/network-proxy/http2-request-handler.d.ts +24 -0
  41. package/dist_ts/proxies/network-proxy/http2-request-handler.js +201 -0
  42. package/dist_ts/proxies/network-proxy/models/types.d.ts +73 -1
  43. package/dist_ts/proxies/network-proxy/models/types.js +242 -1
  44. package/dist_ts/proxies/network-proxy/network-proxy.d.ts +23 -20
  45. package/dist_ts/proxies/network-proxy/network-proxy.js +147 -60
  46. package/dist_ts/proxies/network-proxy/request-handler.d.ts +38 -5
  47. package/dist_ts/proxies/network-proxy/request-handler.js +584 -198
  48. package/dist_ts/proxies/network-proxy/security-manager.d.ts +65 -0
  49. package/dist_ts/proxies/network-proxy/security-manager.js +255 -0
  50. package/dist_ts/proxies/network-proxy/websocket-handler.d.ts +13 -2
  51. package/dist_ts/proxies/network-proxy/websocket-handler.js +238 -20
  52. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
  53. package/dist_ts/proxies/smart-proxy/index.js +3 -3
  54. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +3 -5
  55. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +56 -3
  56. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +4 -57
  57. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +19 -228
  58. package/dist_ts/proxies/smart-proxy/port-manager.d.ts +81 -0
  59. package/dist_ts/proxies/smart-proxy/port-manager.js +166 -0
  60. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +5 -0
  61. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +131 -15
  62. package/dist_ts/proxies/smart-proxy/route-helpers/index.d.ts +3 -1
  63. package/dist_ts/proxies/smart-proxy/route-helpers/index.js +5 -3
  64. package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +5 -178
  65. package/dist_ts/proxies/smart-proxy/route-helpers.js +8 -296
  66. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +11 -2
  67. package/dist_ts/proxies/smart-proxy/route-manager.js +79 -10
  68. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +29 -2
  69. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -43
  70. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +67 -1
  71. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +120 -1
  72. package/dist_ts/proxies/smart-proxy/utils/route-validators.d.ts +3 -3
  73. package/dist_ts/proxies/smart-proxy/utils/route-validators.js +27 -5
  74. package/package.json +1 -1
  75. package/readme.md +102 -14
  76. package/readme.plan.md +103 -168
  77. package/ts/00_commitinfo_data.ts +1 -1
  78. package/ts/core/models/index.ts +2 -0
  79. package/ts/core/models/route-context.ts +113 -0
  80. package/ts/core/models/socket-augmentation.ts +33 -0
  81. package/ts/core/utils/event-system.ts +376 -0
  82. package/ts/core/utils/index.ts +7 -0
  83. package/ts/core/utils/route-manager.ts +489 -0
  84. package/ts/core/utils/route-utils.ts +312 -0
  85. package/ts/core/utils/security-utils.ts +309 -0
  86. package/ts/core/utils/shared-security-manager.ts +333 -0
  87. package/ts/core/utils/template-utils.ts +124 -0
  88. package/ts/core/utils/websocket-utils.ts +81 -0
  89. package/ts/http/router/index.ts +8 -1
  90. package/ts/http/router/route-router.ts +482 -0
  91. package/ts/index.ts +14 -2
  92. package/ts/proxies/index.ts +12 -3
  93. package/ts/proxies/network-proxy/certificate-manager.ts +114 -10
  94. package/ts/proxies/network-proxy/context-creator.ts +145 -0
  95. package/ts/proxies/network-proxy/function-cache.ts +259 -0
  96. package/ts/proxies/network-proxy/http-request-handler.ts +330 -0
  97. package/ts/proxies/network-proxy/http2-request-handler.ts +255 -0
  98. package/ts/proxies/network-proxy/models/types.ts +312 -1
  99. package/ts/proxies/network-proxy/network-proxy.ts +195 -86
  100. package/ts/proxies/network-proxy/request-handler.ts +698 -246
  101. package/ts/proxies/network-proxy/security-manager.ts +298 -0
  102. package/ts/proxies/network-proxy/websocket-handler.ts +276 -33
  103. package/ts/proxies/smart-proxy/index.ts +2 -12
  104. package/ts/proxies/smart-proxy/models/interfaces.ts +7 -4
  105. package/ts/proxies/smart-proxy/models/route-types.ts +78 -10
  106. package/ts/proxies/smart-proxy/network-proxy-bridge.ts +20 -257
  107. package/ts/proxies/smart-proxy/port-manager.ts +195 -0
  108. package/ts/proxies/smart-proxy/route-connection-handler.ts +156 -21
  109. package/ts/proxies/smart-proxy/route-manager.ts +98 -14
  110. package/ts/proxies/smart-proxy/smart-proxy.ts +56 -55
  111. package/ts/proxies/smart-proxy/utils/route-helpers.ts +167 -1
  112. package/ts/proxies/smart-proxy/utils/route-validators.ts +24 -5
  113. package/ts/proxies/smart-proxy/domain-config-manager.ts.bak +0 -441
  114. package/ts/proxies/smart-proxy/route-helpers/index.ts +0 -9
  115. package/ts/proxies/smart-proxy/route-helpers.ts +0 -498
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Route matching utilities for SmartProxy components
3
+ *
4
+ * Contains shared logic for domain matching, path matching, and IP matching
5
+ * to be used by different proxy components throughout the system.
6
+ */
7
+
8
+ /**
9
+ * Match a domain pattern against a domain
10
+ *
11
+ * @param pattern Domain pattern with optional wildcards (e.g., "*.example.com")
12
+ * @param domain Domain to match against the pattern
13
+ * @returns Whether the domain matches the pattern
14
+ */
15
+ export function matchDomain(pattern: string, domain: string): boolean {
16
+ // Handle exact match (case-insensitive)
17
+ if (pattern.toLowerCase() === domain.toLowerCase()) {
18
+ return true;
19
+ }
20
+
21
+ // Handle wildcard pattern
22
+ if (pattern.includes('*')) {
23
+ const regexPattern = pattern
24
+ .replace(/\./g, '\\.') // Escape dots
25
+ .replace(/\*/g, '.*'); // Convert * to .*
26
+
27
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
28
+ return regex.test(domain);
29
+ }
30
+
31
+ return false;
32
+ }
33
+
34
+ /**
35
+ * Match domains from a route against a given domain
36
+ *
37
+ * @param domains Array or single domain pattern to match against
38
+ * @param domain Domain to match
39
+ * @returns Whether the domain matches any of the patterns
40
+ */
41
+ export function matchRouteDomain(domains: string | string[] | undefined, domain: string | undefined): boolean {
42
+ // If no domains specified in the route, match all domains
43
+ if (!domains) {
44
+ return true;
45
+ }
46
+
47
+ // If no domain in the request, can't match domain-specific routes
48
+ if (!domain) {
49
+ return false;
50
+ }
51
+
52
+ const patterns = Array.isArray(domains) ? domains : [domains];
53
+ return patterns.some(pattern => matchDomain(pattern, domain));
54
+ }
55
+
56
+ /**
57
+ * Match a path pattern against a path
58
+ *
59
+ * @param pattern Path pattern with optional wildcards
60
+ * @param path Path to match against the pattern
61
+ * @returns Whether the path matches the pattern
62
+ */
63
+ export function matchPath(pattern: string, path: string): boolean {
64
+ // Handle exact match
65
+ if (pattern === path) {
66
+ return true;
67
+ }
68
+
69
+ // Handle simple wildcard at the end (like /api/*)
70
+ if (pattern.endsWith('*')) {
71
+ const prefix = pattern.slice(0, -1);
72
+ return path.startsWith(prefix);
73
+ }
74
+
75
+ // Handle more complex wildcard patterns
76
+ if (pattern.includes('*')) {
77
+ const regexPattern = pattern
78
+ .replace(/\./g, '\\.') // Escape dots
79
+ .replace(/\*/g, '.*') // Convert * to .*
80
+ .replace(/\//g, '\\/'); // Escape slashes
81
+
82
+ const regex = new RegExp(`^${regexPattern}$`);
83
+ return regex.test(path);
84
+ }
85
+
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * Parse CIDR notation into subnet and mask bits
91
+ *
92
+ * @param cidr CIDR string (e.g., "192.168.1.0/24")
93
+ * @returns Object with subnet and bits, or null if invalid
94
+ */
95
+ export function parseCidr(cidr: string): { subnet: string; bits: number } | null {
96
+ try {
97
+ const [subnet, bitsStr] = cidr.split('/');
98
+ const bits = parseInt(bitsStr, 10);
99
+
100
+ if (isNaN(bits) || bits < 0 || bits > 32) {
101
+ return null;
102
+ }
103
+
104
+ return { subnet, bits };
105
+ } catch (e) {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Convert an IP address to a numeric value
112
+ *
113
+ * @param ip IPv4 address string (e.g., "192.168.1.1")
114
+ * @returns Numeric representation of the IP
115
+ */
116
+ export function ipToNumber(ip: string): number {
117
+ // Handle IPv6-mapped IPv4 addresses (::ffff:192.168.1.1)
118
+ if (ip.startsWith('::ffff:')) {
119
+ ip = ip.slice(7);
120
+ }
121
+
122
+ const parts = ip.split('.').map(part => parseInt(part, 10));
123
+ return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
124
+ }
125
+
126
+ /**
127
+ * Match an IP against a CIDR pattern
128
+ *
129
+ * @param cidr CIDR pattern (e.g., "192.168.1.0/24")
130
+ * @param ip IP to match against the pattern
131
+ * @returns Whether the IP is in the CIDR range
132
+ */
133
+ export function matchIpCidr(cidr: string, ip: string): boolean {
134
+ const parsed = parseCidr(cidr);
135
+ if (!parsed) {
136
+ return false;
137
+ }
138
+
139
+ try {
140
+ const { subnet, bits } = parsed;
141
+
142
+ // Normalize IPv6-mapped IPv4 addresses
143
+ const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
144
+ const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet;
145
+
146
+ // Convert IP addresses to numeric values
147
+ const ipNum = ipToNumber(normalizedIp);
148
+ const subnetNum = ipToNumber(normalizedSubnet);
149
+
150
+ // Calculate subnet mask
151
+ const maskNum = ~(2 ** (32 - bits) - 1);
152
+
153
+ // Check if IP is in subnet
154
+ return (ipNum & maskNum) === (subnetNum & maskNum);
155
+ } catch (e) {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Match an IP pattern against an IP
162
+ *
163
+ * @param pattern IP pattern (exact, CIDR, or with wildcards)
164
+ * @param ip IP to match against the pattern
165
+ * @returns Whether the IP matches the pattern
166
+ */
167
+ export function matchIpPattern(pattern: string, ip: string): boolean {
168
+ // Normalize IPv6-mapped IPv4 addresses
169
+ const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
170
+ const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
171
+
172
+ // Handle exact match with all variations
173
+ if (pattern === ip || normalizedPattern === normalizedIp ||
174
+ pattern === normalizedIp || normalizedPattern === ip) {
175
+ return true;
176
+ }
177
+
178
+ // Handle "all" wildcard
179
+ if (pattern === '*' || normalizedPattern === '*') {
180
+ return true;
181
+ }
182
+
183
+ // Handle CIDR notation (e.g., 192.168.1.0/24)
184
+ if (pattern.includes('/')) {
185
+ return matchIpCidr(pattern, normalizedIp) ||
186
+ (normalizedPattern !== pattern && matchIpCidr(normalizedPattern, normalizedIp));
187
+ }
188
+
189
+ // Handle glob pattern (e.g., 192.168.1.*)
190
+ if (pattern.includes('*')) {
191
+ const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
192
+ const regex = new RegExp(`^${regexPattern}$`);
193
+ if (regex.test(ip) || regex.test(normalizedIp)) {
194
+ return true;
195
+ }
196
+
197
+ // If pattern was normalized, also test with normalized pattern
198
+ if (normalizedPattern !== pattern) {
199
+ const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
200
+ const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`);
201
+ return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp);
202
+ }
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * Match an IP against allowed and blocked IP patterns
210
+ *
211
+ * @param ip IP to check
212
+ * @param allowedIps Array of allowed IP patterns
213
+ * @param blockedIps Array of blocked IP patterns
214
+ * @returns Whether the IP is allowed
215
+ */
216
+ export function isIpAuthorized(
217
+ ip: string,
218
+ allowedIps: string[] = ['*'],
219
+ blockedIps: string[] = []
220
+ ): boolean {
221
+ // Check blocked IPs first
222
+ if (blockedIps.length > 0) {
223
+ for (const pattern of blockedIps) {
224
+ if (matchIpPattern(pattern, ip)) {
225
+ return false; // IP is blocked
226
+ }
227
+ }
228
+ }
229
+
230
+ // If there are allowed IPs, check them
231
+ if (allowedIps.length > 0) {
232
+ // Special case: if '*' is in allowed IPs, all non-blocked IPs are allowed
233
+ if (allowedIps.includes('*')) {
234
+ return true;
235
+ }
236
+
237
+ for (const pattern of allowedIps) {
238
+ if (matchIpPattern(pattern, ip)) {
239
+ return true; // IP is allowed
240
+ }
241
+ }
242
+ return false; // IP not in allowed list
243
+ }
244
+
245
+ // No allowed IPs specified, so IP is allowed by default
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * Match an HTTP header pattern against a header value
251
+ *
252
+ * @param pattern Expected header value (string or RegExp)
253
+ * @param value Actual header value
254
+ * @returns Whether the header matches the pattern
255
+ */
256
+ export function matchHeader(pattern: string | RegExp, value: string): boolean {
257
+ if (typeof pattern === 'string') {
258
+ return pattern === value;
259
+ } else if (pattern instanceof RegExp) {
260
+ return pattern.test(value);
261
+ }
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Calculate route specificity score
267
+ * Higher score means more specific matching criteria
268
+ *
269
+ * @param match Match criteria to evaluate
270
+ * @returns Numeric specificity score
271
+ */
272
+ export function calculateRouteSpecificity(match: {
273
+ domains?: string | string[];
274
+ path?: string;
275
+ clientIp?: string[];
276
+ tlsVersion?: string[];
277
+ headers?: Record<string, string | RegExp>;
278
+ }): number {
279
+ let score = 0;
280
+
281
+ // Path is very specific
282
+ if (match.path) {
283
+ // More specific if it doesn't use wildcards
284
+ score += match.path.includes('*') ? 3 : 4;
285
+ }
286
+
287
+ // Domain is next most specific
288
+ if (match.domains) {
289
+ const domains = Array.isArray(match.domains) ? match.domains : [match.domains];
290
+ // More domains or more specific domains (without wildcards) increase specificity
291
+ score += domains.length;
292
+ // Add bonus for exact domains (without wildcards)
293
+ score += domains.some(d => !d.includes('*')) ? 1 : 0;
294
+ }
295
+
296
+ // Headers are quite specific
297
+ if (match.headers) {
298
+ score += Object.keys(match.headers).length * 2;
299
+ }
300
+
301
+ // Client IP adds some specificity
302
+ if (match.clientIp && match.clientIp.length > 0) {
303
+ score += 1;
304
+ }
305
+
306
+ // TLS version adds minimal specificity
307
+ if (match.tlsVersion && match.tlsVersion.length > 0) {
308
+ score += 1;
309
+ }
310
+
311
+ return score;
312
+ }
@@ -0,0 +1,309 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import {
3
+ matchIpPattern,
4
+ ipToNumber,
5
+ matchIpCidr
6
+ } from './route-utils.js';
7
+
8
+ /**
9
+ * Security utilities for IP validation, rate limiting,
10
+ * authentication, and other security features
11
+ */
12
+
13
+ /**
14
+ * Result of IP validation
15
+ */
16
+ export interface IIpValidationResult {
17
+ allowed: boolean;
18
+ reason?: string;
19
+ }
20
+
21
+ /**
22
+ * IP connection tracking information
23
+ */
24
+ export interface IIpConnectionInfo {
25
+ connections: Set<string>; // ConnectionIDs
26
+ timestamps: number[]; // Connection timestamps
27
+ ipVariants: string[]; // Normalized IP variants (e.g., ::ffff:127.0.0.1 and 127.0.0.1)
28
+ }
29
+
30
+ /**
31
+ * Rate limit tracking
32
+ */
33
+ export interface IRateLimitInfo {
34
+ count: number;
35
+ expiry: number;
36
+ }
37
+
38
+ /**
39
+ * Logger interface for security utilities
40
+ */
41
+ export interface ISecurityLogger {
42
+ info: (message: string, ...args: any[]) => void;
43
+ warn: (message: string, ...args: any[]) => void;
44
+ error: (message: string, ...args: any[]) => void;
45
+ debug?: (message: string, ...args: any[]) => void;
46
+ }
47
+
48
+ /**
49
+ * Normalize IP addresses for comparison
50
+ * Handles IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
51
+ *
52
+ * @param ip IP address to normalize
53
+ * @returns Array of equivalent IP representations
54
+ */
55
+ export function normalizeIP(ip: string): string[] {
56
+ if (!ip) return [];
57
+
58
+ // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1)
59
+ if (ip.startsWith('::ffff:')) {
60
+ const ipv4 = ip.slice(7);
61
+ return [ip, ipv4];
62
+ }
63
+
64
+ // Handle IPv4 addresses by also checking IPv4-mapped form
65
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
66
+ return [ip, `::ffff:${ip}`];
67
+ }
68
+
69
+ return [ip];
70
+ }
71
+
72
+ /**
73
+ * Check if an IP is authorized based on allow and block lists
74
+ *
75
+ * @param ip - The IP address to check
76
+ * @param allowedIPs - Array of allowed IP patterns
77
+ * @param blockedIPs - Array of blocked IP patterns
78
+ * @returns Whether the IP is authorized
79
+ */
80
+ export function isIPAuthorized(
81
+ ip: string,
82
+ allowedIPs: string[] = ['*'],
83
+ blockedIPs: string[] = []
84
+ ): boolean {
85
+ // Skip IP validation if no rules
86
+ if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) {
87
+ return true;
88
+ }
89
+
90
+ // First check if IP is blocked - blocked IPs take precedence
91
+ if (blockedIPs.length > 0) {
92
+ for (const pattern of blockedIPs) {
93
+ if (matchIpPattern(pattern, ip)) {
94
+ return false;
95
+ }
96
+ }
97
+ }
98
+
99
+ // If allowed IPs list has wildcard, all non-blocked IPs are allowed
100
+ if (allowedIPs.includes('*')) {
101
+ return true;
102
+ }
103
+
104
+ // Then check if IP is allowed in the explicit allow list
105
+ if (allowedIPs.length > 0) {
106
+ for (const pattern of allowedIPs) {
107
+ if (matchIpPattern(pattern, ip)) {
108
+ return true;
109
+ }
110
+ }
111
+ // If allowedIPs is specified but no match, deny access
112
+ return false;
113
+ }
114
+
115
+ // Default allow if no explicit allow list
116
+ return true;
117
+ }
118
+
119
+ /**
120
+ * Check if an IP exceeds maximum connections
121
+ *
122
+ * @param ip - The IP address to check
123
+ * @param ipConnectionsMap - Map of IPs to connection info
124
+ * @param maxConnectionsPerIP - Maximum allowed connections per IP
125
+ * @returns Result with allowed status and reason if blocked
126
+ */
127
+ export function checkMaxConnections(
128
+ ip: string,
129
+ ipConnectionsMap: Map<string, IIpConnectionInfo>,
130
+ maxConnectionsPerIP: number
131
+ ): IIpValidationResult {
132
+ if (!ipConnectionsMap.has(ip)) {
133
+ return { allowed: true };
134
+ }
135
+
136
+ const connectionCount = ipConnectionsMap.get(ip)!.connections.size;
137
+
138
+ if (connectionCount >= maxConnectionsPerIP) {
139
+ return {
140
+ allowed: false,
141
+ reason: `Maximum connections per IP (${maxConnectionsPerIP}) exceeded`
142
+ };
143
+ }
144
+
145
+ return { allowed: true };
146
+ }
147
+
148
+ /**
149
+ * Check if an IP exceeds connection rate limit
150
+ *
151
+ * @param ip - The IP address to check
152
+ * @param ipConnectionsMap - Map of IPs to connection info
153
+ * @param rateLimit - Maximum connections per minute
154
+ * @returns Result with allowed status and reason if blocked
155
+ */
156
+ export function checkConnectionRate(
157
+ ip: string,
158
+ ipConnectionsMap: Map<string, IIpConnectionInfo>,
159
+ rateLimit: number
160
+ ): IIpValidationResult {
161
+ const now = Date.now();
162
+ const minute = 60 * 1000;
163
+
164
+ // Get or create connection info
165
+ if (!ipConnectionsMap.has(ip)) {
166
+ const info: IIpConnectionInfo = {
167
+ connections: new Set(),
168
+ timestamps: [now],
169
+ ipVariants: normalizeIP(ip)
170
+ };
171
+ ipConnectionsMap.set(ip, info);
172
+ return { allowed: true };
173
+ }
174
+
175
+ // Get timestamps and filter out entries older than 1 minute
176
+ const info = ipConnectionsMap.get(ip)!;
177
+ const timestamps = info.timestamps.filter(time => now - time < minute);
178
+ timestamps.push(now);
179
+ info.timestamps = timestamps;
180
+
181
+ // Check if rate exceeds limit
182
+ if (timestamps.length > rateLimit) {
183
+ return {
184
+ allowed: false,
185
+ reason: `Connection rate limit (${rateLimit}/min) exceeded`
186
+ };
187
+ }
188
+
189
+ return { allowed: true };
190
+ }
191
+
192
+ /**
193
+ * Track a connection for an IP
194
+ *
195
+ * @param ip - The IP address
196
+ * @param connectionId - The connection ID to track
197
+ * @param ipConnectionsMap - Map of IPs to connection info
198
+ */
199
+ export function trackConnection(
200
+ ip: string,
201
+ connectionId: string,
202
+ ipConnectionsMap: Map<string, IIpConnectionInfo>
203
+ ): void {
204
+ if (!ipConnectionsMap.has(ip)) {
205
+ ipConnectionsMap.set(ip, {
206
+ connections: new Set([connectionId]),
207
+ timestamps: [Date.now()],
208
+ ipVariants: normalizeIP(ip)
209
+ });
210
+ return;
211
+ }
212
+
213
+ const info = ipConnectionsMap.get(ip)!;
214
+ info.connections.add(connectionId);
215
+ }
216
+
217
+ /**
218
+ * Remove connection tracking for an IP
219
+ *
220
+ * @param ip - The IP address
221
+ * @param connectionId - The connection ID to remove
222
+ * @param ipConnectionsMap - Map of IPs to connection info
223
+ */
224
+ export function removeConnection(
225
+ ip: string,
226
+ connectionId: string,
227
+ ipConnectionsMap: Map<string, IIpConnectionInfo>
228
+ ): void {
229
+ if (!ipConnectionsMap.has(ip)) return;
230
+
231
+ const info = ipConnectionsMap.get(ip)!;
232
+ info.connections.delete(connectionId);
233
+
234
+ if (info.connections.size === 0) {
235
+ ipConnectionsMap.delete(ip);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Clean up expired rate limits
241
+ *
242
+ * @param rateLimits - Map of rate limits to clean up
243
+ * @param logger - Logger for debug messages
244
+ */
245
+ export function cleanupExpiredRateLimits(
246
+ rateLimits: Map<string, Map<string, IRateLimitInfo>>,
247
+ logger?: ISecurityLogger
248
+ ): void {
249
+ const now = Date.now();
250
+ let totalRemoved = 0;
251
+
252
+ for (const [routeId, routeLimits] of rateLimits.entries()) {
253
+ let removed = 0;
254
+ for (const [key, limit] of routeLimits.entries()) {
255
+ if (limit.expiry < now) {
256
+ routeLimits.delete(key);
257
+ removed++;
258
+ totalRemoved++;
259
+ }
260
+ }
261
+
262
+ if (removed > 0 && logger?.debug) {
263
+ logger.debug(`Cleaned up ${removed} expired rate limits for route ${routeId}`);
264
+ }
265
+ }
266
+
267
+ if (totalRemoved > 0 && logger?.info) {
268
+ logger.info(`Cleaned up ${totalRemoved} expired rate limits total`);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Generate basic auth header value from username and password
274
+ *
275
+ * @param username - The username
276
+ * @param password - The password
277
+ * @returns Base64 encoded basic auth string
278
+ */
279
+ export function generateBasicAuthHeader(username: string, password: string): string {
280
+ return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
281
+ }
282
+
283
+ /**
284
+ * Parse basic auth header
285
+ *
286
+ * @param authHeader - The Authorization header value
287
+ * @returns Username and password, or null if invalid
288
+ */
289
+ export function parseBasicAuthHeader(
290
+ authHeader: string
291
+ ): { username: string; password: string } | null {
292
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
293
+ return null;
294
+ }
295
+
296
+ try {
297
+ const base64 = authHeader.slice(6); // Remove 'Basic '
298
+ const decoded = Buffer.from(base64, 'base64').toString();
299
+ const [username, password] = decoded.split(':');
300
+
301
+ if (!username || !password) {
302
+ return null;
303
+ }
304
+
305
+ return { username, password };
306
+ } catch (err) {
307
+ return null;
308
+ }
309
+ }