@push.rocks/smartproxy 7.2.0 → 10.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.
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.router.d.ts +9 -10
- package/dist_ts/classes.router.js +3 -5
- package/dist_ts/common/acmeFactory.d.ts +9 -0
- package/dist_ts/common/acmeFactory.js +20 -0
- package/dist_ts/common/eventUtils.d.ts +15 -0
- package/dist_ts/common/eventUtils.js +19 -0
- package/dist_ts/common/types.d.ts +82 -0
- package/dist_ts/common/types.js +17 -0
- package/dist_ts/networkproxy/classes.np.certificatemanager.js +23 -19
- package/dist_ts/networkproxy/classes.np.types.d.ts +5 -10
- package/dist_ts/networkproxy/classes.np.types.js +1 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/port80handler/classes.port80handler.d.ts +8 -91
- package/dist_ts/port80handler/classes.port80handler.js +34 -222
- package/dist_ts/smartproxy/classes.pp.certprovisioner.d.ts +54 -0
- package/dist_ts/smartproxy/classes.pp.certprovisioner.js +166 -0
- package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +2 -33
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +2 -1
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +11 -11
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +1 -11
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +1 -4
- package/dist_ts/smartproxy/classes.smartproxy.js +62 -213
- package/package.json +2 -1
- package/readme.md +253 -452
- package/readme.plan.md +27 -29
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.router.ts +13 -15
- package/ts/common/acmeFactory.ts +23 -0
- package/ts/common/eventUtils.ts +34 -0
- package/ts/common/types.ts +89 -0
- package/ts/networkproxy/classes.np.certificatemanager.ts +23 -19
- package/ts/networkproxy/classes.np.types.ts +6 -10
- package/ts/plugins.ts +13 -2
- package/ts/port80handler/classes.port80handler.ts +44 -310
- package/ts/smartproxy/classes.pp.certprovisioner.ts +188 -0
- package/ts/smartproxy/classes.pp.interfaces.ts +3 -36
- package/ts/smartproxy/classes.pp.networkproxybridge.ts +11 -10
- package/ts/smartproxy/classes.pp.portrangemanager.ts +0 -10
- 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
|
|
4
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
// Renewal scheduling is handled externally by SmartProxy
|
|
89
|
+
// (Removed internal renewal timer)
|
|
150
90
|
private isShuttingDown: boolean = false;
|
|
151
|
-
private options: Required<
|
|
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:
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
638
|
-
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')
|
|
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
|
-
|
|
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<
|
|
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
|
+
}
|