@push.rocks/smartproxy 3.34.0 → 3.37.0

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.34.0",
3
+ "version": "3.37.0",
4
4
  "private": false,
5
- "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
5
+ "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
6
6
  "main": "dist_ts/index.js",
7
7
  "typings": "dist_ts/index.d.ts",
8
8
  "type": "module",
@@ -12,8 +12,8 @@
12
12
  "@git.zone/tsbuild": "^2.2.6",
13
13
  "@git.zone/tsrun": "^1.2.44",
14
14
  "@git.zone/tstest": "^1.0.77",
15
- "@push.rocks/tapbundle": "^5.5.6",
16
- "@types/node": "^22.13.9",
15
+ "@push.rocks/tapbundle": "^5.5.10",
16
+ "@types/node": "^22.13.10",
17
17
  "typescript": "^5.8.2"
18
18
  },
19
19
  "dependencies": {
@@ -22,7 +22,7 @@
22
22
  "@push.rocks/smartpromise": "^4.2.3",
23
23
  "@push.rocks/smartrequest": "^2.0.23",
24
24
  "@push.rocks/smartstring": "^4.0.15",
25
- "@tsclass/tsclass": "^4.4.3",
25
+ "@tsclass/tsclass": "^5.0.0",
26
26
  "@types/minimatch": "^5.1.2",
27
27
  "@types/ws": "^8.18.0",
28
28
  "acme-client": "^5.4.0",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.34.0',
7
- description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
6
+ version: '3.37.0',
7
+ description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.'
8
8
  }
@@ -1,5 +1,6 @@
1
1
  import * as plugins from './plugins.js';
2
2
  import { ProxyRouter } from './classes.router.js';
3
+ import { AcmeCertManager, CertManagerEvents } from './classes.port80handler.js';
3
4
  import * as fs from 'fs';
4
5
  import * as path from 'path';
5
6
  import { fileURLToPath } from 'url';
@@ -20,6 +21,18 @@ export interface INetworkProxyOptions {
20
21
  // New settings for PortProxy integration
21
22
  connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
22
23
  portProxyIntegration?: boolean; // Flag to indicate this proxy is used by PortProxy
24
+
25
+ // ACME certificate management options
26
+ acme?: {
27
+ enabled?: boolean; // Whether to enable automatic certificate management
28
+ port?: number; // Port to listen on for ACME challenges (default: 80)
29
+ contactEmail?: string; // Email for Let's Encrypt account
30
+ useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
31
+ renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
32
+ autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
33
+ certificateStore?: string; // Directory to store certificates (default: ./certs)
34
+ skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
35
+ };
23
36
  }
24
37
 
