@push.rocks/smartproxy 7.2.0 → 10.0.1

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 (41) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.router.d.ts +9 -10
  3. package/dist_ts/classes.router.js +3 -5
  4. package/dist_ts/common/acmeFactory.d.ts +9 -0
  5. package/dist_ts/common/acmeFactory.js +20 -0
  6. package/dist_ts/common/eventUtils.d.ts +15 -0
  7. package/dist_ts/common/eventUtils.js +19 -0
  8. package/dist_ts/common/types.d.ts +82 -0
  9. package/dist_ts/common/types.js +17 -0
  10. package/dist_ts/networkproxy/classes.np.certificatemanager.js +23 -19
  11. package/dist_ts/networkproxy/classes.np.types.d.ts +5 -10
  12. package/dist_ts/networkproxy/classes.np.types.js +1 -1
  13. package/dist_ts/plugins.d.ts +2 -1
  14. package/dist_ts/plugins.js +3 -2
  15. package/dist_ts/port80handler/classes.port80handler.d.ts +8 -91
  16. package/dist_ts/port80handler/classes.port80handler.js +34 -222
  17. package/dist_ts/smartproxy/classes.pp.certprovisioner.d.ts +54 -0
  18. package/dist_ts/smartproxy/classes.pp.certprovisioner.js +166 -0
  19. package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +2 -33
  20. package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +2 -1
  21. package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +11 -11
  22. package/dist_ts/smartproxy/classes.pp.portrangemanager.js +1 -11
  23. package/dist_ts/smartproxy/classes.smartproxy.d.ts +1 -4
  24. package/dist_ts/smartproxy/classes.smartproxy.js +62 -213
  25. package/package.json +2 -1
  26. package/readme.md +254 -453
  27. package/readme.plan.md +27 -29
  28. package/ts/00_commitinfo_data.ts +1 -1
  29. package/ts/classes.router.ts +13 -15
  30. package/ts/common/acmeFactory.ts +23 -0
  31. package/ts/common/eventUtils.ts +34 -0
  32. package/ts/common/types.ts +89 -0
  33. package/ts/networkproxy/classes.np.certificatemanager.ts +23 -19
  34. package/ts/networkproxy/classes.np.types.ts +6 -10
  35. package/ts/plugins.ts +13 -2
  36. package/ts/port80handler/classes.port80handler.ts +44 -310
  37. package/ts/smartproxy/classes.pp.certprovisioner.ts +188 -0
  38. package/ts/smartproxy/classes.pp.interfaces.ts +3 -36
  39. package/ts/smartproxy/classes.pp.networkproxybridge.ts +11 -10
  40. package/ts/smartproxy/classes.pp.portrangemanager.ts +0 -10
  41. package/ts/smartproxy/classes.smartproxy.ts +73 -222
@@ -1,7 +1,15 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { IncomingMessage, ServerResponse } from 'http';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
3
+ import { Port80HandlerEvents } from '../common/types.js';
4
+ import type {
5
+ IForwardConfig,
6
+ IDomainOptions,
7
+ ICertificateData,
8
+ ICertificateFailure,
9
+ ICertificateExpiring,
10
+ IAcmeOptions
11
+ } from '../common/types.js';
12
+ // (fs and path I/O moved to CertProvisioner)
5
13
  // ACME HTTP-01 challenge handler storing tokens in memory (diskless)
6
14
  class DisklessHttp01Handler {
7
15
  private storage: Map<string, string>;
@@ -46,24 +54,6 @@ export class ServerError extends Port80HandlerError {
46
54
  }
47
55
  }
48
56
 
49
- /**
50
- * Domain forwarding configuration
51
- */
52
- export interface IForwardConfig {
53
- ip: string;
54
- port: number;
55
- }
56
-
57
- /**
58
- * Domain configuration options
59
- */
60
- export interface IDomainOptions {
61
- domainName: string;
62
- sslRedirect: boolean; // if true redirects the request to port 443
63
- acmeMaintenance: boolean; // tries to always have a valid cert for this domain
64
- forward?: IForwardConfig; // forwards all http requests to that target
65
- acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
66
- }
67
57
 
