@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.
@@ -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
- for (const route of certRoutes) {
118
- try {
119
- await this.provisionCertificate(route);
120
- } catch (error) {
121
- console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
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
- // Add challenge route before requesting certificate
190
- await this.addChallengeRoute();
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
- console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
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
- // Handle outer try-catch from adding challenge route
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
- const updatedRoutes = [...this.routes, challengeRoute];
350
- await this.updateRoutesCallback(updatedRoutes);
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
- const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
362
- await this.updateRoutesCallback(filteredRoutes);
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
- // Remove any active challenge routes
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
- console.log(`Updating routes (${newRoutes.length} routes)`);
529
+ return this.routeUpdateLock.runExclusive(async () => {
530
+ console.log(`Updating routes (${newRoutes.length} routes)`);
484
531
 
485
- // Get existing routes that use NFTables
486
- const oldNfTablesRoutes = this.settings.routes.filter(
487
- r => r.action.forwardingEngine === 'nftables'
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
- if (!newRoute) {
500
- // Route was removed
501
- await this.nftablesManager.deprovisionRoute(oldRoute);
502
- } else {
503
- // Route was updated
504
- await this.nftablesManager.updateRoute(oldRoute, newRoute);
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
- if (!oldRoute) {
513
- // New route
514
- await this.nftablesManager.provisionRoute(newRoute);
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
- // Update routes in RouteManager
519
- this.routeManager.updateRoutes(newRoutes);
565
+ // Update routes in RouteManager
566
+ this.routeManager.updateRoutes(newRoutes);
520
567
 
521
- // Get the new set of required ports
522
- const requiredPorts = this.routeManager.getListeningPorts();
568
+ // Get the new set of required ports
569
+ const requiredPorts = this.routeManager.getListeningPorts();
523
570
 
524
- // Update port listeners to match the new configuration
525
- await this.portManager.updatePorts(requiredPorts);
526
-
527
- // Update settings with the new routes
528
- this.settings.routes = newRoutes;
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
- // If NetworkProxy is initialized, resync the configurations
531
- if (this.networkProxyBridge.getNetworkProxy()) {
532
- await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
533
- }
577
+ // If NetworkProxy is initialized, resync the configurations
578
+ if (this.networkProxyBridge.getNetworkProxy()) {
579
+ await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
580
+ }
534
581
 
535
- // Update certificate manager with new routes
536
- if (this.certManager) {
537
- const existingAcmeOptions = this.certManager.getAcmeOptions();
538
- await this.certManager.stop();
539
-
540
- // Use the helper method to create and configure the certificate manager
541
- this.certManager = await this.createCertificateManager(
542
- newRoutes,
543
- './certs',
544
- existingAcmeOptions
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
- }