@push.rocks/smartproxy 22.4.2 → 23.0.0
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/changelog.md +36 -0
- package/dist_rust/rustproxy +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -6
- package/dist_ts/index.js +3 -11
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/index.d.ts +1 -6
- package/dist_ts/proxies/index.js +2 -8
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/package.json +1 -1
- package/readme.md +444 -219
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -15
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -12
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
- package/ts/proxies/smart-proxy/models/route-types.ts +0 -2
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/dist_ts/proxies/nftables-proxy/index.d.ts +0 -6
- package/dist_ts/proxies/nftables-proxy/index.js +0 -7
- package/dist_ts/proxies/nftables-proxy/models/errors.d.ts +0 -15
- package/dist_ts/proxies/nftables-proxy/models/errors.js +0 -28
- package/dist_ts/proxies/nftables-proxy/models/index.d.ts +0 -5
- package/dist_ts/proxies/nftables-proxy/models/index.js +0 -6
- package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +0 -75
- package/dist_ts/proxies/nftables-proxy/models/interfaces.js +0 -5
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +0 -124
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +0 -1374
- package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +0 -9
- package/dist_ts/proxies/nftables-proxy/utils/index.js +0 -12
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +0 -66
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +0 -131
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +0 -39
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +0 -112
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +0 -59
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +0 -130
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/default-certificates.ts +0 -150
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -669
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -18
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -413
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/nftables-proxy/index.ts +0 -6
- package/ts/proxies/nftables-proxy/models/errors.ts +0 -30
- package/ts/proxies/nftables-proxy/models/index.ts +0 -5
- package/ts/proxies/nftables-proxy/models/interfaces.ts +0 -94
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +0 -1754
- package/ts/proxies/nftables-proxy/utils/index.ts +0 -38
- package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +0 -162
- package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +0 -125
- package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +0 -156
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -269
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { logger } from '../../core/utils/logger.js';
|
|
2
|
-
import type { IRouteConfig } from './models/route-types.js';
|
|
3
|
-
import type { ILogger } from '../http-proxy/models/types.js';
|
|
4
|
-
import { RouteValidator } from './utils/route-validator.js';
|
|
5
|
-
import { Mutex } from './utils/mutex.js';
|
|
6
|
-
import type { PortManager } from './port-manager.js';
|
|
7
|
-
import type { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
|
8
|
-
import type { HttpProxyBridge } from './http-proxy-bridge.js';
|
|
9
|
-
import type { NFTablesManager } from './nftables-manager.js';
|
|
10
|
-
import type { SmartCertManager } from './certificate-manager.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Orchestrates route updates and coordination between components
|
|
14
|
-
* Extracted from SmartProxy to reduce class complexity
|
|
15
|
-
*/
|
|
16
|
-
export class RouteOrchestrator {
|
|
17
|
-
private routeUpdateLock: Mutex;
|
|
18
|
-
private portManager: PortManager;
|
|
19
|
-
private routeManager: RouteManager;
|
|
20
|
-
private httpProxyBridge: HttpProxyBridge;
|
|
21
|
-
private nftablesManager: NFTablesManager;
|
|
22
|
-
private certManager: SmartCertManager | null = null;
|
|
23
|
-
private logger: ILogger;
|
|
24
|
-
|
|
25
|
-
constructor(
|
|
26
|
-
portManager: PortManager,
|
|
27
|
-
routeManager: RouteManager,
|
|
28
|
-
httpProxyBridge: HttpProxyBridge,
|
|
29
|
-
nftablesManager: NFTablesManager,
|
|
30
|
-
certManager: SmartCertManager | null,
|
|
31
|
-
logger: ILogger
|
|
32
|
-
) {
|
|
33
|
-
this.portManager = portManager;
|
|
34
|
-
this.routeManager = routeManager;
|
|
35
|
-
this.httpProxyBridge = httpProxyBridge;
|
|
36
|
-
this.nftablesManager = nftablesManager;
|
|
37
|
-
this.certManager = certManager;
|
|
38
|
-
this.logger = logger;
|
|
39
|
-
this.routeUpdateLock = new Mutex();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Set or update certificate manager reference
|
|
44
|
-
*/
|
|
45
|
-
public setCertManager(certManager: SmartCertManager | null): void {
|
|
46
|
-
this.certManager = certManager;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get certificate manager reference
|
|
51
|
-
*/
|
|
52
|
-
public getCertManager(): SmartCertManager | null {
|
|
53
|
-
return this.certManager;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Update routes with validation and coordination
|
|
58
|
-
*/
|
|
59
|
-
public async updateRoutes(
|
|
60
|
-
oldRoutes: IRouteConfig[],
|
|
61
|
-
newRoutes: IRouteConfig[],
|
|
62
|
-
options: {
|
|
63
|
-
acmePort?: number;
|
|
64
|
-
acmeOptions?: any;
|
|
65
|
-
acmeState?: any;
|
|
66
|
-
globalChallengeRouteActive?: boolean;
|
|
67
|
-
createCertificateManager?: (
|
|
68
|
-
routes: IRouteConfig[],
|
|
69
|
-
certStore: string,
|
|
70
|
-
acmeOptions?: any,
|
|
71
|
-
initialState?: any
|
|
72
|
-
) => Promise<SmartCertManager>;
|
|
73
|
-
verifyChallengeRouteRemoved?: () => Promise<void>;
|
|
74
|
-
} = {}
|
|
75
|
-
): Promise<{
|
|
76
|
-
portUsageMap: Map<number, Set<string>>;
|
|
77
|
-
newChallengeRouteActive: boolean;
|
|
78
|
-
newCertManager?: SmartCertManager;
|
|
79
|
-
}> {
|
|
80
|
-
return this.routeUpdateLock.runExclusive(async () => {
|
|
81
|
-
// Validate route configurations
|
|
82
|
-
const validation = RouteValidator.validateRoutes(newRoutes);
|
|
83
|
-
if (!validation.valid) {
|
|
84
|
-
RouteValidator.logValidationErrors(validation.errors);
|
|
85
|
-
throw new Error(`Route validation failed: ${validation.errors.size} route(s) have errors`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Track port usage before and after updates
|
|
89
|
-
const oldPortUsage = this.updatePortUsageMap(oldRoutes);
|
|
90
|
-
const newPortUsage = this.updatePortUsageMap(newRoutes);
|
|
91
|
-
|
|
92
|
-
// Get the lists of currently listening ports and new ports needed
|
|
93
|
-
const currentPorts = new Set(this.portManager.getListeningPorts());
|
|
94
|
-
const newPortsSet = new Set(newPortUsage.keys());
|
|
95
|
-
|
|
96
|
-
// Log the port usage for debugging
|
|
97
|
-
this.logger.debug(`Current listening ports: ${Array.from(currentPorts).join(', ')}`);
|
|
98
|
-
this.logger.debug(`Ports needed for new routes: ${Array.from(newPortsSet).join(', ')}`);
|
|
99
|
-
|
|
100
|
-
// Find orphaned ports - ports that no longer have any routes
|
|
101
|
-
const orphanedPorts = this.findOrphanedPorts(oldPortUsage, newPortUsage);
|
|
102
|
-
|
|
103
|
-
// Find new ports that need binding (only ports that we aren't already listening on)
|
|
104
|
-
const newBindingPorts = Array.from(newPortsSet).filter(p => !currentPorts.has(p));
|
|
105
|
-
|
|
106
|
-
// Check for ACME challenge port to give it special handling
|
|
107
|
-
const acmePort = options.acmePort || 80;
|
|
108
|
-
const acmePortNeeded = newPortsSet.has(acmePort);
|
|
109
|
-
const acmePortListed = newBindingPorts.includes(acmePort);
|
|
110
|
-
|
|
111
|
-
if (acmePortNeeded && acmePortListed) {
|
|
112
|
-
this.logger.info(`Adding ACME challenge port ${acmePort} to routes`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Update NFTables routes
|
|
116
|
-
await this.updateNfTablesRoutes(oldRoutes, newRoutes);
|
|
117
|
-
|
|
118
|
-
// Update routes in RouteManager
|
|
119
|
-
this.routeManager.updateRoutes(newRoutes);
|
|
120
|
-
|
|
121
|
-
// Release orphaned ports first to free resources
|
|
122
|
-
if (orphanedPorts.length > 0) {
|
|
123
|
-
this.logger.info(`Releasing ${orphanedPorts.length} orphaned ports: ${orphanedPorts.join(', ')}`);
|
|
124
|
-
await this.portManager.removePorts(orphanedPorts);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Add new ports if needed
|
|
128
|
-
if (newBindingPorts.length > 0) {
|
|
129
|
-
this.logger.info(`Binding to ${newBindingPorts.length} new ports: ${newBindingPorts.join(', ')}`);
|
|
130
|
-
|
|
131
|
-
// Handle port binding with improved error recovery
|
|
132
|
-
try {
|
|
133
|
-
await this.portManager.addPorts(newBindingPorts);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
// Special handling for port binding errors
|
|
136
|
-
if ((error as any).code === 'EADDRINUSE') {
|
|
137
|
-
const port = (error as any).port || newBindingPorts[0];
|
|
138
|
-
const isAcmePort = port === acmePort;
|
|
139
|
-
|
|
140
|
-
if (isAcmePort) {
|
|
141
|
-
this.logger.warn(`Could not bind to ACME challenge port ${port}. It may be in use by another application.`);
|
|
142
|
-
|
|
143
|
-
// Re-throw with more helpful message
|
|
144
|
-
throw new Error(
|
|
145
|
-
`ACME challenge port ${port} is already in use by another application. ` +
|
|
146
|
-
`Configure a different port in settings.acme.port (e.g., 8080) or free up port ${port}.`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Re-throw the original error for other cases
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// If HttpProxy is initialized, resync the configurations
|
|
157
|
-
if (this.httpProxyBridge.getHttpProxy()) {
|
|
158
|
-
await this.httpProxyBridge.syncRoutesToHttpProxy(newRoutes);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Update certificate manager if needed
|
|
162
|
-
let newCertManager: SmartCertManager | undefined;
|
|
163
|
-
let newChallengeRouteActive = options.globalChallengeRouteActive || false;
|
|
164
|
-
|
|
165
|
-
if (this.certManager && options.createCertificateManager) {
|
|
166
|
-
const existingAcmeOptions = this.certManager.getAcmeOptions();
|
|
167
|
-
const existingState = this.certManager.getState();
|
|
168
|
-
|
|
169
|
-
// Store global state before stopping
|
|
170
|
-
newChallengeRouteActive = existingState.challengeRouteActive;
|
|
171
|
-
|
|
172
|
-
// Keep certificate manager routes in sync before stopping
|
|
173
|
-
this.certManager.setRoutes(newRoutes);
|
|
174
|
-
|
|
175
|
-
await this.certManager.stop();
|
|
176
|
-
|
|
177
|
-
// Verify the challenge route has been properly removed
|
|
178
|
-
if (options.verifyChallengeRouteRemoved) {
|
|
179
|
-
await options.verifyChallengeRouteRemoved();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Create new certificate manager with preserved state
|
|
183
|
-
newCertManager = await options.createCertificateManager(
|
|
184
|
-
newRoutes,
|
|
185
|
-
'./certs',
|
|
186
|
-
existingAcmeOptions,
|
|
187
|
-
{ challengeRouteActive: newChallengeRouteActive }
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
this.certManager = newCertManager;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
portUsageMap: newPortUsage,
|
|
195
|
-
newChallengeRouteActive,
|
|
196
|
-
newCertManager
|
|
197
|
-
};
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Update port usage map based on the provided routes
|
|
203
|
-
*/
|
|
204
|
-
public updatePortUsageMap(routes: IRouteConfig[]): Map<number, Set<string>> {
|
|
205
|
-
const portUsage = new Map<number, Set<string>>();
|
|
206
|
-
|
|
207
|
-
for (const route of routes) {
|
|
208
|
-
// Get the ports for this route
|
|
209
|
-
const portsConfig = Array.isArray(route.match.ports)
|
|
210
|
-
? route.match.ports
|
|
211
|
-
: [route.match.ports];
|
|
212
|
-
|
|
213
|
-
// Expand port range objects to individual port numbers
|
|
214
|
-
const expandedPorts: number[] = [];
|
|
215
|
-
for (const portConfig of portsConfig) {
|
|
216
|
-
if (typeof portConfig === 'number') {
|
|
217
|
-
expandedPorts.push(portConfig);
|
|
218
|
-
} else if (typeof portConfig === 'object' && 'from' in portConfig && 'to' in portConfig) {
|
|
219
|
-
// Expand the port range
|
|
220
|
-
for (let p = portConfig.from; p <= portConfig.to; p++) {
|
|
221
|
-
expandedPorts.push(p);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Use route name if available, otherwise generate a unique ID
|
|
227
|
-
const routeName = route.name || `unnamed_${Math.random().toString(36).substring(2, 9)}`;
|
|
228
|
-
|
|
229
|
-
// Add each port to the usage map
|
|
230
|
-
for (const port of expandedPorts) {
|
|
231
|
-
if (!portUsage.has(port)) {
|
|
232
|
-
portUsage.set(port, new Set());
|
|
233
|
-
}
|
|
234
|
-
portUsage.get(port)!.add(routeName);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Log port usage for debugging
|
|
239
|
-
for (const [port, routes] of portUsage.entries()) {
|
|
240
|
-
this.logger.debug(`Port ${port} is used by ${routes.size} routes: ${Array.from(routes).join(', ')}`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return portUsage;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Find ports that have no routes in the new configuration
|
|
248
|
-
*/
|
|
249
|
-
private findOrphanedPorts(oldUsage: Map<number, Set<string>>, newUsage: Map<number, Set<string>>): number[] {
|
|
250
|
-
const orphanedPorts: number[] = [];
|
|
251
|
-
|
|
252
|
-
for (const [port, routes] of oldUsage.entries()) {
|
|
253
|
-
if (!newUsage.has(port) || newUsage.get(port)!.size === 0) {
|
|
254
|
-
orphanedPorts.push(port);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return orphanedPorts;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Update NFTables routes
|
|
263
|
-
*/
|
|
264
|
-
private async updateNfTablesRoutes(oldRoutes: IRouteConfig[], newRoutes: IRouteConfig[]): Promise<void> {
|
|
265
|
-
// Get existing routes that use NFTables and update them
|
|
266
|
-
const oldNfTablesRoutes = oldRoutes.filter(
|
|
267
|
-
r => r.action.forwardingEngine === 'nftables'
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const newNfTablesRoutes = newRoutes.filter(
|
|
271
|
-
r => r.action.forwardingEngine === 'nftables'
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// Update existing NFTables routes
|
|
275
|
-
for (const oldRoute of oldNfTablesRoutes) {
|
|
276
|
-
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
|
277
|
-
|
|
278
|
-
if (!newRoute) {
|
|
279
|
-
// Route was removed
|
|
280
|
-
await this.nftablesManager.deprovisionRoute(oldRoute);
|
|
281
|
-
} else {
|
|
282
|
-
// Route was updated
|
|
283
|
-
await this.nftablesManager.updateRoute(oldRoute, newRoute);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Add new NFTables routes
|
|
288
|
-
for (const newRoute of newNfTablesRoutes) {
|
|
289
|
-
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
|
290
|
-
|
|
291
|
-
if (!oldRoute) {
|
|
292
|
-
// New route
|
|
293
|
-
await this.nftablesManager.provisionRoute(newRoute);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import type { SmartProxy } from './smart-proxy.js';
|
|
3
|
-
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
|
4
|
-
import { isIPAuthorized, normalizeIP } from '../../core/utils/security-utils.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Handles security aspects like IP tracking, rate limiting, and authorization
|
|
8
|
-
* for SmartProxy. This is a lightweight wrapper that uses shared utilities.
|
|
9
|
-
*/
|
|
10
|
-
export class SecurityManager {
|
|
11
|
-
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
12
|
-
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
13
|
-
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
14
|
-
|
|
15
|
-
constructor(private smartProxy: SmartProxy) {
|
|
16
|
-
// Start periodic cleanup every 60 seconds
|
|
17
|
-
this.startPeriodicCleanup();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get connections count by IP (checks normalized variants)
|
|
22
|
-
*/
|
|
23
|
-
public getConnectionCountByIP(ip: string): number {
|
|
24
|
-
// Check all normalized variants of the IP
|
|
25
|
-
const variants = normalizeIP(ip);
|
|
26
|
-
for (const variant of variants) {
|
|
27
|
-
const connections = this.connectionsByIP.get(variant);
|
|
28
|
-
if (connections) {
|
|
29
|
-
return connections.size;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Check and update connection rate for an IP
|
|
37
|
-
* @returns true if within rate limit, false if exceeding limit
|
|
38
|
-
*/
|
|
39
|
-
public checkConnectionRate(ip: string): boolean {
|
|
40
|
-
const now = Date.now();
|
|
41
|
-
const minute = 60 * 1000;
|
|
42
|
-
|
|
43
|
-
// Find existing rate tracking (check normalized variants)
|
|
44
|
-
const variants = normalizeIP(ip);
|
|
45
|
-
let existingKey: string | null = null;
|
|
46
|
-
for (const variant of variants) {
|
|
47
|
-
if (this.connectionRateByIP.has(variant)) {
|
|
48
|
-
existingKey = variant;
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const key = existingKey || ip;
|
|
54
|
-
|
|
55
|
-
if (!this.connectionRateByIP.has(key)) {
|
|
56
|
-
this.connectionRateByIP.set(key, [now]);
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Get timestamps and filter out entries older than 1 minute
|
|
61
|
-
const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute);
|
|
62
|
-
timestamps.push(now);
|
|
63
|
-
this.connectionRateByIP.set(key, timestamps);
|
|
64
|
-
|
|
65
|
-
// Check if rate exceeds limit
|
|
66
|
-
return timestamps.length <= this.smartProxy.settings.connectionRateLimitPerMinute!;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Track connection by IP
|
|
71
|
-
*/
|
|
72
|
-
public trackConnectionByIP(ip: string, connectionId: string): void {
|
|
73
|
-
// Check if any variant already exists
|
|
74
|
-
const variants = normalizeIP(ip);
|
|
75
|
-
let existingKey: string | null = null;
|
|
76
|
-
|
|
77
|
-
for (const variant of variants) {
|
|
78
|
-
if (this.connectionsByIP.has(variant)) {
|
|
79
|
-
existingKey = variant;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const key = existingKey || ip;
|
|
85
|
-
if (!this.connectionsByIP.has(key)) {
|
|
86
|
-
this.connectionsByIP.set(key, new Set());
|
|
87
|
-
}
|
|
88
|
-
this.connectionsByIP.get(key)!.add(connectionId);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Remove connection tracking for an IP
|
|
93
|
-
*/
|
|
94
|
-
public removeConnectionByIP(ip: string, connectionId: string): void {
|
|
95
|
-
// Check all variants to find where the connection is tracked
|
|
96
|
-
const variants = normalizeIP(ip);
|
|
97
|
-
|
|
98
|
-
for (const variant of variants) {
|
|
99
|
-
if (this.connectionsByIP.has(variant)) {
|
|
100
|
-
const connections = this.connectionsByIP.get(variant)!;
|
|
101
|
-
connections.delete(connectionId);
|
|
102
|
-
if (connections.size === 0) {
|
|
103
|
-
this.connectionsByIP.delete(variant);
|
|
104
|
-
}
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Check if an IP is authorized using security rules
|
|
112
|
-
*
|
|
113
|
-
* This method is used to determine if an IP is allowed to connect, based on security
|
|
114
|
-
* rules configured in the route configuration. The allowed and blocked IPs are
|
|
115
|
-
* typically derived from route.security.ipAllowList and ipBlockList.
|
|
116
|
-
*
|
|
117
|
-
* @param ip - The IP address to check
|
|
118
|
-
* @param allowedIPs - Array of allowed IP patterns from security.ipAllowList
|
|
119
|
-
* @param blockedIPs - Array of blocked IP patterns from security.ipBlockList
|
|
120
|
-
* @returns true if IP is authorized, false if blocked
|
|
121
|
-
*/
|
|
122
|
-
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
|
|
123
|
-
return isIPAuthorized(ip, allowedIPs, blockedIPs);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check if IP should be allowed considering connection rate and max connections
|
|
128
|
-
* @returns Object with result and reason
|
|
129
|
-
*/
|
|
130
|
-
public validateIP(ip: string): { allowed: boolean; reason?: string } {
|
|
131
|
-
// Check connection count limit
|
|
132
|
-
if (
|
|
133
|
-
this.smartProxy.settings.maxConnectionsPerIP &&
|
|
134
|
-
this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
|
|
135
|
-
) {
|
|
136
|
-
return {
|
|
137
|
-
allowed: false,
|
|
138
|
-
reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded`
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Check connection rate limit
|
|
143
|
-
if (
|
|
144
|
-
this.smartProxy.settings.connectionRateLimitPerMinute &&
|
|
145
|
-
!this.checkConnectionRate(ip)
|
|
146
|
-
) {
|
|
147
|
-
return {
|
|
148
|
-
allowed: false,
|
|
149
|
-
reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { allowed: true };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Atomically validate an IP and track the connection if allowed.
|
|
158
|
-
* This prevents race conditions where concurrent connections could bypass per-IP limits.
|
|
159
|
-
*
|
|
160
|
-
* @param ip - The IP address to validate
|
|
161
|
-
* @param connectionId - The connection ID to track if validation passes
|
|
162
|
-
* @returns Object with validation result and reason
|
|
163
|
-
*/
|
|
164
|
-
public validateAndTrackIP(ip: string, connectionId: string): { allowed: boolean; reason?: string } {
|
|
165
|
-
// Check connection count limit BEFORE tracking
|
|
166
|
-
if (
|
|
167
|
-
this.smartProxy.settings.maxConnectionsPerIP &&
|
|
168
|
-
this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP
|
|
169
|
-
) {
|
|
170
|
-
return {
|
|
171
|
-
allowed: false,
|
|
172
|
-
reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded`
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Check connection rate limit
|
|
177
|
-
if (
|
|
178
|
-
this.smartProxy.settings.connectionRateLimitPerMinute &&
|
|
179
|
-
!this.checkConnectionRate(ip)
|
|
180
|
-
) {
|
|
181
|
-
return {
|
|
182
|
-
allowed: false,
|
|
183
|
-
reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded`
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Validation passed - immediately track to prevent race conditions
|
|
188
|
-
this.trackConnectionByIP(ip, connectionId);
|
|
189
|
-
|
|
190
|
-
return { allowed: true };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Clears all IP tracking data (for shutdown)
|
|
195
|
-
*/
|
|
196
|
-
public clearIPTracking(): void {
|
|
197
|
-
if (this.cleanupInterval) {
|
|
198
|
-
clearInterval(this.cleanupInterval);
|
|
199
|
-
this.cleanupInterval = null;
|
|
200
|
-
}
|
|
201
|
-
this.connectionsByIP.clear();
|
|
202
|
-
this.connectionRateByIP.clear();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Start periodic cleanup of expired data
|
|
207
|
-
*/
|
|
208
|
-
private startPeriodicCleanup(): void {
|
|
209
|
-
this.cleanupInterval = setInterval(() => {
|
|
210
|
-
this.performCleanup();
|
|
211
|
-
}, 60000); // Run every minute
|
|
212
|
-
|
|
213
|
-
// Unref the timer so it doesn't keep the process alive
|
|
214
|
-
if (this.cleanupInterval.unref) {
|
|
215
|
-
this.cleanupInterval.unref();
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Perform cleanup of expired rate limits and empty IP entries
|
|
221
|
-
*/
|
|
222
|
-
private performCleanup(): void {
|
|
223
|
-
const now = Date.now();
|
|
224
|
-
const minute = 60 * 1000;
|
|
225
|
-
let cleanedRateLimits = 0;
|
|
226
|
-
let cleanedIPs = 0;
|
|
227
|
-
|
|
228
|
-
// Clean up expired rate limit timestamps
|
|
229
|
-
for (const [ip, timestamps] of this.connectionRateByIP.entries()) {
|
|
230
|
-
const validTimestamps = timestamps.filter(time => now - time < minute);
|
|
231
|
-
|
|
232
|
-
if (validTimestamps.length === 0) {
|
|
233
|
-
// No valid timestamps, remove the IP entry
|
|
234
|
-
this.connectionRateByIP.delete(ip);
|
|
235
|
-
cleanedRateLimits++;
|
|
236
|
-
} else if (validTimestamps.length < timestamps.length) {
|
|
237
|
-
// Some timestamps expired, update with valid ones
|
|
238
|
-
this.connectionRateByIP.set(ip, validTimestamps);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Clean up IPs with no active connections
|
|
243
|
-
for (const [ip, connections] of this.connectionsByIP.entries()) {
|
|
244
|
-
if (connections.size === 0) {
|
|
245
|
-
this.connectionsByIP.delete(ip);
|
|
246
|
-
cleanedIPs++;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Log cleanup stats if anything was cleaned
|
|
251
|
-
if (cleanedRateLimits > 0 || cleanedIPs > 0) {
|
|
252
|
-
if (this.smartProxy.settings.enableDetailedLogging) {
|
|
253
|
-
connectionLogDeduplicator.log(
|
|
254
|
-
'ip-cleanup',
|
|
255
|
-
'debug',
|
|
256
|
-
'IP tracking cleanup completed',
|
|
257
|
-
{
|
|
258
|
-
cleanedRateLimits,
|
|
259
|
-
cleanedIPs,
|
|
260
|
-
remainingIPs: this.connectionsByIP.size,
|
|
261
|
-
remainingRateLimits: this.connectionRateByIP.size,
|
|
262
|
-
component: 'security-manager'
|
|
263
|
-
},
|
|
264
|
-
'periodic-cleanup'
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { IThroughputSample, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tracks throughput data using time-series sampling
|
|
5
|
-
*/
|
|
6
|
-
export class ThroughputTracker {
|
|
7
|
-
private samples: IThroughputSample[] = [];
|
|
8
|
-
private readonly maxSamples: number;
|
|
9
|
-
private accumulatedBytesIn: number = 0;
|
|
10
|
-
private accumulatedBytesOut: number = 0;
|
|
11
|
-
private lastSampleTime: number = 0;
|
|
12
|
-
|
|
13
|
-
constructor(retentionSeconds: number = 3600) {
|
|
14
|
-
// Keep samples for the retention period at 1 sample per second
|
|
15
|
-
this.maxSamples = retentionSeconds;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Record bytes transferred (called on every data transfer)
|
|
20
|
-
*/
|
|
21
|
-
public recordBytes(bytesIn: number, bytesOut: number): void {
|
|
22
|
-
this.accumulatedBytesIn += bytesIn;
|
|
23
|
-
this.accumulatedBytesOut += bytesOut;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Take a sample of accumulated bytes (called every second)
|
|
28
|
-
*/
|
|
29
|
-
public takeSample(): void {
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
|
|
32
|
-
// Record accumulated bytes since last sample
|
|
33
|
-
this.samples.push({
|
|
34
|
-
timestamp: now,
|
|
35
|
-
bytesIn: this.accumulatedBytesIn,
|
|
36
|
-
bytesOut: this.accumulatedBytesOut
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Reset accumulators
|
|
40
|
-
this.accumulatedBytesIn = 0;
|
|
41
|
-
this.accumulatedBytesOut = 0;
|
|
42
|
-
this.lastSampleTime = now;
|
|
43
|
-
|
|
44
|
-
// Maintain circular buffer - remove oldest samples
|
|
45
|
-
if (this.samples.length > this.maxSamples) {
|
|
46
|
-
this.samples.shift();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get throughput rate over specified window (bytes per second)
|
|
52
|
-
*/
|
|
53
|
-
public getRate(windowSeconds: number): IThroughputData {
|
|
54
|
-
if (this.samples.length === 0) {
|
|
55
|
-
return { in: 0, out: 0 };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const now = Date.now();
|
|
59
|
-
const windowStart = now - (windowSeconds * 1000);
|
|
60
|
-
|
|
61
|
-
// Find samples within the window
|
|
62
|
-
const relevantSamples = this.samples.filter(s => s.timestamp > windowStart);
|
|
63
|
-
|
|
64
|
-
if (relevantSamples.length === 0) {
|
|
65
|
-
return { in: 0, out: 0 };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Calculate total bytes in window
|
|
69
|
-
const totalBytesIn = relevantSamples.reduce((sum, s) => sum + s.bytesIn, 0);
|
|
70
|
-
const totalBytesOut = relevantSamples.reduce((sum, s) => sum + s.bytesOut, 0);
|
|
71
|
-
|
|
72
|
-
// Use actual number of seconds covered by samples for accurate rate
|
|
73
|
-
const oldestSampleTime = relevantSamples[0].timestamp;
|
|
74
|
-
const newestSampleTime = relevantSamples[relevantSamples.length - 1].timestamp;
|
|
75
|
-
const actualSeconds = Math.max(1, (newestSampleTime - oldestSampleTime) / 1000 + 1);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
in: Math.round(totalBytesIn / actualSeconds),
|
|
79
|
-
out: Math.round(totalBytesOut / actualSeconds)
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get throughput history for specified duration
|
|
85
|
-
*/
|
|
86
|
-
public getHistory(durationSeconds: number): IThroughputHistoryPoint[] {
|
|
87
|
-
const now = Date.now();
|
|
88
|
-
const startTime = now - (durationSeconds * 1000);
|
|
89
|
-
|
|
90
|
-
// Filter samples within duration
|
|
91
|
-
const relevantSamples = this.samples.filter(s => s.timestamp > startTime);
|
|
92
|
-
|
|
93
|
-
// Convert to history points with per-second rates
|
|
94
|
-
const history: IThroughputHistoryPoint[] = [];
|
|
95
|
-
|
|
96
|
-
for (let i = 0; i < relevantSamples.length; i++) {
|
|
97
|
-
const sample = relevantSamples[i];
|
|
98
|
-
|
|
99
|
-
// For the first sample or samples after gaps, we can't calculate rate
|
|
100
|
-
if (i === 0 || sample.timestamp - relevantSamples[i - 1].timestamp > 2000) {
|
|
101
|
-
history.push({
|
|
102
|
-
timestamp: sample.timestamp,
|
|
103
|
-
in: sample.bytesIn,
|
|
104
|
-
out: sample.bytesOut
|
|
105
|
-
});
|
|
106
|
-
} else {
|
|
107
|
-
// Calculate rate based on time since previous sample
|
|
108
|
-
const prevSample = relevantSamples[i - 1];
|
|
109
|
-
const timeDelta = (sample.timestamp - prevSample.timestamp) / 1000;
|
|
110
|
-
|
|
111
|
-
history.push({
|
|
112
|
-
timestamp: sample.timestamp,
|
|
113
|
-
in: Math.round(sample.bytesIn / timeDelta),
|
|
114
|
-
out: Math.round(sample.bytesOut / timeDelta)
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return history;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Clear all samples
|
|
124
|
-
*/
|
|
125
|
-
public clear(): void {
|
|
126
|
-
this.samples = [];
|
|
127
|
-
this.accumulatedBytesIn = 0;
|
|
128
|
-
this.accumulatedBytesOut = 0;
|
|
129
|
-
this.lastSampleTime = 0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get sample count for debugging
|
|
134
|
-
*/
|
|
135
|
-
public getSampleCount(): number {
|
|
136
|
-
return this.samples.length;
|
|
137
|
-
}
|
|
138
|
-
}
|