@push.rocks/smartproxy 19.2.3 → 19.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/proxies/smart-proxy/acme-state-manager.d.ts +42 -0
- package/dist_ts/proxies/smart-proxy/acme-state-manager.js +101 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +18 -1
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +129 -49
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +7 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +84 -44
- package/dist_ts/proxies/smart-proxy/utils/mutex.d.ts +19 -0
- package/dist_ts/proxies/smart-proxy/utils/mutex.js +47 -0
- package/package.json +1 -1
- package/readme.plan.md +241 -65
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/proxies/smart-proxy/acme-state-manager.ts +112 -0
- package/ts/proxies/smart-proxy/certificate-manager.ts +127 -29
- package/ts/proxies/smart-proxy/smart-proxy.ts +114 -57
- package/ts/proxies/smart-proxy/utils/mutex.ts +45 -0
- package/ts/common/port80-adapter.ts +0 -111
|
@@ -3,6 +3,7 @@ import { NetworkProxy } from '../network-proxy/index.js';
|
|
|
3
3
|
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
|
|
4
4
|
import type { IAcmeOptions } from './models/interfaces.js';
|
|
5
5
|
import { CertStore } from './cert-store.js';
|
|
6
|
+
import type { AcmeStateManager } from './acme-state-manager.js';
|
|
6
7
|
|
|
7
8
|
export interface ICertStatus {
|
|
8
9
|
domain: string;
|
|
@@ -38,6 +39,15 @@ export class SmartCertManager {
|
|
|
38
39
|
// Callback to update SmartProxy routes for challenges
|
|
39
40
|
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
|
|
40
41
|
|
|
42
|
+
// Flag to track if challenge route is currently active
|
|
43
|
+
private challengeRouteActive: boolean = false;
|
|
44
|
+
|
|
45
|
+
// Flag to track if provisioning is in progress
|
|
46
|
+
private isProvisioning: boolean = false;
|
|
47
|
+
|
|
48
|
+
// ACME state manager reference
|
|
49
|
+
private acmeStateManager: AcmeStateManager | null = null;
|
|
50
|
+
|
|
41
51
|
constructor(
|
|
42
52
|
private routes: IRouteConfig[],
|
|
43
53
|
private certDir: string = './certs',
|
|
@@ -45,15 +55,39 @@ export class SmartCertManager {
|
|
|
45
55
|
email?: string;
|
|
46
56
|
useProduction?: boolean;
|
|
47
57
|
port?: number;
|
|
58
|
+
},
|
|
59
|
+
private initialState?: {
|
|
60
|
+
challengeRouteActive?: boolean;
|
|
48
61
|
}
|
|
49
62
|
) {
|
|
50
63
|
this.certStore = new CertStore(certDir);
|
|
64
|
+
|
|
65
|
+
// Apply initial state if provided
|
|
66
|
+
if (initialState) {
|
|
67
|
+
this.challengeRouteActive = initialState.challengeRouteActive || false;
|
|
68
|
+
}
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
public setNetworkProxy(networkProxy: NetworkProxy): void {
|
|
54
72
|
this.networkProxy = networkProxy;
|
|
55
73
|
}
|
|
56
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Get the current state of the certificate manager
|
|
77
|
+
*/
|
|
78
|
+
public getState(): { challengeRouteActive: boolean } {
|
|
79
|
+
return {
|
|
80
|
+
challengeRouteActive: this.challengeRouteActive
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Set the ACME state manager
|
|
86
|
+
*/
|
|
87
|
+
public setAcmeStateManager(stateManager: AcmeStateManager): void {
|
|
88
|
+
this.acmeStateManager = stateManager;
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
/**
|
|
58
92
|
* Set global ACME defaults from top-level configuration
|
|
59
93
|
*/
|
|
@@ -96,6 +130,14 @@ export class SmartCertManager {
|
|
|
96
130
|
});
|
|
97
131
|
|
|
98
132
|
await this.smartAcme.start();
|
|
133
|
+
|
|
134
|
+
// Add challenge route once at initialization if not already active
|
|
135
|
+
if (!this.challengeRouteActive) {
|
|
136
|
+
console.log('Adding ACME challenge route during initialization');
|
|
137
|
+
await this.addChallengeRoute();
|
|
138
|
+
} else {
|
|
139
|
+
console.log('Challenge route already active from previous instance');
|
|
140
|
+
}
|
|
99
141
|
}
|
|
100
142
|
|
|
101
143
|
// Provision certificates for all routes
|
|
@@ -114,24 +156,37 @@ export class SmartCertManager {
|
|
|
114
156
|
r.action.tls?.mode === 'terminate-and-reencrypt'
|
|
115
157
|
);
|
|
116
158
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
159
|
+
// Set provisioning flag to prevent concurrent operations
|
|
160
|
+
this.isProvisioning = true;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
for (const route of certRoutes) {
|
|
164
|
+
try {
|
|
165
|
+
await this.provisionCertificate(route, true); // Allow concurrent since we're managing it here
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
|
|
168
|
+
}
|
|
122
169
|
}
|
|
170
|
+
} finally {
|
|
171
|
+
this.isProvisioning = false;
|
|
123
172
|
}
|
|
124
173
|
}
|
|
125
174
|
|
|
126
175
|
/**
|
|
127
176
|
* Provision certificate for a single route
|
|
128
177
|
*/
|
|
129
|
-
public async provisionCertificate(route: IRouteConfig): Promise<void> {
|
|
178
|
+
public async provisionCertificate(route: IRouteConfig, allowConcurrent: boolean = false): Promise<void> {
|
|
130
179
|
const tls = route.action.tls;
|
|
131
180
|
if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
|
|
132
181
|
return;
|
|
133
182
|
}
|
|
134
183
|
|
|
184
|
+
// Check if provisioning is already in progress (prevent concurrent provisioning)
|
|
185
|
+
if (!allowConcurrent && this.isProvisioning) {
|
|
186
|
+
console.log(`Certificate provisioning already in progress, skipping ${route.name}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
135
190
|
const domains = this.extractDomainsFromRoute(route);
|
|
136
191
|
if (domains.length === 0) {
|
|
137
192
|
console.warn(`Route ${route.name} has TLS termination but no domains`);
|
|
@@ -186,13 +241,12 @@ export class SmartCertManager {
|
|
|
186
241
|
this.updateCertStatus(routeName, 'pending', 'acme');
|
|
187
242
|
|
|
188
243
|
try {
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
// Use smartacme to get certificate
|
|
194
|
-
const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
|
|
244
|
+
// Challenge route should already be active from initialization
|
|
245
|
+
// No need to add it for each certificate
|
|
195
246
|
|
|
247
|
+
// Use smartacme to get certificate
|
|
248
|
+
const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
|
|
249
|
+
|
|
196
250
|
// SmartAcme's Cert object has these properties:
|
|
197
251
|
// - publicKey: The certificate PEM string
|
|
198
252
|
// - privateKey: The private key PEM string
|
|
@@ -211,18 +265,9 @@ export class SmartCertManager {
|
|
|
211
265
|
await this.applyCertificate(primaryDomain, certData);
|
|
212
266
|
this.updateCertStatus(routeName, 'valid', 'acme', certData);
|
|
213
267
|
|
|
214
|
-
|
|
215
|
-
} catch (error) {
|
|
216
|
-
console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
|
|
217
|
-
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
|
|
218
|
-
throw error;
|
|
219
|
-
} finally {
|
|
220
|
-
// Always remove challenge route after provisioning
|
|
221
|
-
await this.removeChallengeRoute();
|
|
222
|
-
}
|
|
268
|
+
console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
|
|
223
269
|
} catch (error) {
|
|
224
|
-
|
|
225
|
-
console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`);
|
|
270
|
+
console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
|
|
226
271
|
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
|
|
227
272
|
throw error;
|
|
228
273
|
}
|
|
@@ -337,6 +382,18 @@ export class SmartCertManager {
|
|
|
337
382
|
* Add challenge route to SmartProxy
|
|
338
383
|
*/
|
|
339
384
|
private async addChallengeRoute(): Promise<void> {
|
|
385
|
+
// Check with state manager first
|
|
386
|
+
if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) {
|
|
387
|
+
console.log('Challenge route already active in global state, skipping');
|
|
388
|
+
this.challengeRouteActive = true;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (this.challengeRouteActive) {
|
|
393
|
+
console.log('Challenge route already active locally, skipping');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
340
397
|
if (!this.updateRoutesCallback) {
|
|
341
398
|
throw new Error('No route update callback set');
|
|
342
399
|
}
|
|
@@ -346,20 +403,56 @@ export class SmartCertManager {
|
|
|
346
403
|
}
|
|
347
404
|
const challengeRoute = this.challengeRoute;
|
|
348
405
|
|
|
349
|
-
|
|
350
|
-
|
|
406
|
+
try {
|
|
407
|
+
const updatedRoutes = [...this.routes, challengeRoute];
|
|
408
|
+
await this.updateRoutesCallback(updatedRoutes);
|
|
409
|
+
this.challengeRouteActive = true;
|
|
410
|
+
|
|
411
|
+
// Register with state manager
|
|
412
|
+
if (this.acmeStateManager) {
|
|
413
|
+
this.acmeStateManager.addChallengeRoute(challengeRoute);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
console.log('ACME challenge route successfully added');
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Failed to add challenge route:', error);
|
|
419
|
+
if ((error as any).code === 'EADDRINUSE') {
|
|
420
|
+
throw new Error(`Port ${this.globalAcmeDefaults?.port || 80} is already in use for ACME challenges`);
|
|
421
|
+
}
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
351
424
|
}
|
|
352
425
|
|
|
353
426
|
/**
|
|
354
427
|
* Remove challenge route from SmartProxy
|
|
355
428
|
*/
|
|
356
429
|
private async removeChallengeRoute(): Promise<void> {
|
|
430
|
+
if (!this.challengeRouteActive) {
|
|
431
|
+
console.log('Challenge route not active, skipping removal');
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
357
435
|
if (!this.updateRoutesCallback) {
|
|
358
436
|
return;
|
|
359
437
|
}
|
|
360
438
|
|
|
361
|
-
|
|
362
|
-
|
|
439
|
+
try {
|
|
440
|
+
const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
|
|
441
|
+
await this.updateRoutesCallback(filteredRoutes);
|
|
442
|
+
this.challengeRouteActive = false;
|
|
443
|
+
|
|
444
|
+
// Remove from state manager
|
|
445
|
+
if (this.acmeStateManager) {
|
|
446
|
+
this.acmeStateManager.removeChallengeRoute('acme-challenge');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
console.log('ACME challenge route successfully removed');
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error('Failed to remove challenge route:', error);
|
|
452
|
+
// Reset the flag even on error to avoid getting stuck
|
|
453
|
+
this.challengeRouteActive = false;
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
363
456
|
}
|
|
364
457
|
|
|
365
458
|
/**
|
|
@@ -512,14 +605,19 @@ export class SmartCertManager {
|
|
|
512
605
|
this.renewalTimer = null;
|
|
513
606
|
}
|
|
514
607
|
|
|
608
|
+
// Always remove challenge route on shutdown
|
|
609
|
+
if (this.challengeRoute) {
|
|
610
|
+
console.log('Removing ACME challenge route during shutdown');
|
|
611
|
+
await this.removeChallengeRoute();
|
|
612
|
+
}
|
|
613
|
+
|
|
515
614
|
if (this.smartAcme) {
|
|
516
615
|
await this.smartAcme.stop();
|
|
517
616
|
}
|
|
518
617
|
|
|
519
|
-
//
|
|
618
|
+
// Clear any pending challenges
|
|
520
619
|
if (this.pendingChallenges.size > 0) {
|
|
521
620
|
this.pendingChallenges.clear();
|
|
522
|
-
await this.removeChallengeRoute();
|
|
523
621
|
}
|
|
524
622
|
}
|
|
525
623
|
|
|
@@ -20,6 +20,12 @@ import type {
|
|
|
20
20
|
} from './models/interfaces.js';
|
|
21
21
|
import type { IRouteConfig } from './models/route-types.js';
|
|
22
22
|
|
|
23
|
+
// Import mutex for route update synchronization
|
|
24
|
+
import { Mutex } from './utils/mutex.js';
|
|
25
|
+
|
|
26
|
+
// Import ACME state manager
|
|
27
|
+
import { AcmeStateManager } from './acme-state-manager.js';
|
|
28
|
+
|
|
23
29
|
/**
|
|
24
30
|
* SmartProxy - Pure route-based API
|
|
25
31
|
*
|
|
@@ -52,6 +58,11 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
52
58
|
// Certificate manager for ACME and static certificates
|
|
53
59
|
private certManager: SmartCertManager | null = null;
|
|
54
60
|
|
|
61
|
+
// Global challenge route tracking
|
|
62
|
+
private globalChallengeRouteActive: boolean = false;
|
|
63
|
+
private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
|
|
64
|
+
private acmeStateManager: AcmeStateManager;
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Constructor for SmartProxy
|
|
57
68
|
*
|
|
@@ -171,6 +182,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
171
182
|
|
|
172
183
|
// Initialize NFTablesManager
|
|
173
184
|
this.nftablesManager = new NFTablesManager(this.settings);
|
|
185
|
+
|
|
186
|
+
// Initialize route update mutex for synchronization
|
|
187
|
+
this.routeUpdateLock = new Mutex();
|
|
188
|
+
|
|
189
|
+
// Initialize ACME state manager
|
|
190
|
+
this.acmeStateManager = new AcmeStateManager();
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
/**
|
|
@@ -185,9 +202,10 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
185
202
|
private async createCertificateManager(
|
|
186
203
|
routes: IRouteConfig[],
|
|
187
204
|
certStore: string = './certs',
|
|
188
|
-
acmeOptions?: any
|
|
205
|
+
acmeOptions?: any,
|
|
206
|
+
initialState?: { challengeRouteActive?: boolean }
|
|
189
207
|
): Promise<SmartCertManager> {
|
|
190
|
-
const certManager = new SmartCertManager(routes, certStore, acmeOptions);
|
|
208
|
+
const certManager = new SmartCertManager(routes, certStore, acmeOptions, initialState);
|
|
191
209
|
|
|
192
210
|
// Always set up the route update callback for ACME challenges
|
|
193
211
|
certManager.setUpdateRoutesCallback(async (routes) => {
|
|
@@ -199,6 +217,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
199
217
|
certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
200
218
|
}
|
|
201
219
|
|
|
220
|
+
// Set the ACME state manager
|
|
221
|
+
certManager.setAcmeStateManager(this.acmeStateManager);
|
|
222
|
+
|
|
202
223
|
// Pass down the global ACME config if available
|
|
203
224
|
if (this.settings.acme) {
|
|
204
225
|
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
@@ -441,7 +462,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
441
462
|
|
|
442
463
|
// Stop NetworkProxy
|
|
443
464
|
await this.networkProxyBridge.stop();
|
|
444
|
-
|
|
465
|
+
|
|
466
|
+
// Clear ACME state manager
|
|
467
|
+
this.acmeStateManager.clear();
|
|
445
468
|
|
|
446
469
|
console.log('SmartProxy shutdown complete.');
|
|
447
470
|
}
|
|
@@ -456,6 +479,29 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
456
479
|
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
|
|
457
480
|
}
|
|
458
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Verify the challenge route has been properly removed from routes
|
|
484
|
+
*/
|
|
485
|
+
private async verifyChallengeRouteRemoved(): Promise<void> {
|
|
486
|
+
const maxRetries = 10;
|
|
487
|
+
const retryDelay = 100; // milliseconds
|
|
488
|
+
|
|
489
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
490
|
+
// Check if the challenge route is still in the active routes
|
|
491
|
+
const challengeRouteExists = this.settings.routes.some(r => r.name === 'acme-challenge');
|
|
492
|
+
|
|
493
|
+
if (!challengeRouteExists) {
|
|
494
|
+
console.log('Challenge route successfully removed from routes');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Wait before retrying
|
|
499
|
+
await plugins.smartdelay.delayFor(retryDelay);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
throw new Error('Failed to verify challenge route removal after ' + maxRetries + ' attempts');
|
|
503
|
+
}
|
|
504
|
+
|
|
459
505
|
/**
|
|
460
506
|
* Update routes with new configuration
|
|
461
507
|
*
|
|
@@ -480,70 +526,81 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
480
526
|
* ```
|
|
481
527
|
*/
|
|
482
528
|
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
|
483
|
-
|
|
529
|
+
return this.routeUpdateLock.runExclusive(async () => {
|
|
530
|
+
console.log(`Updating routes (${newRoutes.length} routes)`);
|
|
484
531
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
// Get new routes that use NFTables
|
|
491
|
-
const newNfTablesRoutes = newRoutes.filter(
|
|
492
|
-
r => r.action.forwardingEngine === 'nftables'
|
|
493
|
-
);
|
|
494
|
-
|
|
495
|
-
// Find routes to remove, update, or add
|
|
496
|
-
for (const oldRoute of oldNfTablesRoutes) {
|
|
497
|
-
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
|
532
|
+
// Get existing routes that use NFTables
|
|
533
|
+
const oldNfTablesRoutes = this.settings.routes.filter(
|
|
534
|
+
r => r.action.forwardingEngine === 'nftables'
|
|
535
|
+
);
|
|
498
536
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
537
|
+
// Get new routes that use NFTables
|
|
538
|
+
const newNfTablesRoutes = newRoutes.filter(
|
|
539
|
+
r => r.action.forwardingEngine === 'nftables'
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Find routes to remove, update, or add
|
|
543
|
+
for (const oldRoute of oldNfTablesRoutes) {
|
|
544
|
+
const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
|
|
545
|
+
|
|
546
|
+
if (!newRoute) {
|
|
547
|
+
// Route was removed
|
|
548
|
+
await this.nftablesManager.deprovisionRoute(oldRoute);
|
|
549
|
+
} else {
|
|
550
|
+
// Route was updated
|
|
551
|
+
await this.nftablesManager.updateRoute(oldRoute, newRoute);
|
|
552
|
+
}
|
|
505
553
|
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Find new routes to add
|
|
509
|
-
for (const newRoute of newNfTablesRoutes) {
|
|
510
|
-
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
|
511
554
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
555
|
+
// Find new routes to add
|
|
556
|
+
for (const newRoute of newNfTablesRoutes) {
|
|
557
|
+
const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
|
|
558
|
+
|
|
559
|
+
if (!oldRoute) {
|
|
560
|
+
// New route
|
|
561
|
+
await this.nftablesManager.provisionRoute(newRoute);
|
|
562
|
+
}
|
|
515
563
|
}
|
|
516
|
-
}
|
|
517
564
|
|
|
518
|
-
|
|
519
|
-
|
|
565
|
+
// Update routes in RouteManager
|
|
566
|
+
this.routeManager.updateRoutes(newRoutes);
|
|
520
567
|
|
|
521
|
-
|
|
522
|
-
|
|
568
|
+
// Get the new set of required ports
|
|
569
|
+
const requiredPorts = this.routeManager.getListeningPorts();
|
|
523
570
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
571
|
+
// Update port listeners to match the new configuration
|
|
572
|
+
await this.portManager.updatePorts(requiredPorts);
|
|
573
|
+
|
|
574
|
+
// Update settings with the new routes
|
|
575
|
+
this.settings.routes = newRoutes;
|
|
529
576
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
577
|
+
// If NetworkProxy is initialized, resync the configurations
|
|
578
|
+
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
579
|
+
await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
|
|
580
|
+
}
|
|
534
581
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
582
|
+
// Update certificate manager with new routes
|
|
583
|
+
if (this.certManager) {
|
|
584
|
+
const existingAcmeOptions = this.certManager.getAcmeOptions();
|
|
585
|
+
const existingState = this.certManager.getState();
|
|
586
|
+
|
|
587
|
+
// Store global state before stopping
|
|
588
|
+
this.globalChallengeRouteActive = existingState.challengeRouteActive;
|
|
589
|
+
|
|
590
|
+
await this.certManager.stop();
|
|
591
|
+
|
|
592
|
+
// Verify the challenge route has been properly removed
|
|
593
|
+
await this.verifyChallengeRouteRemoved();
|
|
594
|
+
|
|
595
|
+
// Create new certificate manager with preserved state
|
|
596
|
+
this.certManager = await this.createCertificateManager(
|
|
597
|
+
newRoutes,
|
|
598
|
+
'./certs',
|
|
599
|
+
existingAcmeOptions,
|
|
600
|
+
{ challengeRouteActive: this.globalChallengeRouteActive }
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
547
604
|
}
|
|
548
605
|
|
|
549
606
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple mutex implementation for async operations
|
|
3
|
+
*/
|
|
4
|
+
export class Mutex {
|
|
5
|
+
private isLocked: boolean = false;
|
|
6
|
+
private waitQueue: Array<() => void> = [];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Acquire the lock
|
|
10
|
+
*/
|
|
11
|
+
async acquire(): Promise<void> {
|
|
12
|
+
return new Promise<void>((resolve) => {
|
|
13
|
+
if (!this.isLocked) {
|
|
14
|
+
this.isLocked = true;
|
|
15
|
+
resolve();
|
|
16
|
+
} else {
|
|
17
|
+
this.waitQueue.push(resolve);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Release the lock
|
|
24
|
+
*/
|
|
25
|
+
release(): void {
|
|
26
|
+
this.isLocked = false;
|
|
27
|
+
const nextResolve = this.waitQueue.shift();
|
|
28
|
+
if (nextResolve) {
|
|
29
|
+
this.isLocked = true;
|
|
30
|
+
nextResolve();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run a function exclusively with the lock
|
|
36
|
+
*/
|
|
37
|
+
async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
|
|
38
|
+
await this.acquire();
|
|
39
|
+
try {
|
|
40
|
+
return await fn();
|
|
41
|
+
} finally {
|
|
42
|
+
this.release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../plugins.js';
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
IForwardConfig as ILegacyForwardConfig,
|
|
5
|
-
IDomainOptions
|
|
6
|
-
} from './types.js';
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
IForwardConfig
|
|
10
|
-
} from '../forwarding/config/forwarding-types.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Converts a forwarding configuration target to the legacy format
|
|
14
|
-
* for Port80Handler
|
|
15
|
-
*/
|
|
16
|
-
export function convertToLegacyForwardConfig(
|
|
17
|
-
forwardConfig: IForwardConfig
|
|
18
|
-
): ILegacyForwardConfig {
|
|
19
|
-
// Determine host from the target configuration
|
|
20
|
-
const host = Array.isArray(forwardConfig.target.host)
|
|
21
|
-
? forwardConfig.target.host[0] // Use the first host in the array
|
|
22
|
-
: forwardConfig.target.host;
|
|
23
|
-
|
|
24
|
-
// Extract port number, handling different port formats
|
|
25
|
-
let port: number;
|
|
26
|
-
if (typeof forwardConfig.target.port === 'function') {
|
|
27
|
-
// Use a default port for function-based ports in adapter context
|
|
28
|
-
port = 80;
|
|
29
|
-
} else if (forwardConfig.target.port === 'preserve') {
|
|
30
|
-
// For 'preserve', use the default port 80 in this adapter context
|
|
31
|
-
port = 80;
|
|
32
|
-
} else {
|
|
33
|
-
port = forwardConfig.target.port;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
ip: host,
|
|
38
|
-
port: port
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Creates Port80Handler domain options from a domain name and forwarding config
|
|
44
|
-
*/
|
|
45
|
-
export function createPort80HandlerOptions(
|
|
46
|
-
domain: string,
|
|
47
|
-
forwardConfig: IForwardConfig
|
|
48
|
-
): IDomainOptions {
|
|
49
|
-
// Determine if we should redirect HTTP to HTTPS
|
|
50
|
-
let sslRedirect = false;
|
|
51
|
-
if (forwardConfig.http?.redirectToHttps) {
|
|
52
|
-
sslRedirect = true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Determine if ACME maintenance should be enabled
|
|
56
|
-
// Enable by default for termination types, unless explicitly disabled
|
|
57
|
-
const requiresTls =
|
|
58
|
-
forwardConfig.type === 'https-terminate-to-http' ||
|
|
59
|
-
forwardConfig.type === 'https-terminate-to-https';
|
|
60
|
-
|
|
61
|
-
const acmeMaintenance =
|
|
62
|
-
requiresTls &&
|
|
63
|
-
forwardConfig.acme?.enabled !== false;
|
|
64
|
-
|
|
65
|
-
// Set up forwarding configuration
|
|
66
|
-
const options: IDomainOptions = {
|
|
67
|
-
domainName: domain,
|
|
68
|
-
sslRedirect,
|
|
69
|
-
acmeMaintenance
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Add ACME challenge forwarding if configured
|
|
73
|
-
if (forwardConfig.acme?.forwardChallenges) {
|
|
74
|
-
options.acmeForward = {
|
|
75
|
-
ip: Array.isArray(forwardConfig.acme.forwardChallenges.host)
|
|
76
|
-
? forwardConfig.acme.forwardChallenges.host[0]
|
|
77
|
-
: forwardConfig.acme.forwardChallenges.host,
|
|
78
|
-
port: forwardConfig.acme.forwardChallenges.port
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Add HTTP forwarding if this is an HTTP-only config or if HTTP is enabled
|
|
83
|
-
const supportsHttp =
|
|
84
|
-
forwardConfig.type === 'http-only' ||
|
|
85
|
-
(forwardConfig.http?.enabled !== false &&
|
|
86
|
-
(forwardConfig.type === 'https-terminate-to-http' ||
|
|
87
|
-
forwardConfig.type === 'https-terminate-to-https'));
|
|
88
|
-
|
|
89
|
-
if (supportsHttp) {
|
|
90
|
-
// Determine port value handling different formats
|
|
91
|
-
let port: number;
|
|
92
|
-
if (typeof forwardConfig.target.port === 'function') {
|
|
93
|
-
// Use a default port for function-based ports
|
|
94
|
-
port = 80;
|
|
95
|
-
} else if (forwardConfig.target.port === 'preserve') {
|
|
96
|
-
// For 'preserve', use 80 in this adapter context
|
|
97
|
-
port = 80;
|
|
98
|
-
} else {
|
|
99
|
-
port = forwardConfig.target.port;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
options.forward = {
|
|
103
|
-
ip: Array.isArray(forwardConfig.target.host)
|
|
104
|
-
? forwardConfig.target.host[0]
|
|
105
|
-
: forwardConfig.target.host,
|
|
106
|
-
port: port
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return options;
|
|
111
|
-
}
|