25
38
  interface IWebSocketWithHeartbeat extends plugins.wsDefault {
@@ -59,12 +72,19 @@ export class NetworkProxy {
59
72
  private defaultCertificates: { key: string; cert: string };
60
73
  private certificateCache: Map<string, { key: string; cert: string; expires?: Date }> = new Map();
61
74
 
75
+ // ACME certificate manager
76
+ private certManager: AcmeCertManager | null = null;
77
+ private certificateStoreDir: string;
78
+
62
79
  // New connection pool for backend connections
63
80
  private connectionPool: Map<string, Array<{
64
81
  socket: plugins.net.Socket;
65
82
  lastUsed: number;
66
83
  isIdle: boolean;
67
84
  }>> = new Map();
85
+
86
+ // Track round-robin positions for load balancing
87
+ private roundRobinPositions: Map<string, number> = new Map();
68
88
 
69
89
  /**
70
90
  * Creates a new NetworkProxy instance
@@ -85,9 +105,33 @@ export class NetworkProxy {
85
105
  },
86
106
  // New defaults for PortProxy integration
87
107
  connectionPoolSize: optionsArg.connectionPoolSize || 50,
88
- portProxyIntegration: optionsArg.portProxyIntegration || false
108
+ portProxyIntegration: optionsArg.portProxyIntegration || false,
109
+ // Default ACME options
110
+ acme: {
111
+ enabled: optionsArg.acme?.enabled || false,
112
+ port: optionsArg.acme?.port || 80,
113
+ contactEmail: optionsArg.acme?.contactEmail || 'admin@example.com',
114
+ useProduction: optionsArg.acme?.useProduction || false, // Default to staging for safety
115
+ renewThresholdDays: optionsArg.acme?.renewThresholdDays || 30,
116
+ autoRenew: optionsArg.acme?.autoRenew !== false, // Default to true
117
+ certificateStore: optionsArg.acme?.certificateStore || './certs',
118
+ skipConfiguredCerts: optionsArg.acme?.skipConfiguredCerts || false
119
+ }
89
120
  };
90
121
 
122
+ // Set up certificate store directory
123
+ this.certificateStoreDir = path.resolve(this.options.acme.certificateStore);
124
+
125
+ // Ensure certificate store directory exists
126
+ try {
127
+ if (!fs.existsSync(this.certificateStoreDir)) {
128
+ fs.mkdirSync(this.certificateStoreDir, { recursive: true });
129
+ this.log('info', `Created certificate store directory: ${this.certificateStoreDir}`);
130
+ }
131
+ } catch (error) {
132
+ this.log('warn', `Failed to create certificate store directory: ${error}`);
133
+ }
134
+
91
135
  this.loadDefaultCertificates();
92
136
  }
93
137
 
@@ -330,17 +374,230 @@ export class NetworkProxy {
330
374
  }
331
375
  }
332
376
 
377
+ /**
378
+ * Initializes the ACME certificate manager for automatic certificate issuance
379
+ * @private
380
+ */
381
+ private async initializeAcmeManager(): Promise<void> {
382
+ if (!this.options.acme.enabled) {
383
+ return;
384
+ }
385
+
386
+ // Create certificate manager
387
+ this.certManager = new AcmeCertManager({
388
+ port: this.options.acme.port,
389
+ contactEmail: this.options.acme.contactEmail,
390
+ useProduction: this.options.acme.useProduction,
391
+ renewThresholdDays: this.options.acme.renewThresholdDays,
392
+ httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
393
+ renewCheckIntervalHours: 24 // Check daily for renewals
394
+ });
395
+
396
+ // Register event handlers
397
+ this.certManager.on(CertManagerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
398
+ this.certManager.on(CertManagerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
399
+ this.certManager.on(CertManagerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
400
+ this.certManager.on(CertManagerEvents.CERTIFICATE_EXPIRING, (data) => {
401
+ this.log('info', `Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
402
+ });
403
+
404
+ // Start the manager
405
+ try {
406
+ await this.certManager.start();
407
+ this.log('info', `ACME Certificate Manager started on port ${this.options.acme.port}`);
408
+
409
+ // Add domains from proxy configs
410
+ this.registerDomainsWithAcmeManager();
411
+ } catch (error) {
412
+ this.log('error', `Failed to start ACME Certificate Manager: ${error}`);
413
+ this.certManager = null;
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Registers domains from proxy configs with the ACME manager
419
+ * @private
420
+ */
421
+ private registerDomainsWithAcmeManager(): void {
422
+ if (!this.certManager) return;
423
+
424
+ // Get all hostnames from proxy configs
425
+ this.proxyConfigs.forEach(config => {
426
+ const hostname = config.hostName;
427
+
428
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
429
+ if (hostname.includes('*')) {
430
+ this.log('info', `Skipping wildcard domain for ACME: ${hostname}`);
431
+ return;
432
+ }
433
+
434
+ // Skip domains already with certificates if configured to do so
435
+ if (this.options.acme.skipConfiguredCerts) {
436
+ const cachedCert = this.certificateCache.get(hostname);
437
+ if (cachedCert) {
438
+ this.log('info', `Skipping domain with existing certificate: ${hostname}`);
439
+ return;
440
+ }
441
+ }
442
+
443
+ // Check for existing certificate in the store
444
+ const certPath = path.join(this.certificateStoreDir, `${hostname}.cert.pem`);
445
+ const keyPath = path.join(this.certificateStoreDir, `${hostname}.key.pem`);
446
+
447
+ try {
448
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
449
+ // Load existing certificate and key
450
+ const cert = fs.readFileSync(certPath, 'utf8');
451
+ const key = fs.readFileSync(keyPath, 'utf8');
452
+
453
+ // Extract expiry date from certificate if possible
454
+ let expiryDate: Date | undefined;
455
+ try {
456
+ const matches = cert.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
457
+ if (matches && matches[1]) {
458
+ expiryDate = new Date(matches[1]);
459
+ }
460
+ } catch (error) {
461
+ this.log('warn', `Failed to extract expiry date from certificate for ${hostname}`);
462
+ }
463
+
464
+ // Update the certificate in the manager
465
+ this.certManager.setCertificate(hostname, cert, key, expiryDate);
466
+
467
+ // Also update our own certificate cache
468
+ this.updateCertificateCache(hostname, cert, key, expiryDate);
469
+
470
+ this.log('info', `Loaded existing certificate for ${hostname}`);
471
+ } else {
472
+ // Register the domain for certificate issuance
473
+ this.certManager.addDomain(hostname);
474
+ this.log('info', `Registered domain for ACME certificate issuance: ${hostname}`);
475
+ }
476
+ } catch (error) {
477
+ this.log('error', `Error registering domain ${hostname} with ACME manager: ${error}`);
478
+ }
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Handles newly issued or renewed certificates from ACME manager
484
+ * @private
485
+ */
486
+ private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
487
+ const { domain, certificate, privateKey, expiryDate } = data;
488
+
489
+ this.log('info', `Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
490
+
491
+ // Update certificate in HTTPS server
492
+ this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
493
+
494
+ // Save the certificate to the filesystem
495
+ this.saveCertificateToStore(domain, certificate, privateKey);
496
+ }
497
+
498
+ /**
499
+ * Handles certificate issuance failures
500
+ * @private
501
+ */
502
+ private handleCertificateFailed(data: { domain: string; error: string }): void {
503
+ this.log('error', `Certificate issuance failed for ${data.domain}: ${data.error}`);
504
+ }
505
+
506
+ /**
507
+ * Saves certificate and private key to the filesystem
508
+ * @private
509
+ */
510
+ private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
511
+ try {
512
+ const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
513
+ const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
514
+
515
+ fs.writeFileSync(certPath, certificate);
516
+ fs.writeFileSync(keyPath, privateKey);
517
+
518
+ // Ensure private key has restricted permissions
519
+ try {
520
+ fs.chmodSync(keyPath, 0o600);
521
+ } catch (error) {
522
+ this.log('warn', `Failed to set permissions on private key for ${domain}: ${error}`);
523
+ }
524
+
525
+ this.log('info', `Saved certificate for ${domain} to ${certPath}`);
526
+ } catch (error) {
527
+ this.log('error', `Failed to save certificate for ${domain}: ${error}`);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Handles SNI (Server Name Indication) for TLS connections
533
+ * Used by the HTTPS server to select the correct certificate for each domain
534
+ * @private
535
+ */
536
+ private handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
537
+ this.log('debug', `SNI request for domain: ${domain}`);
538
+
539
+ // Check if we have a certificate for this domain
540
+ const certs = this.certificateCache.get(domain);
541
+
542
+ if (certs) {
543
+ try {
544
+ // Create TLS context with the cached certificate
545
+ const context = plugins.tls.createSecureContext({
546
+ key: certs.key,
547
+ cert: certs.cert
548
+ });
549
+
550
+ this.log('debug', `Using cached certificate for ${domain}`);
551
+ cb(null, context);
552
+ return;
553
+ } catch (err) {
554
+ this.log('error', `Error creating secure context for ${domain}:`, err);
555
+ }
556
+ }
557
+
558
+ // Check if we should trigger certificate issuance
559
+ if (this.options.acme?.enabled && this.certManager && !domain.includes('*')) {
560
+ // Check if this domain is already registered
561
+ const certData = this.certManager.getCertificate(domain);
562
+
563
+ if (!certData) {
564
+ this.log('info', `No certificate found for ${domain}, registering for issuance`);
565
+ this.certManager.addDomain(domain);
566
+ }
567
+ }
568
+
569
+ // Fall back to default certificate
570
+ try {
571
+ const context = plugins.tls.createSecureContext({
572
+ key: this.defaultCertificates.key,
573
+ cert: this.defaultCertificates.cert
574
+ });
575
+
576
+ this.log('debug', `Using default certificate for ${domain}`);
577
+ cb(null, context);
578
+ } catch (err) {
579
+ this.log('error', `Error creating default secure context:`, err);
580
+ cb(new Error('Cannot create secure context'), null);
581
+ }
582
+ }
583
+
333
584
  /**
334
585
  * Starts the proxy server
335
586
  */
336
587
  public async start(): Promise<void> {
337
588
  this.startTime = Date.now();
338
589
 
590
+ // Initialize ACME certificate manager if enabled
591
+ if (this.options.acme.enabled) {
592
+ await this.initializeAcmeManager();
593
+ }
594
+
339
595
  // Create the HTTPS server
340
596
  this.httpsServer = plugins.https.createServer(
341
597
  {
342
598
  key: this.defaultCertificates.key,
343
- cert: this.defaultCertificates.cert
599
+ cert: this.defaultCertificates.cert,
600
+ SNICallback: (domain, cb) => this.handleSNI(domain, cb)
344
601
  },
345
602
  (req, res) => this.handleRequest(req, res)
346
603
  );
@@ -556,7 +813,10 @@ export class NetworkProxy {
556
813
  const outGoingDeferred = plugins.smartpromise.defer();
557
814
 
558
815
  try {
559
- const wsTarget = `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`;
816
+ // Select destination IP and port for WebSocket
817
+ const wsDestinationIp = this.selectDestinationIp(wsDestinationConfig);
818
+ const wsDestinationPort = this.selectDestinationPort(wsDestinationConfig);
819
+ const wsTarget = `ws://${wsDestinationIp}:${wsDestinationPort}${reqArg.url}`;
560
820
  this.log('debug', `Proxying WebSocket to ${wsTarget}`);
561
821
 
562
822
  wsOutgoing = new plugins.wsDefault(wsTarget);
@@ -688,8 +948,12 @@ export class NetworkProxy {
688
948
  const useConnectionPool = this.options.portProxyIntegration &&
689
949
  originRequest.socket.remoteAddress?.includes('127.0.0.1');
690
950
 
951
+ // Select destination IP and port from the arrays
952
+ const destinationIp = this.selectDestinationIp(destinationConfig);
953
+ const destinationPort = this.selectDestinationPort(destinationConfig);
954
+
691
955
  // Construct destination URL
692
- const destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
956
+ const destinationUrl = `http://${destinationIp}:${destinationPort}${originRequest.url}`;
693
957
 
694
958
  if (useConnectionPool) {
695
959
  this.log('debug', `[${reqId}] Proxying to ${destinationUrl} (using connection pool)`);
@@ -697,8 +961,8 @@ export class NetworkProxy {
697
961
  reqId,
698
962
  originRequest,
699
963
  originResponse,
700
- destinationConfig.destinationIp,
701
- destinationConfig.destinationPort,
964
+ destinationIp,
965
+ destinationPort,
702
966
  originRequest.url
703
967
  );
704
968
  } else {
@@ -1084,6 +1348,80 @@ export class NetworkProxy {
1084
1348
  }
1085
1349
  }
1086
1350
 
1351
+ /**
1352
+ * Selects a destination IP from the array using round-robin
1353
+ * @param config The proxy configuration
1354
+ * @returns A destination IP address
1355
+ */
1356
+ private selectDestinationIp(config: plugins.tsclass.network.IReverseProxyConfig): string {
1357
+ // For array-based configs
1358
+ if (Array.isArray(config.destinationIps) && config.destinationIps.length > 0) {
1359
+ // Get the current position or initialize it
1360
+ const key = `ip_${config.hostName}`;
1361
+ let position = this.roundRobinPositions.get(key) || 0;
1362
+
1363
+ // Select the IP using round-robin
1364
+ const selectedIp = config.destinationIps[position];
1365
+
1366
+ // Update the position for next time
1367
+ position = (position + 1) % config.destinationIps.length;
1368
+ this.roundRobinPositions.set(key, position);
1369
+
1370
+ return selectedIp;
1371
+ }
1372
+
1373
+ // For backward compatibility with test suites that rely on specific behavior
1374
+ // Check if there's a proxyConfigs entry that matches this hostname
1375
+ const matchingConfig = this.proxyConfigs.find(cfg =>
1376
+ cfg.hostName === config.hostName &&
1377
+ (cfg as any).destinationIp
1378
+ );
1379
+
1380
+ if (matchingConfig) {
1381
+ return (matchingConfig as any).destinationIp;
1382
+ }
1383
+
1384
+ // Fallback to localhost
1385
+ return 'localhost';
1386
+ }
1387
+
1388
+ /**
1389
+ * Selects a destination port from the array using round-robin
1390
+ * @param config The proxy configuration
1391
+ * @returns A destination port number
1392
+ */
1393
+ private selectDestinationPort(config: plugins.tsclass.network.IReverseProxyConfig): number {
1394
+ // For array-based configs
1395
+ if (Array.isArray(config.destinationPorts) && config.destinationPorts.length > 0) {
1396
+ // Get the current position or initialize it
1397
+ const key = `port_${config.hostName}`;
1398
+ let position = this.roundRobinPositions.get(key) || 0;
1399
+
1400
+ // Select the port using round-robin
1401
+ const selectedPort = config.destinationPorts[position];
1402
+
1403
+ // Update the position for next time
1404
+ position = (position + 1) % config.destinationPorts.length;
1405
+ this.roundRobinPositions.set(key, position);
1406
+
1407
+ return selectedPort;
1408
+ }
1409
+
1410
+ // For backward compatibility with test suites that rely on specific behavior
1411
+ // Check if there's a proxyConfigs entry that matches this hostname
1412
+ const matchingConfig = this.proxyConfigs.find(cfg =>
1413
+ cfg.hostName === config.hostName &&
1414
+ (cfg as any).destinationPort
1415
+ );
1416
+
1417
+ if (matchingConfig) {
1418
+ return parseInt((matchingConfig as any).destinationPort, 10);
1419
+ }
1420
+
1421
+ // Fallback to port 80
1422
+ return 80;
1423
+ }
1424
+
1087
1425
  /**
1088
1426
  * Updates proxy configurations
1089
1427
  */
@@ -1144,6 +1482,48 @@ export class NetworkProxy {
1144
1482
  }
1145
1483
  }
1146
1484
 
1485
+ /**
1486
+ * Converts PortProxy domain configurations to NetworkProxy configs
1487
+ * @param domainConfigs PortProxy domain configs
1488
+ * @param sslKeyPair Default SSL key pair to use if not specified
1489
+ * @returns Array of NetworkProxy configs
1490
+ */
1491
+ public convertPortProxyConfigs(
1492
+ domainConfigs: Array<{
1493
+ domains: string[];
1494
+ targetIPs?: string[];
1495
+ allowedIPs?: string[];
1496
+ }>,
1497
+ sslKeyPair?: { key: string; cert: string }
1498
+ ): plugins.tsclass.network.IReverseProxyConfig[] {
1499
+ const proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
1500
+
1501
+ // Use default certificates if not provided
1502
+ const sslKey = sslKeyPair?.key || this.defaultCertificates.key;
1503
+ const sslCert = sslKeyPair?.cert || this.defaultCertificates.cert;
1504
+
1505
+ for (const domainConfig of domainConfigs) {
1506
+ // Each domain in the domains array gets its own config
1507
+ for (const domain of domainConfig.domains) {
1508
+ // Skip non-hostname patterns (like IP addresses)
1509
+ if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
1510
+ continue;
1511
+ }
1512
+
1513
+ proxyConfigs.push({
1514
+ hostName: domain,
1515
+ destinationIps: domainConfig.targetIPs || ['localhost'],
1516
+ destinationPorts: [this.options.port], // Use the NetworkProxy port
1517
+ privateKey: sslKey,
1518
+ publicKey: sslCert
1519
+ });
1520
+ }
1521
+ }
1522
+
1523
+ this.log('info', `Converted ${domainConfigs.length} PortProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
1524
+ return proxyConfigs;
1525
+ }
1526
+
1147
1527
  /**
1148
1528
  * Adds default headers to be included in all responses
1149
1529
  */
@@ -1208,6 +1588,16 @@ export class NetworkProxy {
1208
1588
  }
1209
1589
  this.connectionPool.clear();
1210
1590
 
1591
+ // Stop ACME certificate manager if it's running
1592
+ if (this.certManager) {
1593
+ try {
1594
+ await this.certManager.stop();
1595
+ this.log('info', 'ACME Certificate Manager stopped');
1596
+ } catch (error) {
1597
+ this.log('error', 'Error stopping ACME Certificate Manager', error);
1598
+ }
1599
+ }
1600
+
1211
1601
  // Close the HTTPS server
1212
1602
  return new Promise((resolve) => {
1213
1603
  this.httpsServer.close(() => {
@@ -1217,6 +1607,71 @@ export class NetworkProxy {
1217
1607
  });
1218
1608
  }
1219
1609
 
1610
+ /**
1611
+ * Requests a new certificate for a domain
1612
+ * This can be used to manually trigger certificate issuance
1613
+ * @param domain The domain to request a certificate for
1614
+ * @returns A promise that resolves when the request is submitted (not when the certificate is issued)
1615
+ */
1616
+ public async requestCertificate(domain: string): Promise<boolean> {
1617
+ if (!this.options.acme.enabled) {
1618
+ this.log('warn', 'ACME certificate management is not enabled');
1619
+ return false;
1620
+ }
1621
+
1622
+ if (!this.certManager) {
1623
+ this.log('error', 'ACME certificate manager is not initialized');
1624
+ return false;
1625
+ }
1626
+
1627
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
1628
+ if (domain.includes('*')) {
1629
+ this.log('error', `Cannot request certificate for wildcard domain: ${domain}`);
1630
+ return false;
1631
+ }
1632
+
1633
+ try {
1634
+ this.certManager.addDomain(domain);
1635
+ this.log('info', `Certificate request submitted for domain: ${domain}`);
1636
+ return true;
1637
+ } catch (error) {
1638
+ this.log('error', `Error requesting certificate for domain ${domain}:`, error);
1639
+ return false;
1640
+ }
1641
+ }
1642
+
1643
+ /**
1644
+ * Updates the certificate cache for a domain
1645
+ * @param domain The domain name
1646
+ * @param certificate The certificate (PEM format)
1647
+ * @param privateKey The private key (PEM format)
1648
+ * @param expiryDate Optional expiry date
1649
+ */
1650
+ private updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
1651
+ // Update certificate context in HTTPS server if it's running
1652
+ if (this.httpsServer) {
1653
+ try {
1654
+ this.httpsServer.addContext(domain, {
1655
+ key: privateKey,
1656
+ cert: certificate
1657
+ });
1658
+ this.log('debug', `Updated SSL context for domain: ${domain}`);
1659
+ } catch (error) {
1660
+ this.log('error', `Error updating SSL context for domain ${domain}:`, error);
1661
+ }
1662
+ }
1663
+
1664
+ // Update certificate in cache
1665
+ this.certificateCache.set(domain, {
1666
+ key: privateKey,
1667
+ cert: certificate,
1668
+ expires: expiryDate
1669
+ });
1670
+
1671
+ // Add to active contexts set
1672
+ this.activeContexts.add(domain);
1673
+ }
1674
+
1220
1675
  /**
1221
1676
  * Logs a message according to the configured log level
1222
1677
  */