@push.rocks/smartproxy 5.0.0 → 6.0.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.
Files changed (78) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.pp.interfaces.d.ts +23 -0
  3. package/dist_ts/classes.pp.networkproxybridge.d.ts +15 -1
  4. package/dist_ts/classes.pp.networkproxybridge.js +116 -21
  5. package/dist_ts/classes.pp.portproxy.d.ts +20 -4
  6. package/dist_ts/classes.pp.portproxy.js +321 -22
  7. package/dist_ts/index.d.ts +6 -6
  8. package/dist_ts/index.js +7 -7
  9. package/dist_ts/networkproxy/classes.np.certificatemanager.d.ts +77 -0
  10. package/dist_ts/networkproxy/classes.np.certificatemanager.js +354 -0
  11. package/dist_ts/networkproxy/classes.np.connectionpool.d.ts +47 -0
  12. package/dist_ts/networkproxy/classes.np.connectionpool.js +210 -0
  13. package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +117 -0
  14. package/dist_ts/networkproxy/classes.np.networkproxy.js +375 -0
  15. package/dist_ts/networkproxy/classes.np.requesthandler.d.ts +51 -0
  16. package/dist_ts/networkproxy/classes.np.requesthandler.js +210 -0
  17. package/dist_ts/networkproxy/classes.np.types.d.ts +82 -0
  18. package/dist_ts/networkproxy/classes.np.types.js +35 -0
  19. package/dist_ts/networkproxy/classes.np.websockethandler.d.ts +38 -0
  20. package/dist_ts/networkproxy/classes.np.websockethandler.js +188 -0
  21. package/dist_ts/networkproxy/index.d.ts +6 -0
  22. package/dist_ts/networkproxy/index.js +8 -0
  23. package/dist_ts/nfttablesproxy/classes.nftablesproxy.d.ts +219 -0
  24. package/dist_ts/nfttablesproxy/classes.nftablesproxy.js +1542 -0
  25. package/dist_ts/port80handler/classes.port80handler.d.ts +260 -0
  26. package/dist_ts/port80handler/classes.port80handler.js +928 -0
  27. package/dist_ts/smartproxy/classes.pp.connectionhandler.d.ts +39 -0
  28. package/dist_ts/smartproxy/classes.pp.connectionhandler.js +754 -0
  29. package/dist_ts/smartproxy/classes.pp.connectionmanager.d.ts +78 -0
  30. package/dist_ts/smartproxy/classes.pp.connectionmanager.js +378 -0
  31. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.d.ts +55 -0
  32. package/dist_ts/smartproxy/classes.pp.domainconfigmanager.js +103 -0
  33. package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +133 -0
  34. package/dist_ts/smartproxy/classes.pp.interfaces.js +2 -0
  35. package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +57 -0
  36. package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +306 -0
  37. package/dist_ts/smartproxy/classes.pp.portrangemanager.d.ts +56 -0
  38. package/dist_ts/smartproxy/classes.pp.portrangemanager.js +179 -0
  39. package/dist_ts/smartproxy/classes.pp.securitymanager.d.ts +47 -0
  40. package/dist_ts/smartproxy/classes.pp.securitymanager.js +126 -0
  41. package/dist_ts/smartproxy/classes.pp.snihandler.d.ts +153 -0
  42. package/dist_ts/smartproxy/classes.pp.snihandler.js +1053 -0
  43. package/dist_ts/smartproxy/classes.pp.timeoutmanager.d.ts +47 -0
  44. package/dist_ts/smartproxy/classes.pp.timeoutmanager.js +154 -0
  45. package/dist_ts/smartproxy/classes.pp.tlsalert.d.ts +149 -0
  46. package/dist_ts/smartproxy/classes.pp.tlsalert.js +225 -0
  47. package/dist_ts/smartproxy/classes.pp.tlsmanager.d.ts +57 -0
  48. package/dist_ts/smartproxy/classes.pp.tlsmanager.js +132 -0
  49. package/dist_ts/smartproxy/classes.smartproxy.d.ts +64 -0
  50. package/dist_ts/smartproxy/classes.smartproxy.js +567 -0
  51. package/package.json +1 -1
  52. package/readme.md +77 -27
  53. package/ts/00_commitinfo_data.ts +1 -1
  54. package/ts/index.ts +6 -6
  55. package/ts/networkproxy/classes.np.certificatemanager.ts +398 -0
  56. package/ts/networkproxy/classes.np.connectionpool.ts +241 -0
  57. package/ts/networkproxy/classes.np.networkproxy.ts +469 -0
  58. package/ts/networkproxy/classes.np.requesthandler.ts +278 -0
  59. package/ts/networkproxy/classes.np.types.ts +123 -0
  60. package/ts/networkproxy/classes.np.websockethandler.ts +226 -0
  61. package/ts/networkproxy/index.ts +7 -0
  62. package/ts/{classes.port80handler.ts → port80handler/classes.port80handler.ts} +249 -1
  63. package/ts/{classes.pp.connectionhandler.ts → smartproxy/classes.pp.connectionhandler.ts} +1 -1
  64. package/ts/{classes.pp.connectionmanager.ts → smartproxy/classes.pp.connectionmanager.ts} +1 -1
  65. package/ts/{classes.pp.domainconfigmanager.ts → smartproxy/classes.pp.domainconfigmanager.ts} +1 -1
  66. package/ts/{classes.pp.interfaces.ts → smartproxy/classes.pp.interfaces.ts} +31 -5
  67. package/ts/{classes.pp.networkproxybridge.ts → smartproxy/classes.pp.networkproxybridge.ts} +129 -28
  68. package/ts/{classes.pp.securitymanager.ts → smartproxy/classes.pp.securitymanager.ts} +1 -1
  69. package/ts/{classes.pp.tlsmanager.ts → smartproxy/classes.pp.tlsmanager.ts} +1 -1
  70. package/ts/smartproxy/classes.smartproxy.ts +679 -0
  71. package/ts/classes.networkproxy.ts +0 -1730
  72. package/ts/classes.pp.acmemanager.ts +0 -149
  73. package/ts/classes.pp.portproxy.ts +0 -344
  74. /package/ts/{classes.nftablesproxy.ts → nfttablesproxy/classes.nftablesproxy.ts} +0 -0
  75. /package/ts/{classes.pp.portrangemanager.ts → smartproxy/classes.pp.portrangemanager.ts} +0 -0
  76. /package/ts/{classes.pp.snihandler.ts → smartproxy/classes.pp.snihandler.ts} +0 -0
  77. /package/ts/{classes.pp.timeoutmanager.ts → smartproxy/classes.pp.timeoutmanager.ts} +0 -0
  78. /package/ts/{classes.pp.tlsalert.ts → smartproxy/classes.pp.tlsalert.ts} +0 -0
