@push.rocks/smartproxy 21.1.2 → 21.1.5
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 +23 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/core/models/socket-augmentation.d.ts +3 -0
- package/dist_ts/core/models/socket-augmentation.js +1 -1
- package/dist_ts/core/utils/socket-tracker.d.ts +16 -0
- package/dist_ts/core/utils/socket-tracker.js +49 -0
- package/dist_ts/detection/detectors/http-detector.js +19 -7
- package/dist_ts/detection/detectors/tls-detector.d.ts +1 -9
- package/dist_ts/detection/detectors/tls-detector.js +6 -29
- package/dist_ts/detection/protocol-detector.d.ts +7 -2
- package/dist_ts/detection/protocol-detector.js +80 -8
- package/dist_ts/proxies/http-proxy/http-proxy.d.ts +5 -1
- package/dist_ts/proxies/http-proxy/http-proxy.js +63 -39
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +5 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +20 -6
- package/dist_ts/proxies/smart-proxy/connection-manager.js +12 -1
- package/dist_ts/proxies/smart-proxy/index.d.ts +1 -0
- package/dist_ts/proxies/smart-proxy/index.js +2 -1
- package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/metrics-collector.js +52 -7
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +7 -7
- package/dist_ts/proxies/smart-proxy/route-orchestrator.d.ts +56 -0
- package/dist_ts/proxies/smart-proxy/route-orchestrator.js +204 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +1 -11
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -237
- package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +42 -7
- package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +58 -0
- package/dist_ts/proxies/smart-proxy/utils/route-validator.js +405 -0
- package/package.json +3 -2
- package/readme.md +321 -828
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/models/socket-augmentation.ts +5 -0
- package/ts/core/utils/socket-tracker.ts +63 -0
- package/ts/detection/detectors/http-detector.ts +20 -7
- package/ts/detection/protocol-detector.ts +57 -6
- package/ts/proxies/http-proxy/http-proxy.ts +73 -48
- package/ts/proxies/smart-proxy/certificate-manager.ts +21 -5
- package/ts/proxies/smart-proxy/index.ts +1 -0
- package/ts/proxies/smart-proxy/metrics-collector.ts +58 -6
- package/ts/proxies/smart-proxy/route-orchestrator.ts +297 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +66 -270
- package/ts/proxies/smart-proxy/utils/route-helpers.ts +45 -6
- package/ts/proxies/smart-proxy/utils/route-validator.ts +453 -0
|
@@ -0,0 +1,297 @@
|
|
|
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
|
+
}
|