@push.rocks/smartproxy 19.2.4 → 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 +15 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +46 -6
- 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 +166 -26
- 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 +55 -4
- 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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { IRouteConfig } from './models/route-types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Global state store for ACME operations
|
|
5
|
+
* Tracks active challenge routes and port allocations
|
|
6
|
+
*/
|
|
7
|
+
export class AcmeStateManager {
|
|
8
|
+
private activeChallengeRoutes: Map<string, IRouteConfig> = new Map();
|
|
9
|
+
private acmePortAllocations: Set<number> = new Set();
|
|
10
|
+
private primaryChallengeRoute: IRouteConfig | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a challenge route is active
|
|
14
|
+
*/
|
|
15
|
+
public isChallengeRouteActive(): boolean {
|
|
16
|
+
return this.activeChallengeRoutes.size > 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register a challenge route as active
|
|
21
|
+
*/
|
|
22
|
+
public addChallengeRoute(route: IRouteConfig): void {
|
|
23
|
+
this.activeChallengeRoutes.set(route.name, route);
|
|
24
|
+
|
|
25
|
+
// Track the primary challenge route
|
|
26
|
+
if (!this.primaryChallengeRoute || route.priority > (this.primaryChallengeRoute.priority || 0)) {
|
|
27
|
+
this.primaryChallengeRoute = route;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Track port allocations
|
|
31
|
+
const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
|
|
32
|
+
ports.forEach(port => this.acmePortAllocations.add(port));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remove a challenge route
|
|
37
|
+
*/
|
|
38
|
+
public removeChallengeRoute(routeName: string): void {
|
|
39
|
+
const route = this.activeChallengeRoutes.get(routeName);
|
|
40
|
+
if (!route) return;
|
|
41
|
+
|
|
42
|
+
this.activeChallengeRoutes.delete(routeName);
|
|
43
|
+
|
|
44
|
+
// Update primary challenge route if needed
|
|
45
|
+
if (this.primaryChallengeRoute?.name === routeName) {
|
|
46
|
+
this.primaryChallengeRoute = null;
|
|
47
|
+
// Find new primary route with highest priority
|
|
48
|
+
let highestPriority = -1;
|
|
49
|
+
for (const [_, activeRoute] of this.activeChallengeRoutes) {
|
|
50
|
+
const priority = activeRoute.priority || 0;
|
|
51
|
+
if (priority > highestPriority) {
|
|
52
|
+
highestPriority = priority;
|
|
53
|
+
this.primaryChallengeRoute = activeRoute;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Update port allocations - only remove if no other routes use this port
|
|
59
|
+
const ports = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
|
|
60
|
+
ports.forEach(port => {
|
|
61
|
+
let portStillUsed = false;
|
|
62
|
+
for (const [_, activeRoute] of this.activeChallengeRoutes) {
|
|
63
|
+
const activePorts = Array.isArray(activeRoute.match.ports) ?
|
|
64
|
+
activeRoute.match.ports : [activeRoute.match.ports];
|
|
65
|
+
if (activePorts.includes(port)) {
|
|
66
|
+
portStillUsed = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!portStillUsed) {
|
|
71
|
+
this.acmePortAllocations.delete(port);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get all active challenge routes
|
|
78
|
+
*/
|
|
79
|
+
public getActiveChallengeRoutes(): IRouteConfig[] {
|
|
80
|
+
return Array.from(this.activeChallengeRoutes.values());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the primary challenge route
|
|
85
|
+
*/
|
|
86
|
+
public getPrimaryChallengeRoute(): IRouteConfig | null {
|
|
87
|
+
return this.primaryChallengeRoute;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a port is allocated for ACME
|
|
92
|
+
*/
|
|
93
|
+
public isPortAllocatedForAcme(port: number): boolean {
|
|
94
|
+
return this.acmePortAllocations.has(port);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get all ACME ports
|
|
99
|
+
*/
|
|
100
|
+
public getAcmePorts(): number[] {
|
|
101
|
+
return Array.from(this.acmePortAllocations);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Clear all state (for shutdown or reset)
|
|
106
|
+
*/
|
|
107
|
+
public clear(): void {
|
|
108
|
+
this.activeChallengeRoutes.clear();
|
|
109
|
+
this.acmePortAllocations.clear();
|
|
110
|
+
this.primaryChallengeRoute = null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -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;
|
|
@@ -44,6 +45,9 @@ export class SmartCertManager {
|
|
|
44
45
|
// Flag to track if provisioning is in progress
|
|
45
46
|
private isProvisioning: boolean = false;
|
|
46
47
|
|
|
48
|
+
// ACME state manager reference
|
|
49
|
+
private acmeStateManager: AcmeStateManager | null = null;
|
|
50
|
+
|
|
47
51
|
constructor(
|
|
48
52
|
private routes: IRouteConfig[],
|
|
49
53
|
private certDir: string = './certs',
|
|
@@ -51,15 +55,39 @@ export class SmartCertManager {
|
|
|
51
55
|
email?: string;
|
|
52
56
|
useProduction?: boolean;
|
|
53
57
|
port?: number;
|
|
58
|
+
},
|
|
59
|
+
private initialState?: {
|
|
60
|
+
challengeRouteActive?: boolean;
|
|
54
61
|
}
|
|
55
62
|
) {
|
|
56
63
|
this.certStore = new CertStore(certDir);
|
|
64
|
+
|
|
65
|
+
// Apply initial state if provided
|
|
66
|
+
if (initialState) {
|
|
67
|
+
this.challengeRouteActive = initialState.challengeRouteActive || false;
|
|
68
|
+
}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
public setNetworkProxy(networkProxy: NetworkProxy): void {
|
|
60
72
|
this.networkProxy = networkProxy;
|
|
61
73
|
}
|
|
62
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
|
+
|
|
63
91
|
/**
|
|
64
92
|
* Set global ACME defaults from top-level configuration
|
|
65
93
|
*/
|
|
@@ -103,9 +131,13 @@ export class SmartCertManager {
|
|
|
103
131
|
|
|
104
132
|
await this.smartAcme.start();
|
|
105
133
|
|
|
106
|
-
// Add challenge route once at initialization
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
141
|
}
|
|
110
142
|
|
|
111
143
|
// Provision certificates for all routes
|
|
@@ -350,8 +382,15 @@ export class SmartCertManager {
|
|
|
350
382
|
* Add challenge route to SmartProxy
|
|
351
383
|
*/
|
|
352
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
|
+
|
|
353
392
|
if (this.challengeRouteActive) {
|
|
354
|
-
console.log('Challenge route already active, skipping');
|
|
393
|
+
console.log('Challenge route already active locally, skipping');
|
|
355
394
|
return;
|
|
356
395
|
}
|
|
357
396
|
|
|
@@ -368,6 +407,12 @@ export class SmartCertManager {
|
|
|
368
407
|
const updatedRoutes = [...this.routes, challengeRoute];
|
|
369
408
|
await this.updateRoutesCallback(updatedRoutes);
|
|
370
409
|
this.challengeRouteActive = true;
|
|
410
|
+
|
|
411
|
+
// Register with state manager
|
|
412
|
+
if (this.acmeStateManager) {
|
|
413
|
+
this.acmeStateManager.addChallengeRoute(challengeRoute);
|
|
414
|
+
}
|
|
415
|
+
|
|
371
416
|
console.log('ACME challenge route successfully added');
|
|
372
417
|
} catch (error) {
|
|
373
418
|
console.error('Failed to add challenge route:', error);
|
|
@@ -395,6 +440,12 @@ export class SmartCertManager {
|
|
|
395
440
|
const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
|
|
396
441
|
await this.updateRoutesCallback(filteredRoutes);
|
|
397
442
|
this.challengeRouteActive = false;
|
|
443
|
+
|
|
444
|
+
// Remove from state manager
|
|
445
|
+
if (this.acmeStateManager) {
|
|
446
|
+
this.acmeStateManager.removeChallengeRoute('acme-challenge');
|
|
447
|
+
}
|
|
448
|
+
|
|
398
449
|
console.log('ACME challenge route successfully removed');
|
|
399
450
|
} catch (error) {
|
|
400
451
|
console.error('Failed to remove challenge route:', error);
|
|
@@ -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
|
-
}
|