package/readme.md CHANGED
@@ -16,7 +16,7 @@ flowchart TB
16
16
  HTTP80[HTTP Port 80\nSslRedirect]
17
17
  HTTPS443[HTTPS Port 443\nNetworkProxy]
18
18
  PortProxy[TCP Port Proxy\nwith SNI routing]
19
- IPTables[IPTablesProxy]
19
+ NfTables[NfTablesProxy]
20
20
  Router[ProxyRouter]
21
21
  ACME[Port80Handler\nACME/Let's Encrypt]
22
22
  Certs[(SSL Certificates)]
@@ -40,7 +40,7 @@ flowchart TB
40
40
  PortProxy -->|Direct TCP| Service2
41
41
  PortProxy -->|Direct TCP| Service3
42
42
 
43
- IPTables -.->|Low-level forwarding| PortProxy
43
+ NfTables -.->|Low-level forwarding| PortProxy
44
44
 
45
45
  HTTP80 -.->|Challenge Response| ACME
46
46
  ACME -.->|Generate/Manage| Certs
@@ -197,7 +197,7 @@ sequenceDiagram
197
197
  - **HTTP to HTTPS Redirection** - Automatically redirect HTTP requests to HTTPS
198
198
  - **Let's Encrypt Integration** - Automatic certificate management using ACME protocol
199
199
  - **IP Filtering** - Control access with IP allow/block lists using glob patterns
200
- - **IPTables Integration** - Direct manipulation of iptables for low-level port forwarding
200
+ - **NfTables Integration** - Direct manipulation of nftables for advanced low-level port forwarding
201
201
  - **Basic Authentication** - Support for basic auth on proxied routes
202
202
  - **Connection Management** - Intelligent connection tracking and cleanup with configurable timeouts
203
203
  - **Browser Compatibility** - Optimized for modern browsers with fixes for common TLS handshake issues
@@ -315,13 +315,13 @@ const portProxy = new PortProxy({
315
315
  portProxy.start();
316
316
  ```
317
317
 
318
- ### IPTables Port Forwarding
318
+ ### NfTables Port Forwarding
319
319
 
320
320
  ```typescript
321
- import { IPTablesProxy } from '@push.rocks/smartproxy';
321
+ import { NfTablesProxy } from '@push.rocks/smartproxy';
322
322
 
323
323
  // Basic usage - forward single port
324
- const basicProxy = new IPTablesProxy({
324
+ const basicProxy = new NfTablesProxy({
325
325
  fromPort: 80,
326
326
  toPort: 8080,
327
327
  toHost: 'localhost',
@@ -330,7 +330,7 @@ const basicProxy = new IPTablesProxy({
330
330
  });
331
331
 
332
332
  // Forward port ranges
333
- const rangeProxy = new IPTablesProxy({
333
+ const rangeProxy = new NfTablesProxy({
334
334
  fromPort: { from: 3000, to: 3010 }, // Forward ports 3000-3010
335
335
  toPort: { from: 8000, to: 8010 }, // To ports 8000-8010
336
336
  protocol: 'tcp', // TCP protocol (default)
@@ -339,19 +339,26 @@ const rangeProxy = new IPTablesProxy({
339
339
  });
340
340
 
341
341
  // Multiple port specifications with IP filtering
342
- const advancedProxy = new IPTablesProxy({
342
+ const advancedProxy = new NfTablesProxy({
343
343
  fromPort: [80, 443, { from: 8000, to: 8010 }], // Multiple ports/ranges
344
344
  toPort: [8080, 8443, { from: 18000, to: 18010 }],
345
345
  allowedSourceIPs: ['10.0.0.0/8', '192.168.1.0/24'], // Only allow these IPs
346
346
  bannedSourceIPs: ['192.168.1.100'], // Explicitly block these IPs
347
- addJumpRule: true, // Use custom chain for better management
348
- checkExistingRules: true // Check for duplicate rules
347
+ useIPSets: true, // Use IP sets for efficient IP management
348
+ forceCleanSlate: false // Clean all NfTablesProxy rules before starting
349
349
  });
350
350
 
351
- // NetworkProxy integration for SSL termination
352
- const sslProxy = new IPTablesProxy({
351
+ // Advanced features: QoS, connection tracking, and NetworkProxy integration
352
+ const advancedProxy = new NfTablesProxy({
353
353
  fromPort: 443,
354
354
  toPort: 8443,
355
+ toHost: 'localhost',
356
+ useAdvancedNAT: true, // Use connection tracking for stateful NAT
357
+ qos: {
358
+ enabled: true,
359
+ maxRate: '10mbps', // Limit bandwidth
360
+ priority: 1 // Set traffic priority (1-10)
361
+ },
355
362
  netProxyIntegration: {
356
363
  enabled: true,
357
364
  redirectLocalhost: true, // Redirect localhost traffic to NetworkProxy
@@ -372,8 +379,25 @@ import { Port80Handler } from '@push.rocks/smartproxy';
372
379
  const acmeHandler = new Port80Handler();
373
380
 
374
381
  // Add domains to manage certificates for
375
- acmeHandler.addDomain('example.com');
376
- acmeHandler.addDomain('api.example.com');
382
+ acmeHandler.addDomain({
383
+ domainName: 'example.com',
384
+ sslRedirect: true,
385
+ acmeMaintenance: true
386
+ });
387
+
388
+ acmeHandler.addDomain({
389
+ domainName: 'api.example.com',
390
+ sslRedirect: true,
391
+ acmeMaintenance: true
392
+ });
393
+
394
+ // Support for glob pattern domains for routing (certificates not issued for glob patterns)
395
+ acmeHandler.addDomain({
396
+ domainName: '*.example.com',
397
+ sslRedirect: true,
398
+ acmeMaintenance: false, // Can't issue certificates for wildcard domains via HTTP-01
399
+ forward: { ip: '192.168.1.10', port: 8080 } // Forward requests to this target
400
+ });
377
401
  ```
378
402
 
379
403
  ## Configuration Options
@@ -412,7 +436,7 @@ acmeHandler.addDomain('api.example.com');
412
436
  | `enableDetailedLogging` | Enable detailed connection logging | false |
413
437
  | `enableRandomizedTimeouts`| Randomize timeouts slightly to prevent thundering herd | true |
414
438
 
415
- ### IPTablesProxy Settings
439
+ ### NfTablesProxy Settings
416
440
 
417
441
  | Option | Description | Default |
418
442
  |-----------------------|---------------------------------------------------|-------------|
@@ -420,18 +444,32 @@ acmeHandler.addDomain('api.example.com');
420
444
  | `toPort` | Destination port(s) or range(s) to forward to | - |
421
445
  | `toHost` | Destination host to forward to | 'localhost' |
422
446
  | `preserveSourceIP` | Preserve the original client IP | false |
423
- | `deleteOnExit` | Remove iptables rules when process exits | false |
447
+ | `deleteOnExit` | Remove nftables rules when process exits | false |
424
448
  | `protocol` | Protocol to forward ('tcp', 'udp', or 'all') | 'tcp' |
425
449
  | `enableLogging` | Enable detailed logging | false |
426
- | `ipv6Support` | Enable IPv6 support with ip6tables | false |
450
+ | `logFormat` | Format for logs ('plain' or 'json') | 'plain' |
451
+ | `ipv6Support` | Enable IPv6 support | false |
427
452
  | `allowedSourceIPs` | Array of IP addresses/CIDR allowed to connect | - |
428
453
  | `bannedSourceIPs` | Array of IP addresses/CIDR blocked from connecting | - |
429
- | `forceCleanSlate` | Clear all IPTablesProxy rules before starting | false |
430
- | `addJumpRule` | Add a custom chain for cleaner rule management | false |
431
- | `checkExistingRules` | Check if rules already exist before adding | true |
454
+ | `useIPSets` | Use nftables sets for efficient IP management | true |
455
+ | `forceCleanSlate` | Clear all NfTablesProxy rules before starting | false |
456
+ | `tableName` | Custom table name | 'portproxy' |
457
+ | `maxRetries` | Maximum number of retries for failed commands | 3 |
458
+ | `retryDelayMs` | Delay between retries in milliseconds | 1000 |
459
+ | `useAdvancedNAT` | Use connection tracking for stateful NAT | false |
460
+ | `qos` | Quality of Service options (object) | - |
432
461
  | `netProxyIntegration` | NetworkProxy integration options (object) | - |
433
462
 
434
- #### IPTablesProxy NetworkProxy Integration Options
463
+ #### NfTablesProxy QoS Options
464
+
465
+ | Option | Description | Default |
466
+ |----------------------|---------------------------------------------------|---------|
467
+ | `enabled` | Enable Quality of Service features | false |
468
+ | `maxRate` | Maximum bandwidth rate (e.g. "10mbps") | - |
469
+ | `priority` | Traffic priority (1-10, 1 is highest) | - |
470
+ | `markConnections` | Mark connections for easier management | false |
471
+
472
+ #### NfTablesProxy NetworkProxy Integration Options
435
473
 
436
474
  | Option | Description | Default |
437
475
  |----------------------|---------------------------------------------------|---------|
@@ -490,18 +528,30 @@ The `PortProxy` class can inspect the SNI (Server Name Indication) field in TLS
490
528
  - Domain-specific allowed IP ranges
491
529
  - Protection against SNI renegotiation attacks
492
530
 
493
- ### Enhanced IPTables Management
531
+ ### Enhanced NfTables Management
494
532
 
495
- The improved `IPTablesProxy` class offers advanced capabilities:
533
+ The `NfTablesProxy` class offers advanced capabilities compared to the previous IPTablesProxy:
496
534
 
497
535
  - Support for multiple port ranges and individual ports
498
- - IPv6 support with ip6tables
499
- - Source IP filtering with allow/block lists
500
- - Custom chain creation for better rule organization
536
+ - More efficient IP filtering using nftables sets
537
+ - IPv6 support with full feature parity
538
+ - Quality of Service (QoS) features including bandwidth limiting and traffic prioritization
539
+ - Advanced connection tracking for stateful NAT
540
+ - Robust error handling with retry mechanisms
541
+ - Structured logging with JSON support
501
542
  - NetworkProxy integration for SSL termination
502
- - Automatic rule existence checking to prevent duplicates
503
543
  - Comprehensive cleanup on shutdown
504
544
 
545
+ ### Port80Handler with Glob Pattern Support
546
+
547
+ The `Port80Handler` class now includes support for glob pattern domain matching:
548
+
549
+ - Supports wildcard domains like `*.example.com` for HTTP request routing
550
+ - Detects glob patterns and skips certificate issuance for them
551
+ - Smart routing that first attempts exact matches, then tries pattern matching
552
+ - Supports forwarding HTTP requests to backend services
553
+ - Separate forwarding configuration for ACME challenges
554
+
505
555
  ## Troubleshooting
506
556
 
507
557
  ### Browser Certificate Errors
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '5.0.0',
6
+ version: '5.1.0',
7
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
  }
package/ts/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from './classes.nftablesproxy.js';
2
- export * from './classes.networkproxy.js';
3
- export * from './classes.port80handler.js';
1
+ export * from './nfttablesproxy/classes.nftablesproxy.js';
2
+ export * from './networkproxy/classes.np.networkproxy.js';
3
+ export * from './port80handler/classes.port80handler.js';
4
4
  export * from './classes.sslredirect.js';
5
- export * from './classes.pp.portproxy.js';
6
- export * from './classes.pp.snihandler.js';
7
- export * from './classes.pp.interfaces.js';
5
+ export * from './smartproxy/classes.smartproxy.js';
6
+ export * from './smartproxy/classes.pp.snihandler.js';
7
+ export * from './smartproxy/classes.pp.interfaces.js';
@@ -0,0 +1,398 @@
1
+ import * as plugins from '../plugins.js';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './classes.np.types.js';
6
+ import { Port80Handler, Port80HandlerEvents, type IDomainOptions } from '../port80handler/classes.port80handler.js';
7
+
8
+ /**
9
+ * Manages SSL certificates for NetworkProxy including ACME integration
10
+ */
11
+ export class CertificateManager {
12
+ private defaultCertificates: { key: string; cert: string };
13
+ private certificateCache: Map<string, ICertificateEntry> = new Map();
14
+ private port80Handler: Port80Handler | null = null;
15
+ private externalPort80Handler: boolean = false;
16
+ private certificateStoreDir: string;
17
+ private logger: ILogger;
18
+ private httpsServer: plugins.https.Server | null = null;
19
+
20
+ constructor(private options: INetworkProxyOptions) {
21
+ this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
22
+ this.logger = createLogger(options.logLevel || 'info');
23
+
24
+ // Ensure certificate store directory exists
25
+ try {
26
+ if (!fs.existsSync(this.certificateStoreDir)) {
27
+ fs.mkdirSync(this.certificateStoreDir, { recursive: true });
28
+ this.logger.info(`Created certificate store directory: ${this.certificateStoreDir}`);
29
+ }
30
+ } catch (error) {
31
+ this.logger.warn(`Failed to create certificate store directory: ${error}`);
32
+ }
33
+
34
+ this.loadDefaultCertificates();
35
+ }
36
+
37
+ /**
38
+ * Loads default certificates from the filesystem
39
+ */
40
+ public loadDefaultCertificates(): void {
41
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
+ const certPath = path.join(__dirname, '..', '..', 'assets', 'certs');
43
+
44
+ try {
45
+ this.defaultCertificates = {
46
+ key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
47
+ cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
48
+ };
49
+ this.logger.info('Default certificates loaded successfully');
50
+ } catch (error) {
51
+ this.logger.error('Error loading default certificates', error);
52
+
53
+ // Generate self-signed fallback certificates
54
+ try {
55
+ // This is a placeholder for actual certificate generation code
56
+ // In a real implementation, you would use a library like selfsigned to generate certs
57
+ this.defaultCertificates = {
58
+ key: "FALLBACK_KEY_CONTENT",
59
+ cert: "FALLBACK_CERT_CONTENT"
60
+ };
61
+ this.logger.warn('Using fallback self-signed certificates');
62
+ } catch (fallbackError) {
63
+ this.logger.error('Failed to generate fallback certificates', fallbackError);
64
+ throw new Error('Could not load or generate SSL certificates');
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Set the HTTPS server reference for context updates
71
+ */
72
+ public setHttpsServer(server: plugins.https.Server): void {
73
+ this.httpsServer = server;
74
+ }
75
+
76
+ /**
77
+ * Get default certificates
78
+ */
79
+ public getDefaultCertificates(): { key: string; cert: string } {
80
+ return { ...this.defaultCertificates };
81
+ }
82
+
83
+ /**
84
+ * Sets an external Port80Handler for certificate management
85
+ */
86
+ public setExternalPort80Handler(handler: Port80Handler): void {
87
+ if (this.port80Handler && !this.externalPort80Handler) {
88
+ this.logger.warn('Replacing existing internal Port80Handler with external handler');
89
+
90
+ // Clean up existing handler if needed
91
+ if (this.port80Handler !== handler) {
92
+ // Unregister event handlers to avoid memory leaks
93
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_ISSUED);
94
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_RENEWED);
95
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_FAILED);
96
+ this.port80Handler.removeAllListeners(Port80HandlerEvents.CERTIFICATE_EXPIRING);
97
+ }
98
+ }
99
+
100
+ // Set the external handler
101
+ this.port80Handler = handler;
102
+ this.externalPort80Handler = true;
103
+
104
+ // Register event handlers
105
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
106
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
107
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
108
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
109
+ this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
110
+ });
111
+
112
+ this.logger.info('External Port80Handler connected to CertificateManager');
113
+
114
+ // Register domains with Port80Handler if we have any certificates cached
115
+ if (this.certificateCache.size > 0) {
116
+ const domains = Array.from(this.certificateCache.keys())
117
+ .filter(domain => !domain.includes('*')); // Skip wildcard domains
118
+
119
+ this.registerDomainsWithPort80Handler(domains);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Handle newly issued or renewed certificates from Port80Handler
125
+ */
126
+ private handleCertificateIssued(data: { domain: string; certificate: string; privateKey: string; expiryDate: Date }): void {
127
+ const { domain, certificate, privateKey, expiryDate } = data;
128
+
129
+ this.logger.info(`Certificate ${this.certificateCache.has(domain) ? 'renewed' : 'issued'} for ${domain}, valid until ${expiryDate.toISOString()}`);
130
+
131
+ // Update certificate in HTTPS server
132
+ this.updateCertificateCache(domain, certificate, privateKey, expiryDate);
133
+
134
+ // Save the certificate to the filesystem if not using external handler
135
+ if (!this.externalPort80Handler && this.options.acme?.certificateStore) {
136
+ this.saveCertificateToStore(domain, certificate, privateKey);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Handle certificate issuance failures
142
+ */
143
+ private handleCertificateFailed(data: { domain: string; error: string }): void {
144
+ this.logger.error(`Certificate issuance failed for ${data.domain}: ${data.error}`);
145
+ }
146
+
147
+ /**
148
+ * Saves certificate and private key to the filesystem
149
+ */
150
+ private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
151
+ try {
152
+ const certPath = path.join(this.certificateStoreDir, `${domain}.cert.pem`);
153
+ const keyPath = path.join(this.certificateStoreDir, `${domain}.key.pem`);
154
+
155
+ fs.writeFileSync(certPath, certificate);
156
+ fs.writeFileSync(keyPath, privateKey);
157
+
158
+ // Ensure private key has restricted permissions
159
+ try {
160
+ fs.chmodSync(keyPath, 0o600);
161
+ } catch (error) {
162
+ this.logger.warn(`Failed to set permissions on private key for ${domain}: ${error}`);
163
+ }
164
+
165
+ this.logger.info(`Saved certificate for ${domain} to ${certPath}`);
166
+ } catch (error) {
167
+ this.logger.error(`Failed to save certificate for ${domain}: ${error}`);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Handles SNI (Server Name Indication) for TLS connections
173
+ * Used by the HTTPS server to select the correct certificate for each domain
174
+ */
175
+ public handleSNI(domain: string, cb: (err: Error | null, ctx: plugins.tls.SecureContext) => void): void {
176
+ this.logger.debug(`SNI request for domain: ${domain}`);
177
+
178
+ // Check if we have a certificate for this domain
179
+ const certs = this.certificateCache.get(domain);
180
+
181
+ if (certs) {
182
+ try {
183
+ // Create TLS context with the cached certificate
184
+ const context = plugins.tls.createSecureContext({
185
+ key: certs.key,
186
+ cert: certs.cert
187
+ });
188
+
189
+ this.logger.debug(`Using cached certificate for ${domain}`);
190
+ cb(null, context);
191
+ return;
192
+ } catch (err) {
193
+ this.logger.error(`Error creating secure context for ${domain}:`, err);
194
+ }
195
+ }
196
+
197
+ // Check if we should trigger certificate issuance
198
+ if (this.options.acme?.enabled && this.port80Handler && !domain.includes('*')) {
199
+ // Check if this domain is already registered
200
+ const certData = this.port80Handler.getCertificate(domain);
201
+
202
+ if (!certData) {
203
+ this.logger.info(`No certificate found for ${domain}, registering for issuance`);
204
+
205
+ // Register with new domain options format
206
+ const domainOptions: IDomainOptions = {
207
+ domainName: domain,
208
+ sslRedirect: true,
209
+ acmeMaintenance: true
210
+ };
211
+
212
+ this.port80Handler.addDomain(domainOptions);
213
+ }
214
+ }
215
+
216
+ // Fall back to default certificate
217
+ try {
218
+ const context = plugins.tls.createSecureContext({
219
+ key: this.defaultCertificates.key,
220
+ cert: this.defaultCertificates.cert
221
+ });
222
+
223
+ this.logger.debug(`Using default certificate for ${domain}`);
224
+ cb(null, context);
225
+ } catch (err) {
226
+ this.logger.error(`Error creating default secure context:`, err);
227
+ cb(new Error('Cannot create secure context'), null);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Updates certificate in cache
233
+ */
234
+ public updateCertificateCache(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
235
+ // Update certificate context in HTTPS server if it's running
236
+ if (this.httpsServer) {
237
+ try {
238
+ this.httpsServer.addContext(domain, {
239
+ key: privateKey,
240
+ cert: certificate
241
+ });
242
+ this.logger.debug(`Updated SSL context for domain: ${domain}`);
243
+ } catch (error) {
244
+ this.logger.error(`Error updating SSL context for domain ${domain}:`, error);
245
+ }
246
+ }
247
+
248
+ // Update certificate in cache
249
+ this.certificateCache.set(domain, {
250
+ key: privateKey,
251
+ cert: certificate,
252
+ expires: expiryDate
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Gets a certificate for a domain
258
+ */
259
+ public getCertificate(domain: string): ICertificateEntry | undefined {
260
+ return this.certificateCache.get(domain);
261
+ }
262
+
263
+ /**
264
+ * Requests a new certificate for a domain
265
+ */
266
+ public async requestCertificate(domain: string): Promise<boolean> {
267
+ if (!this.options.acme?.enabled && !this.externalPort80Handler) {
268
+ this.logger.warn('ACME certificate management is not enabled');
269
+ return false;
270
+ }
271
+
272
+ if (!this.port80Handler) {
273
+ this.logger.error('Port80Handler is not initialized');
274
+ return false;
275
+ }
276
+
277
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
278
+ if (domain.includes('*')) {
279
+ this.logger.error(`Cannot request certificate for wildcard domain: ${domain}`);
280
+ return false;
281
+ }
282
+
283
+ try {
284
+ // Use the new domain options format
285
+ const domainOptions: IDomainOptions = {
286
+ domainName: domain,
287
+ sslRedirect: true,
288
+ acmeMaintenance: true
289
+ };
290
+
291
+ this.port80Handler.addDomain(domainOptions);
292
+ this.logger.info(`Certificate request submitted for domain: ${domain}`);
293
+ return true;
294
+ } catch (error) {
295
+ this.logger.error(`Error requesting certificate for domain ${domain}:`, error);
296
+ return false;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Registers domains with Port80Handler for ACME certificate management
302
+ */
303
+ public registerDomainsWithPort80Handler(domains: string[]): void {
304
+ if (!this.port80Handler) {
305
+ this.logger.warn('Port80Handler is not initialized');
306
+ return;
307
+ }
308
+
309
+ for (const domain of domains) {
310
+ // Skip wildcard domains - can't get certs for these with HTTP-01 validation
311
+ if (domain.includes('*')) {
312
+ this.logger.info(`Skipping wildcard domain for ACME: ${domain}`);
313
+ continue;
314
+ }
315
+
316
+ // Skip domains already with certificates if configured to do so
317
+ if (this.options.acme?.skipConfiguredCerts) {
318
+ const cachedCert = this.certificateCache.get(domain);
319
+ if (cachedCert) {
320
+ this.logger.info(`Skipping domain with existing certificate: ${domain}`);
321
+ continue;
322
+ }
323
+ }
324
+
325
+ // Register the domain for certificate issuance with new domain options format
326
+ const domainOptions: IDomainOptions = {
327
+ domainName: domain,
328
+ sslRedirect: true,
329
+ acmeMaintenance: true
330
+ };
331
+
332
+ this.port80Handler.addDomain(domainOptions);
333
+ this.logger.info(`Registered domain for ACME certificate issuance: ${domain}`);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Initialize internal Port80Handler
339
+ */
340
+ public async initializePort80Handler(): Promise<Port80Handler | null> {
341
+ // Skip if using external handler
342
+ if (this.externalPort80Handler) {
343
+ this.logger.info('Using external Port80Handler, skipping initialization');
344
+ return this.port80Handler;
345
+ }
346
+
347
+ if (!this.options.acme?.enabled) {
348
+ return null;
349
+ }
350
+
351
+ // Create certificate manager
352
+ this.port80Handler = new Port80Handler({
353
+ port: this.options.acme.port,
354
+ contactEmail: this.options.acme.contactEmail,
355
+ useProduction: this.options.acme.useProduction,
356
+ renewThresholdDays: this.options.acme.renewThresholdDays,
357
+ httpsRedirectPort: this.options.port, // Redirect to our HTTPS port
358
+ renewCheckIntervalHours: 24, // Check daily for renewals
359
+ enabled: this.options.acme.enabled,
360
+ autoRenew: this.options.acme.autoRenew,
361
+ certificateStore: this.options.acme.certificateStore,
362
+ skipConfiguredCerts: this.options.acme.skipConfiguredCerts
363
+ });
364
+
365
+ // Register event handlers
366
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, this.handleCertificateIssued.bind(this));
367
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, this.handleCertificateIssued.bind(this));
368
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_FAILED, this.handleCertificateFailed.bind(this));
369
+ this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_EXPIRING, (data) => {
370
+ this.logger.info(`Certificate for ${data.domain} expires in ${data.daysRemaining} days`);
371
+ });
372
+
373
+ // Start the handler
374
+ try {
375
+ await this.port80Handler.start();
376
+ this.logger.info(`Port80Handler started on port ${this.options.acme.port}`);
377
+ return this.port80Handler;
378
+ } catch (error) {
379
+ this.logger.error(`Failed to start Port80Handler: ${error}`);
380
+ this.port80Handler = null;
381
+ return null;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Stop the Port80Handler if it was internally created
387
+ */
388
+ public async stopPort80Handler(): Promise<void> {
389
+ if (this.port80Handler && !this.externalPort80Handler) {
390
+ try {
391
+ await this.port80Handler.stop();
392
+ this.logger.info('Port80Handler stopped');
393
+ } catch (error) {
394
+ this.logger.error('Error stopping Port80Handler', error);
395
+ }
396
+ }
397
+ }
398
+ }