68
58
  /**
69
59
  * Represents a domain configuration with certificate status information
@@ -81,59 +71,8 @@ interface IDomainCertificate {
81
71
  /**
82
72
  * Configuration options for the Port80Handler
83
73
  */
84
- interface IPort80HandlerOptions {
85
- port?: number;
86
- contactEmail?: string;
87
- useProduction?: boolean;
88
- renewThresholdDays?: number;
89
- httpsRedirectPort?: number;
90
- renewCheckIntervalHours?: number;
91
- enabled?: boolean; // Whether ACME is enabled at all
92
- autoRenew?: boolean; // Whether to automatically renew certificates
93
- certificateStore?: string; // Directory to store certificates
94
- skipConfiguredCerts?: boolean; // Skip domains that already have certificates
95
- }
96
-
97
- /**
98
- * Certificate data that can be emitted via events or set from outside
99
- */
100
- export interface ICertificateData {
101
- domain: string;
102
- certificate: string;
103
- privateKey: string;
104
- expiryDate: Date;
105
- }
74
+ // Port80Handler options moved to common types
106
75
 
107
- /**
108
- * Events emitted by the Port80Handler
109
- */
110
- export enum Port80HandlerEvents {
111
- CERTIFICATE_ISSUED = 'certificate-issued',
112
- CERTIFICATE_RENEWED = 'certificate-renewed',
113
- CERTIFICATE_FAILED = 'certificate-failed',
114
- CERTIFICATE_EXPIRING = 'certificate-expiring',
115
- MANAGER_STARTED = 'manager-started',
116
- MANAGER_STOPPED = 'manager-stopped',
117
- REQUEST_FORWARDED = 'request-forwarded',
118
- }
119
-
120
- /**
121
- * Certificate failure payload type
122
- */
123
- export interface ICertificateFailure {
124
- domain: string;
125
- error: string;
126
- isRenewal: boolean;
127
- }
128
-
129
- /**
130
- * Certificate expiry payload type
131
- */
132
- export interface ICertificateExpiring {
133
- domain: string;
134
- expiryDate: Date;
135
- daysRemaining: number;
136
- }
137
76
 
138
77
  /**
139
78
  * Port80Handler with ACME certificate management and request forwarding capabilities
@@ -146,15 +85,16 @@ export class Port80Handler extends plugins.EventEmitter {
146
85
  // SmartAcme instance for certificate management
147
86
  private smartAcme: plugins.smartacme.SmartAcme | null = null;
148
87
  private server: plugins.http.Server | null = null;
149
- private renewalTimer: NodeJS.Timeout | null = null;
88
+ // Renewal scheduling is handled externally by SmartProxy
89
+ // (Removed internal renewal timer)
150
90
  private isShuttingDown: boolean = false;
151
- private options: Required<IPort80HandlerOptions>;
91
+ private options: Required<IAcmeOptions>;
152
92
 
153
93
  /**
154
94
  * Creates a new Port80Handler
155
95
  * @param options Configuration options
156
96
  */
