@push.rocks/smartproxy 19.3.12 → 19.3.14
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/00_commitinfo_data.js +1 -1
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +117 -9
- package/dist_ts/proxies/smart-proxy/port-manager.d.ts +39 -0
- package/dist_ts/proxies/smart-proxy/port-manager.js +164 -9
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +12 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +161 -8
- package/package.json +1 -1
- package/readme.plan.md +384 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/proxies/smart-proxy/certificate-manager.ts +115 -8
- package/ts/proxies/smart-proxy/port-manager.ts +182 -11
- package/ts/proxies/smart-proxy/smart-proxy.ts +171 -8
package/readme.plan.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# SmartProxy Development Plan
|
|
2
|
+
|
|
3
|
+
## ACME Route Port Binding Intelligence Improvement
|
|
4
|
+
|
|
5
|
+
### Problem Statement
|
|
6
|
+
Currently, SmartProxy has an issue with port binding conflicts between regular routes and ACME challenge routes. While SmartProxy is designed to support multiple routes sharing the same port (differentiated by host, path, etc.), there's a specific conflict when adding ACME challenge routes to a port that is already in use by other routes.
|
|
7
|
+
|
|
8
|
+
This results in the error: `Port 80 is already in use for ACME challenges` when SmartProxy tries to bind the ACME challenge route to a port that it's already using.
|
|
9
|
+
|
|
10
|
+
### Root Cause Analysis
|
|
11
|
+
1. **Double Binding Attempt**: SmartProxy tries to bind to port 80 twice - once for application routes and once for ACME challenge routes.
|
|
12
|
+
2. **Overlapping Route Updates**: When adding a challenge route, it triggers a port binding operation without checking if the port is already bound.
|
|
13
|
+
3. **Naive Error Handling**: The code detects EADDRINUSE but doesn't distinguish between external conflicts and internal conflicts.
|
|
14
|
+
4. **Port Binding Semantics**: The port manager doesn't recognize that a port already bound by SmartProxy can be reused for additional routes.
|
|
15
|
+
|
|
16
|
+
### Solution Architecture
|
|
17
|
+
We need a more intelligent approach to port binding that understands when a port can be shared between routes vs. when a new binding is needed:
|
|
18
|
+
|
|
19
|
+
1. **Port Binding Awareness**: Track what ports are already bound by SmartProxy itself.
|
|
20
|
+
2. **Smart Route Updates**: Only attempt to bind to ports that aren't already bound by SmartProxy.
|
|
21
|
+
3. **Route Merging Logic**: When adding ACME challenge routes, merge them with existing routes on the same ports.
|
|
22
|
+
4. **Dynamic Port Management**: Release port bindings when no routes are using them and rebind when needed.
|
|
23
|
+
5. **Improved Error Recovery**: Handle port conflicts gracefully, with distinct handling for internal vs. external conflicts.
|
|
24
|
+
|
|
25
|
+
### Implementation Plan
|
|
26
|
+
|
|
27
|
+
#### Phase 1: Improve Port Manager Intelligence
|
|
28
|
+
- [ ] Enhance `PortManager` to distinguish between ports that need new bindings vs ports that can reuse existing bindings
|
|
29
|
+
- [ ] Add an internal tracking mechanism to detect when a requested port is already bound internally
|
|
30
|
+
- [ ] Modify port addition logic to skip binding operations for ports already bound by SmartProxy
|
|
31
|
+
- [ ] Implement reference counting for port bindings to track how many routes use each port
|
|
32
|
+
- [ ] Add logic to release port bindings when no routes are using them anymore
|
|
33
|
+
- [ ] Update error handling to provide more context for port binding failures
|
|
34
|
+
|
|
35
|
+
#### Phase 2: Refine ACME Challenge Route Integration
|
|
36
|
+
- [ ] Modify `addChallengeRoute()` to check if the port is already in use by SmartProxy
|
|
37
|
+
- [ ] Ensure route updates don't trigger unnecessary port binding operations
|
|
38
|
+
- [ ] Implement a merging strategy for ACME routes with existing routes on the same port
|
|
39
|
+
- [ ] Add diagnostic logging to track route and port binding relationships
|
|
40
|
+
|
|
41
|
+
#### Phase 3: Enhance Proxy Route Management
|
|
42
|
+
- [ ] Restructure route update process to group routes by port
|
|
43
|
+
- [ ] Implement a more efficient route update mechanism that minimizes port binding operations
|
|
44
|
+
- [ ] Develop port lifecycle management to track usage across route changes
|
|
45
|
+
- [ ] Add validation to detect potential binding conflicts before attempting operations
|
|
46
|
+
- [ ] Create a proper route dependency graph to understand the relationships between routes
|
|
47
|
+
- [ ] Implement efficient detection of "orphaned" ports that no longer have associated routes
|
|
48
|
+
|
|
49
|
+
#### Phase 4: Improve Error Handling and Recovery
|
|
50
|
+
- [ ] Enhance error messages to be more specific about the nature of port conflicts
|
|
51
|
+
- [ ] Add recovery mechanisms for common port binding scenarios
|
|
52
|
+
- [ ] Implement a fallback port selection strategy for ACME challenges
|
|
53
|
+
- [ ] Create a more robust validation system to catch issues before they cause runtime errors
|
|
54
|
+
|
|
55
|
+
### Detailed Technical Tasks
|
|
56
|
+
|
|
57
|
+
#### Phase 1: Improve Port Manager Intelligence
|
|
58
|
+
1. Modify `/ts/proxies/smart-proxy/port-manager.ts`:
|
|
59
|
+
- Add a new method `isPortBoundBySmartProxy(port: number): boolean`
|
|
60
|
+
- Refactor `addPort()` to check if the port is already bound
|
|
61
|
+
- Update `updatePorts()` to be more intelligent about which ports need binding
|
|
62
|
+
- Add reference counting for port usage
|
|
63
|
+
|
|
64
|
+
2. Implement Port Reference Counting:
|
|
65
|
+
```typescript
|
|
66
|
+
// Add to PortManager class
|
|
67
|
+
private portRefCounts: Map<number, number> = new Map();
|
|
68
|
+
|
|
69
|
+
public incrementPortRefCount(port: number): void {
|
|
70
|
+
const currentCount = this.portRefCounts.get(port) || 0;
|
|
71
|
+
this.portRefCounts.set(port, currentCount + 1);
|
|
72
|
+
logger.log('debug', `Port ${port} reference count increased to ${currentCount + 1}`, { port, refCount: currentCount + 1 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public decrementPortRefCount(port: number): number {
|
|
76
|
+
const currentCount = this.portRefCounts.get(port) || 0;
|
|
77
|
+
if (currentCount <= 0) {
|
|
78
|
+
logger.log('warn', `Attempted to decrement reference count for port ${port} below zero`, { port });
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const newCount = currentCount - 1;
|
|
83
|
+
this.portRefCounts.set(port, newCount);
|
|
84
|
+
logger.log('debug', `Port ${port} reference count decreased to ${newCount}`, { port, refCount: newCount });
|
|
85
|
+
return newCount;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public getPortRefCount(port: number): number {
|
|
89
|
+
return this.portRefCounts.get(port) || 0;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
3. Port Binding Logic Enhancements:
|
|
94
|
+
```typescript
|
|
95
|
+
public async addPort(port: number): Promise<void> {
|
|
96
|
+
// If already bound by this instance, just increment ref count and return
|
|
97
|
+
if (this.servers.has(port)) {
|
|
98
|
+
this.incrementPortRefCount(port);
|
|
99
|
+
logger.log('debug', `Port ${port} is already bound by SmartProxy, reusing binding`, { port });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Initialize ref count for new port
|
|
104
|
+
this.portRefCounts.set(port, 1);
|
|
105
|
+
|
|
106
|
+
// Continue with normal binding...
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async removePort(port: number): Promise<void> {
|
|
110
|
+
// Decrement reference count
|
|
111
|
+
const newCount = this.decrementPortRefCount(port);
|
|
112
|
+
|
|
113
|
+
// If port is still in use by other routes, keep it
|
|
114
|
+
if (newCount > 0) {
|
|
115
|
+
logger.log('debug', `Port ${port} still in use by ${newCount} routes, keeping binding open`, { port, refCount: newCount });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// No more references, can actually close the port
|
|
120
|
+
const server = this.servers.get(port);
|
|
121
|
+
if (!server) {
|
|
122
|
+
logger.log('warn', `Port ${port} not found in servers map`, { port });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Continue with normal unbinding logic...
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
4. Add Smarter Port Conflict Detection:
|
|
131
|
+
```typescript
|
|
132
|
+
private isPortConflict(error: any): { isConflict: boolean; isExternal: boolean } {
|
|
133
|
+
if (error.code !== 'EADDRINUSE') {
|
|
134
|
+
return { isConflict: false, isExternal: false };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if we already have this port
|
|
138
|
+
const isBoundInternally = this.servers.has(Number(error.port));
|
|
139
|
+
return { isConflict: true, isExternal: !isBoundInternally };
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### Phase 2: Refine ACME Challenge Route Integration
|
|
144
|
+
1. Modify `/ts/proxies/smart-proxy/certificate-manager.ts`:
|
|
145
|
+
- Enhance `addChallengeRoute()` to be aware of existing port bindings
|
|
146
|
+
- Add port verification before attempting to add challenge routes
|
|
147
|
+
|
|
148
|
+
2. Smart Route Merging Logic:
|
|
149
|
+
```typescript
|
|
150
|
+
private async addChallengeRoute(): Promise<void> {
|
|
151
|
+
// Check if route is already active
|
|
152
|
+
if (this.challengeRouteActive) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create challenge route
|
|
157
|
+
const challengeRoute = this.challengeRoute;
|
|
158
|
+
const challengePort = this.globalAcmeDefaults?.port || 80;
|
|
159
|
+
|
|
160
|
+
// Check if port is already in use by another route
|
|
161
|
+
const portAlreadyUsed = this.routes.some(r =>
|
|
162
|
+
Array.isArray(r.match.ports)
|
|
163
|
+
? r.match.ports.includes(challengePort)
|
|
164
|
+
: r.match.ports === challengePort
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (portAlreadyUsed) {
|
|
168
|
+
logger.log('info', `Port ${challengePort} is already used by an existing route, merging ACME challenge route`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Continue with route update...
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
3. Update Route Manager Communication:
|
|
176
|
+
```typescript
|
|
177
|
+
// Add this method to smart-proxy.ts
|
|
178
|
+
private async addRouteWithoutRebinding(route: IRouteConfig): Promise<void> {
|
|
179
|
+
// Add route to configuration without triggering a port rebind
|
|
180
|
+
this.settings.routes.push(route);
|
|
181
|
+
this.routeManager.updateRoutes(this.settings.routes);
|
|
182
|
+
|
|
183
|
+
// Update HttpProxy if needed, but skip port binding updates
|
|
184
|
+
if (this.httpProxyBridge.getHttpProxy()) {
|
|
185
|
+
await this.httpProxyBridge.syncRoutesToHttpProxy(this.settings.routes);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### Phase 3: Enhance Proxy Route Management
|
|
191
|
+
1. Modify `/ts/proxies/smart-proxy/smart-proxy.ts`:
|
|
192
|
+
- Refactor `updateRoutes()` to group routes by port
|
|
193
|
+
- Implement incremental updates that preserve port bindings
|
|
194
|
+
- Add orphaned port detection and cleanup
|
|
195
|
+
|
|
196
|
+
2. Group Routes by Port:
|
|
197
|
+
```typescript
|
|
198
|
+
private groupRoutesByPort(routes: IRouteConfig[]): Map<number, IRouteConfig[]> {
|
|
199
|
+
const portMap = new Map<number, IRouteConfig[]>();
|
|
200
|
+
|
|
201
|
+
for (const route of routes) {
|
|
202
|
+
const ports = Array.isArray(route.match.ports)
|
|
203
|
+
? route.match.ports
|
|
204
|
+
: [route.match.ports];
|
|
205
|
+
|
|
206
|
+
for (const port of ports) {
|
|
207
|
+
if (!portMap.has(port)) {
|
|
208
|
+
portMap.set(port, []);
|
|
209
|
+
}
|
|
210
|
+
portMap.get(port)!.push(route);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return portMap;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
3. Implement Port Usage Tracking:
|
|
219
|
+
```typescript
|
|
220
|
+
private updatePortUsageMap(routes: IRouteConfig[]): Map<number, Set<string>> {
|
|
221
|
+
// Map of port -> Set of route names using that port
|
|
222
|
+
const portUsage = new Map<number, Set<string>>();
|
|
223
|
+
|
|
224
|
+
for (const route of routes) {
|
|
225
|
+
const ports = Array.isArray(route.match.ports)
|
|
226
|
+
? route.match.ports
|
|
227
|
+
: [route.match.ports];
|
|
228
|
+
|
|
229
|
+
const routeName = route.name || `unnamed_${Math.random().toString(36).substring(2, 9)}`;
|
|
230
|
+
|
|
231
|
+
for (const port of ports) {
|
|
232
|
+
if (!portUsage.has(port)) {
|
|
233
|
+
portUsage.set(port, new Set());
|
|
234
|
+
}
|
|
235
|
+
portUsage.get(port)!.add(routeName);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return portUsage;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private findOrphanedPorts(oldUsage: Map<number, Set<string>>, newUsage: Map<number, Set<string>>): number[] {
|
|
243
|
+
// Find ports that have no routes in new configuration
|
|
244
|
+
const orphanedPorts: number[] = [];
|
|
245
|
+
|
|
246
|
+
for (const [port, routes] of oldUsage.entries()) {
|
|
247
|
+
if (!newUsage.has(port) || newUsage.get(port)!.size === 0) {
|
|
248
|
+
orphanedPorts.push(port);
|
|
249
|
+
logger.log('info', `Port ${port} no longer has any associated routes, will be released`, { port });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return orphanedPorts;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
4. Implement Incremental Update Logic:
|
|
258
|
+
```typescript
|
|
259
|
+
public async updateRoutesIncremental(newRoutes: IRouteConfig[]): Promise<void> {
|
|
260
|
+
// Track port usage before and after update
|
|
261
|
+
const oldPortUsage = this.updatePortUsageMap(this.settings.routes);
|
|
262
|
+
const newPortUsage = this.updatePortUsageMap(newRoutes);
|
|
263
|
+
|
|
264
|
+
// Find orphaned ports - ports that no longer have any routes
|
|
265
|
+
const orphanedPorts = this.findOrphanedPorts(oldPortUsage, newPortUsage);
|
|
266
|
+
|
|
267
|
+
// Ports that need new bindings - not in old configuration
|
|
268
|
+
const newBindingPorts = [...newPortUsage.keys()].filter(p => !oldPortUsage.has(p));
|
|
269
|
+
|
|
270
|
+
// Close orphaned ports
|
|
271
|
+
if (orphanedPorts.length > 0) {
|
|
272
|
+
logger.log('info', `Releasing ${orphanedPorts.length} orphaned ports: ${orphanedPorts.join(', ')}`, { ports: orphanedPorts });
|
|
273
|
+
await this.portManager.removePorts(orphanedPorts);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Bind to new ports
|
|
277
|
+
if (newBindingPorts.length > 0) {
|
|
278
|
+
logger.log('info', `Binding to ${newBindingPorts.length} new ports: ${newBindingPorts.join(', ')}`, { ports: newBindingPorts });
|
|
279
|
+
await this.portManager.addPorts(newBindingPorts);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update route configuration
|
|
283
|
+
this.settings.routes = newRoutes;
|
|
284
|
+
this.routeManager.updateRoutes(newRoutes);
|
|
285
|
+
|
|
286
|
+
// Update other components...
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Phase 4: Improve Error Handling and Recovery
|
|
291
|
+
1. Enhance Error Reporting:
|
|
292
|
+
```typescript
|
|
293
|
+
private handlePortBindingError(port: number, error: any): void {
|
|
294
|
+
if (error.code === 'EADDRINUSE') {
|
|
295
|
+
const isInternalConflict = this.portManager.isPortBoundBySmartProxy(port);
|
|
296
|
+
if (isInternalConflict) {
|
|
297
|
+
logger.log('warn', `Port ${port} is already bound by SmartProxy. This is likely a route configuration issue.`, { port });
|
|
298
|
+
} else {
|
|
299
|
+
logger.log('error', `Port ${port} is in use by another application. Please choose a different port.`, { port });
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
logger.log('error', `Failed to bind to port ${port}: ${error.message}`, { port, error });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
2. Implement ACME Port Fallback Strategy:
|
|
308
|
+
```typescript
|
|
309
|
+
private async selectAcmePort(): Promise<number> {
|
|
310
|
+
const preferredPort = this.globalAcmeDefaults?.port || 80;
|
|
311
|
+
|
|
312
|
+
// Check if preferred port is already bound internally
|
|
313
|
+
if (this.portManager.isPortBoundBySmartProxy(preferredPort)) {
|
|
314
|
+
// We can use it without a new binding
|
|
315
|
+
return preferredPort;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Try to bind to preferred port
|
|
319
|
+
try {
|
|
320
|
+
// Temporary test binding
|
|
321
|
+
const server = plugins.net.createServer();
|
|
322
|
+
await new Promise<void>((resolve, reject) => {
|
|
323
|
+
server.listen(preferredPort, () => {
|
|
324
|
+
server.close();
|
|
325
|
+
resolve();
|
|
326
|
+
}).on('error', reject);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// If we get here, port is available
|
|
330
|
+
return preferredPort;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (error.code === 'EADDRINUSE') {
|
|
333
|
+
// Port is unavailable, try fallback ports
|
|
334
|
+
for (const fallbackPort of [8080, 8081, 8082, 8083, 8084]) {
|
|
335
|
+
try {
|
|
336
|
+
// Test if we can bind to fallback
|
|
337
|
+
const server = plugins.net.createServer();
|
|
338
|
+
await new Promise<void>((resolve, reject) => {
|
|
339
|
+
server.listen(fallbackPort, () => {
|
|
340
|
+
server.close();
|
|
341
|
+
resolve();
|
|
342
|
+
}).on('error', reject);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
logger.log('warn', `Primary ACME port ${preferredPort} is unavailable, using fallback port ${fallbackPort}`);
|
|
346
|
+
return fallbackPort;
|
|
347
|
+
} catch {
|
|
348
|
+
// Try next fallback
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// All attempts failed
|
|
354
|
+
throw new Error(`Could not find an available port for ACME challenges`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Testing Strategy
|
|
360
|
+
1. **Unit Tests**:
|
|
361
|
+
- Test port binding intelligence
|
|
362
|
+
- Test route merging logic
|
|
363
|
+
- Test error handling mechanisms
|
|
364
|
+
- Test port reference counting
|
|
365
|
+
- Test orphaned port detection and cleanup
|
|
366
|
+
|
|
367
|
+
2. **Integration Tests**:
|
|
368
|
+
- Test multiple routes on the same port
|
|
369
|
+
- Test ACME challenges on ports with existing routes
|
|
370
|
+
- Test dynamic route addition and removal
|
|
371
|
+
- Test port lifecycle (bind → share → release)
|
|
372
|
+
- Test various recovery scenarios
|
|
373
|
+
|
|
374
|
+
3. **Stress Tests**:
|
|
375
|
+
- Test rapid route updates
|
|
376
|
+
- Test concurrent operations
|
|
377
|
+
- Test large scale route changes (add/remove many at once)
|
|
378
|
+
- Test frequent changes to see if ports are properly released
|
|
379
|
+
- Test recovery from port conflicts
|
|
380
|
+
|
|
381
|
+
### Release Plan
|
|
382
|
+
1. **19.4.0** - Phase 1 & 2: Port Manager and ACME Route Improvements
|
|
383
|
+
2. **19.5.0** - Phase 3: Enhanced Route Management
|
|
384
|
+
3. **19.6.0** - Phase 4: Improved Error Handling and Recovery
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '19.3.
|
|
6
|
+
version: '19.3.13',
|
|
7
7
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
|
8
8
|
}
|
|
@@ -93,6 +93,12 @@ export class SmartCertManager {
|
|
|
93
93
|
*/
|
|
94
94
|
public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
|
|
95
95
|
this.updateRoutesCallback = callback;
|
|
96
|
+
try {
|
|
97
|
+
logger.log('debug', 'Route update callback set successfully', { component: 'certificate-manager' });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Silently handle logging errors
|
|
100
|
+
console.log('[DEBUG] Route update callback set successfully');
|
|
101
|
+
}
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
/**
|
|
@@ -399,13 +405,23 @@ export class SmartCertManager {
|
|
|
399
405
|
private async addChallengeRoute(): Promise<void> {
|
|
400
406
|
// Check with state manager first
|
|
401
407
|
if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) {
|
|
402
|
-
|
|
408
|
+
try {
|
|
409
|
+
logger.log('info', 'Challenge route already active in global state, skipping', { component: 'certificate-manager' });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
// Silently handle logging errors
|
|
412
|
+
console.log('[INFO] Challenge route already active in global state, skipping');
|
|
413
|
+
}
|
|
403
414
|
this.challengeRouteActive = true;
|
|
404
415
|
return;
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
if (this.challengeRouteActive) {
|
|
408
|
-
|
|
419
|
+
try {
|
|
420
|
+
logger.log('info', 'Challenge route already active locally, skipping', { component: 'certificate-manager' });
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// Silently handle logging errors
|
|
423
|
+
console.log('[INFO] Challenge route already active locally, skipping');
|
|
424
|
+
}
|
|
409
425
|
return;
|
|
410
426
|
}
|
|
411
427
|
|
|
@@ -416,9 +432,53 @@ export class SmartCertManager {
|
|
|
416
432
|
if (!this.challengeRoute) {
|
|
417
433
|
throw new Error('Challenge route not initialized');
|
|
418
434
|
}
|
|
435
|
+
|
|
436
|
+
// Get the challenge port
|
|
437
|
+
const challengePort = this.globalAcmeDefaults?.port || 80;
|
|
438
|
+
|
|
439
|
+
// Check if any existing routes are already using this port
|
|
440
|
+
const portInUseByRoutes = this.routes.some(route => {
|
|
441
|
+
const routePorts = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
|
|
442
|
+
return routePorts.some(p => {
|
|
443
|
+
// Handle both number and port range objects
|
|
444
|
+
if (typeof p === 'number') {
|
|
445
|
+
return p === challengePort;
|
|
446
|
+
} else if (typeof p === 'object' && 'from' in p && 'to' in p) {
|
|
447
|
+
// Port range case - check if challengePort is in range
|
|
448
|
+
return challengePort >= p.from && challengePort <= p.to;
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (portInUseByRoutes) {
|
|
455
|
+
logger.log('info', `Port ${challengePort} is already used by another route, merging ACME challenge route`, {
|
|
456
|
+
port: challengePort,
|
|
457
|
+
component: 'certificate-manager'
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Add the challenge route
|
|
419
462
|
const challengeRoute = this.challengeRoute;
|
|
420
463
|
|
|
464
|
+
// If the port is already in use by other routes in this SmartProxy instance,
|
|
465
|
+
// we can safely add the ACME challenge route without trying to bind to the port again
|
|
421
466
|
try {
|
|
467
|
+
// Check if we're already listening on the challenge port
|
|
468
|
+
const isPortAlreadyBound = portInUseByRoutes;
|
|
469
|
+
|
|
470
|
+
if (isPortAlreadyBound) {
|
|
471
|
+
try {
|
|
472
|
+
logger.log('info', `Port ${challengePort} is already bound by SmartProxy, adding ACME challenge route without rebinding`, {
|
|
473
|
+
port: challengePort,
|
|
474
|
+
component: 'certificate-manager'
|
|
475
|
+
});
|
|
476
|
+
} catch (error) {
|
|
477
|
+
// Silently handle logging errors
|
|
478
|
+
console.log(`[INFO] Port ${challengePort} is already bound by SmartProxy, adding ACME challenge route without rebinding`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
422
482
|
const updatedRoutes = [...this.routes, challengeRoute];
|
|
423
483
|
await this.updateRoutesCallback(updatedRoutes);
|
|
424
484
|
this.challengeRouteActive = true;
|
|
@@ -428,11 +488,43 @@ export class SmartCertManager {
|
|
|
428
488
|
this.acmeStateManager.addChallengeRoute(challengeRoute);
|
|
429
489
|
}
|
|
430
490
|
|
|
431
|
-
|
|
491
|
+
try {
|
|
492
|
+
logger.log('info', 'ACME challenge route successfully added', { component: 'certificate-manager' });
|
|
493
|
+
} catch (error) {
|
|
494
|
+
// Silently handle logging errors
|
|
495
|
+
console.log('[INFO] ACME challenge route successfully added');
|
|
496
|
+
}
|
|
432
497
|
} catch (error) {
|
|
433
|
-
|
|
498
|
+
// Handle specific EADDRINUSE errors differently based on whether it's an internal conflict
|
|
434
499
|
if ((error as any).code === 'EADDRINUSE') {
|
|
435
|
-
|
|
500
|
+
try {
|
|
501
|
+
logger.log('error', `Failed to add challenge route on port ${challengePort}: ${error.message}`, {
|
|
502
|
+
error: (error as Error).message,
|
|
503
|
+
port: challengePort,
|
|
504
|
+
component: 'certificate-manager'
|
|
505
|
+
});
|
|
506
|
+
} catch (logError) {
|
|
507
|
+
// Silently handle logging errors
|
|
508
|
+
console.log(`[ERROR] Failed to add challenge route on port ${challengePort}: ${error.message}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Provide a more informative error message
|
|
512
|
+
throw new Error(
|
|
513
|
+
`Port ${challengePort} is already in use. ` +
|
|
514
|
+
`If it's in use by an external process, configure a different port in the ACME settings. ` +
|
|
515
|
+
`If it's in use by SmartProxy, there may be a route configuration issue.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Log and rethrow other errors
|
|
520
|
+
try {
|
|
521
|
+
logger.log('error', `Failed to add challenge route: ${(error as Error).message}`, {
|
|
522
|
+
error: (error as Error).message,
|
|
523
|
+
component: 'certificate-manager'
|
|
524
|
+
});
|
|
525
|
+
} catch (logError) {
|
|
526
|
+
// Silently handle logging errors
|
|
527
|
+
console.log(`[ERROR] Failed to add challenge route: ${(error as Error).message}`);
|
|
436
528
|
}
|
|
437
529
|
throw error;
|
|
438
530
|
}
|
|
@@ -443,7 +535,12 @@ export class SmartCertManager {
|
|
|
443
535
|
*/
|
|
444
536
|
private async removeChallengeRoute(): Promise<void> {
|
|
445
537
|
if (!this.challengeRouteActive) {
|
|
446
|
-
|
|
538
|
+
try {
|
|
539
|
+
logger.log('info', 'Challenge route not active, skipping removal', { component: 'certificate-manager' });
|
|
540
|
+
} catch (error) {
|
|
541
|
+
// Silently handle logging errors
|
|
542
|
+
console.log('[INFO] Challenge route not active, skipping removal');
|
|
543
|
+
}
|
|
447
544
|
return;
|
|
448
545
|
}
|
|
449
546
|
|
|
@@ -461,9 +558,19 @@ export class SmartCertManager {
|
|
|
461
558
|
this.acmeStateManager.removeChallengeRoute('acme-challenge');
|
|
462
559
|
}
|
|
463
560
|
|
|
464
|
-
|
|
561
|
+
try {
|
|
562
|
+
logger.log('info', 'ACME challenge route successfully removed', { component: 'certificate-manager' });
|
|
563
|
+
} catch (error) {
|
|
564
|
+
// Silently handle logging errors
|
|
565
|
+
console.log('[INFO] ACME challenge route successfully removed');
|
|
566
|
+
}
|
|
465
567
|
} catch (error) {
|
|
466
|
-
|
|
568
|
+
try {
|
|
569
|
+
logger.log('error', `Failed to remove challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' });
|
|
570
|
+
} catch (logError) {
|
|
571
|
+
// Silently handle logging errors
|
|
572
|
+
console.log(`[ERROR] Failed to remove challenge route: ${error.message}`);
|
|
573
|
+
}
|
|
467
574
|
// Reset the flag even on error to avoid getting stuck
|
|
468
575
|
this.challengeRouteActive = false;
|
|
469
576
|
throw error;
|