157
- constructor(options: IPort80HandlerOptions = {}) {
97
+ constructor(options: IAcmeOptions = {}) {
158
98
  super();
159
99
  this.domainCertificates = new Map<string, IDomainCertificate>();
160
100
 
@@ -163,13 +103,14 @@ export class Port80Handler extends plugins.EventEmitter {
163
103
  port: options.port ?? 80,
164
104
  contactEmail: options.contactEmail ?? 'admin@example.com',
165
105
  useProduction: options.useProduction ?? false, // Safer default: staging
166
- renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
167
106
  httpsRedirectPort: options.httpsRedirectPort ?? 443,
168
- renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
169
107
  enabled: options.enabled ?? true, // Enable by default
170
- autoRenew: options.autoRenew ?? true, // Auto-renew by default
171
- certificateStore: options.certificateStore ?? './certs', // Default store location
172
- skipConfiguredCerts: options.skipConfiguredCerts ?? false
108
+ certificateStore: options.certificateStore ?? './certs',
109
+ skipConfiguredCerts: options.skipConfiguredCerts ?? false,
110
+ renewThresholdDays: options.renewThresholdDays ?? 30,
111
+ renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
112
+ autoRenew: options.autoRenew ?? true,
113
+ domainForwards: options.domainForwards ?? []
173
114
  };
174
115
  }
175
116
 
@@ -204,10 +145,6 @@ export class Port80Handler extends plugins.EventEmitter {
204
145
 
205
146
  return new Promise((resolve, reject) => {
206
147
  try {
207
- // Load certificates from store if enabled
208
- if (this.options.certificateStore) {
209
- this.loadCertificatesFromStore();
210
- }
211
148
 
212
149
  this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
213
150
 
@@ -223,7 +160,6 @@ export class Port80Handler extends plugins.EventEmitter {
223
160
 
224
161
  this.server.listen(this.options.port, () => {
225
162
  console.log(`Port80Handler is listening on port ${this.options.port}`);
226
- this.startRenewalTimer();
227
163
  this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
228
164
 
229
165
  // Start certificate process for domains with acmeMaintenance enabled
@@ -260,11 +196,6 @@ export class Port80Handler extends plugins.EventEmitter {
260
196
 
261
197
  this.isShuttingDown = true;
262
198
 
263
- // Stop the renewal timer
264
- if (this.renewalTimer) {
265
- clearInterval(this.renewalTimer);
266
- this.renewalTimer = null;
267
- }
268
199
 
269
200
  return new Promise<void>((resolve) => {
270
201
  if (this.server) {
@@ -379,10 +310,7 @@ export class Port80Handler extends plugins.EventEmitter {
379
310
 
380
311
  console.log(`Certificate set for ${domain}`);
381
312
 
382
- // Save certificate to store if enabled
383
- if (this.options.certificateStore) {
384
- this.saveCertificateToStore(domain, certificate, privateKey);
385
- }
313
+ // (Persistence of certificates moved to CertProvisioner)
386
314
 
387
315
  // Emit certificate event
388
316
  this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
@@ -417,134 +345,7 @@ export class Port80Handler extends plugins.EventEmitter {
417
345
  };
418
346
  }
419
347
 
420
- /**
421
- * Saves a certificate to the filesystem store
422
- * @param domain The domain for the certificate
423
- * @param certificate The certificate (PEM format)
424
- * @param privateKey The private key (PEM format)
425
- * @private
426
- */
427
- private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
428
- // Skip if certificate store is not enabled
429
- if (!this.options.certificateStore) return;
430
-
431
- try {
432
- const storePath = this.options.certificateStore;
433
-
434
- // Ensure the directory exists
435
- if (!fs.existsSync(storePath)) {
436
- fs.mkdirSync(storePath, { recursive: true });
437
- console.log(`Created certificate store directory: ${storePath}`);
438
- }
439
-
440
- const certPath = path.join(storePath, `${domain}.cert.pem`);
441
- const keyPath = path.join(storePath, `${domain}.key.pem`);
442
-
443
- // Write certificate and private key files
444
- fs.writeFileSync(certPath, certificate);
445
- fs.writeFileSync(keyPath, privateKey);
446
-
447
- // Set secure permissions for private key
448
- try {
449
- fs.chmodSync(keyPath, 0o600);
450
- } catch (err) {
451
- console.log(`Warning: Could not set secure permissions on ${keyPath}`);
452
- }
453
-
454
- console.log(`Saved certificate for ${domain} to ${certPath}`);
455
- } catch (err) {
456
- console.error(`Error saving certificate for ${domain}:`, err);
457
- }
458
- }
459
348
 
460
- /**
461
- * Loads certificates from the certificate store
462
- * @private
463
- */
464
- private loadCertificatesFromStore(): void {
465
- if (!this.options.certificateStore) return;
466
-
467
- try {
468
- const storePath = this.options.certificateStore;
469
-
470
- // Ensure the directory exists
471
- if (!fs.existsSync(storePath)) {
472
- fs.mkdirSync(storePath, { recursive: true });
473
- console.log(`Created certificate store directory: ${storePath}`);
474
- return;
475
- }
476
-
477
- // Get list of certificate files
478
- const files = fs.readdirSync(storePath);
479
- const certFiles = files.filter(file => file.endsWith('.cert.pem'));
480
-
481
- // Load each certificate
482
- for (const certFile of certFiles) {
483
- const domain = certFile.replace('.cert.pem', '');
484
- const keyFile = `${domain}.key.pem`;
485
-
486
- // Skip if key file doesn't exist
487
- if (!files.includes(keyFile)) {
488
- console.log(`Warning: Found certificate for ${domain} but no key file`);
489
- continue;
490
- }
491
-
492
- // Skip if we should skip configured certs
493
- if (this.options.skipConfiguredCerts) {
494
- const domainInfo = this.domainCertificates.get(domain);
495
- if (domainInfo && domainInfo.certObtained) {
496
- console.log(`Skipping already configured certificate for ${domain}`);
497
- continue;
498
- }
499
- }
500
-
501
- // Load certificate and key
502
- try {
503
- const certificate = fs.readFileSync(path.join(storePath, certFile), 'utf8');
504
- const privateKey = fs.readFileSync(path.join(storePath, keyFile), 'utf8');
505
-
506
- // Extract expiry date
507
- let expiryDate: Date | undefined;
508
- try {
509
- const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
510
- if (matches && matches[1]) {
511
- expiryDate = new Date(matches[1]);
512
- }
513
- } catch (err) {
514
- console.log(`Warning: Could not extract expiry date from certificate for ${domain}`);
515
- }
516
-
517
- // Check if domain is already registered
518
- let domainInfo = this.domainCertificates.get(domain);
519
- if (!domainInfo) {
520
- // Register domain if not already registered
521
- domainInfo = {
522
- options: {
523
- domainName: domain,
524
- sslRedirect: true,
525
- acmeMaintenance: true
526
- },
527
- certObtained: false,
528
- obtainingInProgress: false
529
- };
530
- this.domainCertificates.set(domain, domainInfo);
531
- }
532
-
533
- // Set certificate
534
- domainInfo.certificate = certificate;
535
- domainInfo.privateKey = privateKey;
536
- domainInfo.certObtained = true;
537
- domainInfo.expiryDate = expiryDate;
538
-
539
- console.log(`Loaded certificate for ${domain} from store, valid until ${expiryDate?.toISOString() || 'unknown'}`);
540
- } catch (err) {
541
- console.error(`Error loading certificate for ${domain}:`, err);
542
- }
543
- }
544
- } catch (err) {
545
- console.error('Error loading certificates from store:', err);
546
- }
547
- }
548
349
 
549
350
  /**
550
351
  * Check if a domain is a glob pattern
@@ -634,13 +435,19 @@ export class Port80Handler extends plugins.EventEmitter {
634
435
  const { domainInfo, pattern } = domainMatch;
635
436
  const options = domainInfo.options;
636
437
 
637
- // Serve or forward ACME HTTP-01 challenge requests
638
- if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && options.acmeMaintenance) {
438
+ // Handle ACME HTTP-01 challenge requests or forwarding
439
+ if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
639
440
  // Forward ACME requests if configured
640
441
  if (options.acmeForward) {
641
442
  this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
642
443
  return;
643
444
  }
445
+ // If not managing ACME for this domain, return 404
446
+ if (!options.acmeMaintenance) {
447
+ res.statusCode = 404;
448
+ res.end('Not found');
449
+ return;
450
+ }
644
451
  // Serve challenge response from in-memory storage
645
452
  const token = req.url.split('/').pop() || '';
646
453
  const keyAuth = this.acmeHttp01Storage.get(token);
@@ -804,9 +611,7 @@ export class Port80Handler extends plugins.EventEmitter {
804
611
  domainInfo.expiryDate = expiryDate;
805
612
 
806
613
  console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
807
- if (this.options.certificateStore) {
808
- this.saveCertificateToStore(domain, certificate, privateKey);
809
- }
614
+ // Persistence moved to CertProvisioner
810
615
  const eventType = isRenewal
811
616
  ? Port80HandlerEvents.CERTIFICATE_RENEWED
812
617
  : Port80HandlerEvents.CERTIFICATE_ISSUED;
@@ -830,89 +635,6 @@ export class Port80Handler extends plugins.EventEmitter {
830
635
  }
831
636
  }
832
637
 
833
- /**
834
- * Starts the certificate renewal timer
835
- */
836
- private startRenewalTimer(): void {
837
- if (this.renewalTimer) {
838
- clearInterval(this.renewalTimer);
839
- }
840
-
841
- // Convert hours to milliseconds
842
- const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
843
-
844
- this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
845
-
846
- // Prevent the timer from keeping the process alive
847
- if (this.renewalTimer.unref) {
848
- this.renewalTimer.unref();
849
- }
850
-
851
- console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
852
- }
853
-
854
- /**
855
- * Checks for certificates that need renewal
856
- */
857
- private checkForRenewals(): void {
858
- if (this.isShuttingDown) {
859
- return;
860
- }
861
-
862
- // Skip renewal if auto-renewal is disabled
863
- if (this.options.autoRenew === false) {
864
- console.log('Auto-renewal is disabled, skipping certificate renewal check');
865
- return;
866
- }
867
-
868
- console.log('Checking for certificates that need renewal...');
869
-
870
- const now = new Date();
871
- const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
872
-
873
- for (const [domain, domainInfo] of this.domainCertificates.entries()) {
874
- // Skip glob patterns
875
- if (this.isGlobPattern(domain)) {
876
- continue;
877
- }
878
-
879
- // Skip domains with acmeMaintenance disabled
880
- if (!domainInfo.options.acmeMaintenance) {
881
- continue;
882
- }
883
-
884
- // Skip domains without certificates or already in renewal
885
- if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
886
- continue;
887
- }
888
-
889
- // Skip domains without expiry dates
890
- if (!domainInfo.expiryDate) {
891
- continue;
892
- }
893
-
894
- const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
895
-
896
- // Check if certificate is near expiry
897
- if (timeUntilExpiry <= renewThresholdMs) {
898
- console.log(`Certificate for ${domain} expires soon, renewing...`);
899
-
900
- const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
901
-
902
- this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
903
- domain,
904
- expiryDate: domainInfo.expiryDate,
905
- daysRemaining
906
- } as ICertificateExpiring);
907
-
908
- // Start renewal process
909
- this.obtainCertificate(domain, true).catch(err => {
910
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
911
- console.error(`Error renewing certificate for ${domain}:`, errorMessage);
912
- });
913
- }
914
- }
915
- }
916
638
 
917
639
  /**
918
640
  * Extract expiry date from certificate using a more robust approach
@@ -1038,7 +760,19 @@ export class Port80Handler extends plugins.EventEmitter {
1038
760
  * Gets configuration details
1039
761
  * @returns Current configuration
1040
762
  */
1041
- public getConfig(): Required<IPort80HandlerOptions> {
763
+ public getConfig(): Required<IAcmeOptions> {
1042
764
  return { ...this.options };
1043
765
  }
766
+
767
+ /**
768
+ * Request a certificate renewal for a specific domain.
769
+ * @param domain The domain to renew.
770
+ */
771
+ public async renewCertificate(domain: string): Promise<void> {
772
+ if (!this.domainCertificates.has(domain)) {
773
+ throw new Port80HandlerError(`Domain not managed: ${domain}`);
774
+ }
775
+ // Trigger renewal via ACME
776
+ await this.obtainCertificate(domain, true);
777
+ }
1044
778
  }
@@ -0,0 +1,188 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type { IDomainConfig, ISmartProxyCertProvisionObject } from './classes.pp.interfaces.js';
3
+ import { Port80Handler } from '../port80handler/classes.port80handler.js';
4
+ import { Port80HandlerEvents } from '../common/types.js';
5
+ import { subscribeToPort80Handler } from '../common/eventUtils.js';
6
+ import type { ICertificateData } from '../common/types.js';
7
+ import type { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
8
+
9
+ /**
10
+ * CertProvisioner manages certificate provisioning and renewal workflows,
11
+ * unifying static certificates and HTTP-01 challenges via Port80Handler.
12
+ */
13
+ export class CertProvisioner extends plugins.EventEmitter {
14
+ private domainConfigs: IDomainConfig[];
15
+ private port80Handler: Port80Handler;
16
+ private networkProxyBridge: NetworkProxyBridge;
17
+ private certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>;
18
+ private forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }>;
19
+ private renewThresholdDays: number;
20
+ private renewCheckIntervalHours: number;
21
+ private autoRenew: boolean;
22
+ private renewManager?: plugins.taskbuffer.TaskManager;
23
+ // Track provisioning type per domain: 'http01' or 'static'
24
+ private provisionMap: Map<string, 'http01' | 'static'>;
25
+
26
+ /**
27
+ * @param domainConfigs Array of domain configuration objects
28
+ * @param port80Handler HTTP-01 challenge handler instance
29
+ * @param networkProxyBridge Bridge for applying external certificates
30
+ * @param certProvider Optional callback returning a static cert or 'http01'
31
+ * @param renewThresholdDays Days before expiry to trigger renewals
32
+ * @param renewCheckIntervalHours Interval in hours to check for renewals
33
+ * @param autoRenew Whether to automatically schedule renewals
34
+ */
35
+ constructor(
36
+ domainConfigs: IDomainConfig[],
37
+ port80Handler: Port80Handler,
38
+ networkProxyBridge: NetworkProxyBridge,
39
+ certProvider?: (domain: string) => Promise<ISmartProxyCertProvisionObject>,
40
+ renewThresholdDays: number = 30,
41
+ renewCheckIntervalHours: number = 24,
42
+ autoRenew: boolean = true,
43
+ forwardConfigs: Array<{ domain: string; forwardConfig?: { ip: string; port: number }; acmeForwardConfig?: { ip: string; port: number }; sslRedirect: boolean }> = []
44
+ ) {
45
+ super();
46
+ this.domainConfigs = domainConfigs;
47
+ this.port80Handler = port80Handler;
48
+ this.networkProxyBridge = networkProxyBridge;
49
+ this.certProvider = certProvider;
50
+ this.renewThresholdDays = renewThresholdDays;
51
+ this.renewCheckIntervalHours = renewCheckIntervalHours;
52
+ this.autoRenew = autoRenew;
53
+ this.provisionMap = new Map();
54
+ this.forwardConfigs = forwardConfigs;
55
+ }
56
+
57
+ /**
58
+ * Start initial provisioning and schedule renewals.
59
+ */
60
+ public async start(): Promise<void> {
61
+ // Subscribe to Port80Handler certificate events
62
+ subscribeToPort80Handler(this.port80Handler, {
63
+ onCertificateIssued: (data: ICertificateData) => {
64
+ this.emit('certificate', { ...data, source: 'http01', isRenewal: false });
65
+ },
66
+ onCertificateRenewed: (data: ICertificateData) => {
67
+ this.emit('certificate', { ...data, source: 'http01', isRenewal: true });
68
+ }
69
+ });
70
+
71
+ // Apply external forwarding for ACME challenges (e.g. Synology)
72
+ for (const f of this.forwardConfigs) {
73
+ this.port80Handler.addDomain({
74
+ domainName: f.domain,
75
+ sslRedirect: f.sslRedirect,
76
+ acmeMaintenance: false,
77
+ forward: f.forwardConfig,
78
+ acmeForward: f.acmeForwardConfig
79
+ });
80
+ }
81
+ // Initial provisioning for all domains
82
+ const domains = this.domainConfigs.flatMap(cfg => cfg.domains);
83
+ for (const domain of domains) {
84
+ // Skip wildcard domains
85
+ if (domain.includes('*')) continue;
86
+ let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
87
+ if (this.certProvider) {
88
+ try {
89
+ provision = await this.certProvider(domain);
90
+ } catch (err) {
91
+ console.error(`certProvider error for ${domain}:`, err);
92
+ }
93
+ }
94
+ if (provision === 'http01') {
95
+ this.provisionMap.set(domain, 'http01');
96
+ this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true });
97
+ } else {
98
+ this.provisionMap.set(domain, 'static');
99
+ const certObj = provision as plugins.tsclass.network.ICert;
100
+ const certData: ICertificateData = {
101
+ domain: certObj.domainName,
102
+ certificate: certObj.publicKey,
103
+ privateKey: certObj.privateKey,
104
+ expiryDate: new Date(certObj.validUntil)
105
+ };
106
+ this.networkProxyBridge.applyExternalCertificate(certData);
107
+ this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
108
+ }
109
+ }
110
+
111
+ // Schedule renewals if enabled
112
+ if (this.autoRenew) {
113
+ this.renewManager = new plugins.taskbuffer.TaskManager();
114
+ const renewTask = new plugins.taskbuffer.Task({
115
+ name: 'CertificateRenewals',
116
+ taskFunction: async () => {
117
+ for (const [domain, type] of this.provisionMap.entries()) {
118
+ // Skip wildcard domains
119
+ if (domain.includes('*')) continue;
120
+ try {
121
+ if (type === 'http01') {
122
+ await this.port80Handler.renewCertificate(domain);
123
+ } else if (type === 'static' && this.certProvider) {
124
+ const provision2 = await this.certProvider(domain);
125
+ if (provision2 !== 'http01') {
126
+ const certObj = provision2 as plugins.tsclass.network.ICert;
127
+ const certData: ICertificateData = {
128
+ domain: certObj.domainName,
129
+ certificate: certObj.publicKey,
130
+ privateKey: certObj.privateKey,
131
+ expiryDate: new Date(certObj.validUntil)
132
+ };
133
+ this.networkProxyBridge.applyExternalCertificate(certData);
134
+ this.emit('certificate', { ...certData, source: 'static', isRenewal: true });
135
+ }
136
+ }
137
+ } catch (err) {
138
+ console.error(`Renewal error for ${domain}:`, err);
139
+ }
140
+ }
141
+ }
142
+ });
143
+ const hours = this.renewCheckIntervalHours;
144
+ const cronExpr = `0 0 */${hours} * * *`;
145
+ this.renewManager.addAndScheduleTask(renewTask, cronExpr);
146
+ this.renewManager.start();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Stop all scheduled renewal tasks.
152
+ */
153
+ public async stop(): Promise<void> {
154
+ // Stop scheduled renewals
155
+ if (this.renewManager) {
156
+ this.renewManager.stop();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Request a certificate on-demand for the given domain.
162
+ * @param domain Domain name to provision
163
+ */
164
+ public async requestCertificate(domain: string): Promise<void> {
165
+ // Skip wildcard domains
166
+ if (domain.includes('*')) {
167
+ throw new Error(`Cannot request certificate for wildcard domain: ${domain}`);
168
+ }
169
+ // Determine provisioning method
170
+ let provision: ISmartProxyCertProvisionObject | 'http01' = 'http01';
171
+ if (this.certProvider) {
172
+ provision = await this.certProvider(domain);
173
+ }
174
+ if (provision === 'http01') {
175
+ await this.port80Handler.renewCertificate(domain);
176
+ } else {
177
+ const certObj = provision as plugins.tsclass.network.ICert;
178
+ const certData: ICertificateData = {
179
+ domain: certObj.domainName,
180
+ certificate: certObj.publicKey,
181
+ privateKey: certObj.privateKey,
182
+ expiryDate: new Date(certObj.validUntil)
183
+ };
184
+ this.networkProxyBridge.applyExternalCertificate(certData);
185
+ this.emit('certificate', { ...certData, source: 'static', isRenewal: false });
186
+ }
187
+ }
188
+ }