@push.rocks/smartproxy 7.1.2 → 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.networkproxy.d.ts +1 -0
- package/dist_ts/networkproxy/classes.np.networkproxy.js +5 -1
- 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 +6 -3
- package/dist_ts/plugins.js +7 -4
- package/dist_ts/port80handler/classes.port80handler.d.ts +14 -111
- package/dist_ts/port80handler/classes.port80handler.js +94 -373
- 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 +11 -33
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +5 -0
- package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +21 -11
- package/dist_ts/smartproxy/classes.pp.portrangemanager.js +1 -11
- package/dist_ts/smartproxy/classes.smartproxy.d.ts +3 -5
- package/dist_ts/smartproxy/classes.smartproxy.js +94 -180
- package/package.json +12 -10
- package/readme.hints.md +64 -1
- package/readme.md +253 -408
- package/readme.plan.md +29 -0
- 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.networkproxy.ts +4 -0
- package/ts/networkproxy/classes.np.types.ts +6 -10
- package/ts/plugins.ts +17 -4
- package/ts/port80handler/classes.port80handler.ts +108 -509
- package/ts/smartproxy/classes.pp.certprovisioner.ts +188 -0
- package/ts/smartproxy/classes.pp.interfaces.ts +13 -36
- package/ts/smartproxy/classes.pp.networkproxybridge.ts +22 -10
- package/ts/smartproxy/classes.pp.portrangemanager.ts +0 -10
- package/ts/smartproxy/classes.smartproxy.ts +103 -195
|
@@ -1,7 +1,30 @@
|
|
|
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)
|
|
13
|
+
// ACME HTTP-01 challenge handler storing tokens in memory (diskless)
|
|
14
|
+
class DisklessHttp01Handler {
|
|
15
|
+
private storage: Map<string, string>;
|
|
16
|
+
constructor(storage: Map<string, string>) { this.storage = storage; }
|
|
17
|
+
public getSupportedTypes(): string[] { return ['http-01']; }
|
|
18
|
+
public async prepare(ch: any): Promise<void> {
|
|
19
|
+
this.storage.set(ch.token, ch.keyAuthorization);
|
|
20
|
+
}
|
|
21
|
+
public async verify(ch: any): Promise<void> {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
public async cleanup(ch: any): Promise<void> {
|
|
25
|
+
this.storage.delete(ch.token);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
5
28
|
|
|
6
29
|
/**
|
|
7
30
|
* Custom error classes for better error handling
|
|
@@ -31,24 +54,6 @@ export class ServerError extends Port80HandlerError {
|
|
|
31
54
|
}
|
|
32
55
|
}
|
|
33
56
|
|
|
34
|
-
/**
|
|
35
|
-
* Domain forwarding configuration
|
|
36
|
-
*/
|
|
37
|
-
export interface IForwardConfig {
|
|
38
|
-
ip: string;
|
|
39
|
-
port: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Domain configuration options
|
|
44
|
-
*/
|
|
45
|
-
export interface IDomainOptions {
|
|
46
|
-
domainName: string;
|
|
47
|
-
sslRedirect: boolean; // if true redirects the request to port 443
|
|
48
|
-
acmeMaintenance: boolean; // tries to always have a valid cert for this domain
|
|
49
|
-
forward?: IForwardConfig; // forwards all http requests to that target
|
|
50
|
-
acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
|
|
51
|
-
}
|
|
52
57
|
|
|
53
58
|
/**
|
|
54
59
|
* Represents a domain configuration with certificate status information
|
|
@@ -59,8 +64,6 @@ interface IDomainCertificate {
|
|
|
59
64
|
obtainingInProgress: boolean;
|
|
60
65
|
certificate?: string;
|
|
61
66
|
privateKey?: string;
|
|
62
|
-
challengeToken?: string;
|
|
63
|
-
challengeKeyAuthorization?: string;
|
|
64
67
|
expiryDate?: Date;
|
|
65
68
|
lastRenewalAttempt?: Date;
|
|
66
69
|
}
|
|
@@ -68,59 +71,8 @@ interface IDomainCertificate {
|
|
|
68
71
|
/**
|
|
69
72
|
* Configuration options for the Port80Handler
|
|
70
73
|
*/
|
|
71
|
-
|
|
72
|
-
port?: number;
|
|
73
|
-
contactEmail?: string;
|
|
74
|
-
useProduction?: boolean;
|
|
75
|
-
renewThresholdDays?: number;
|
|
76
|
-
httpsRedirectPort?: number;
|
|
77
|
-
renewCheckIntervalHours?: number;
|
|
78
|
-
enabled?: boolean; // Whether ACME is enabled at all
|
|
79
|
-
autoRenew?: boolean; // Whether to automatically renew certificates
|
|
80
|
-
certificateStore?: string; // Directory to store certificates
|
|
81
|
-
skipConfiguredCerts?: boolean; // Skip domains that already have certificates
|
|
82
|
-
}
|
|
74
|
+
// Port80Handler options moved to common types
|
|
83
75
|
|
|
84
|
-
/**
|
|
85
|
-
* Certificate data that can be emitted via events or set from outside
|
|
86
|
-
*/
|
|
87
|
-
export interface ICertificateData {
|
|
88
|
-
domain: string;
|
|
89
|
-
certificate: string;
|
|
90
|
-
privateKey: string;
|
|
91
|
-
expiryDate: Date;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Events emitted by the Port80Handler
|
|
96
|
-
*/
|
|
97
|
-
export enum Port80HandlerEvents {
|
|
98
|
-
CERTIFICATE_ISSUED = 'certificate-issued',
|
|
99
|
-
CERTIFICATE_RENEWED = 'certificate-renewed',
|
|
100
|
-
CERTIFICATE_FAILED = 'certificate-failed',
|
|
101
|
-
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
|
102
|
-
MANAGER_STARTED = 'manager-started',
|
|
103
|
-
MANAGER_STOPPED = 'manager-stopped',
|
|
104
|
-
REQUEST_FORWARDED = 'request-forwarded',
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Certificate failure payload type
|
|
109
|
-
*/
|
|
110
|
-
export interface ICertificateFailure {
|
|
111
|
-
domain: string;
|
|
112
|
-
error: string;
|
|
113
|
-
isRenewal: boolean;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Certificate expiry payload type
|
|
118
|
-
*/
|
|
119
|
-
export interface ICertificateExpiring {
|
|
120
|
-
domain: string;
|
|
121
|
-
expiryDate: Date;
|
|
122
|
-
daysRemaining: number;
|
|
123
|
-
}
|
|
124
76
|
|
|
125
77
|
/**
|
|
126
78
|
* Port80Handler with ACME certificate management and request forwarding capabilities
|
|
@@ -128,18 +80,21 @@ export interface ICertificateExpiring {
|
|
|
128
80
|
*/
|
|
129
81
|
export class Port80Handler extends plugins.EventEmitter {
|
|
130
82
|
private domainCertificates: Map<string, IDomainCertificate>;
|
|
83
|
+
// In-memory storage for ACME HTTP-01 challenge tokens
|
|
84
|
+
private acmeHttp01Storage: Map<string, string> = new Map();
|
|
85
|
+
// SmartAcme instance for certificate management
|
|
86
|
+
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
|
131
87
|
private server: plugins.http.Server | null = null;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
private renewalTimer: NodeJS.Timeout | null = null;
|
|
88
|
+
// Renewal scheduling is handled externally by SmartProxy
|
|
89
|
+
// (Removed internal renewal timer)
|
|
135
90
|
private isShuttingDown: boolean = false;
|
|
136
|
-
private options: Required<
|
|
91
|
+
private options: Required<IAcmeOptions>;
|
|
137
92
|
|
|
138
93
|
/**
|
|
139
94
|
* Creates a new Port80Handler
|
|
140
95
|
* @param options Configuration options
|
|
141
96
|
*/
|
|
142
|
-
constructor(options:
|
|
97
|
+
constructor(options: IAcmeOptions = {}) {
|
|
143
98
|
super();
|
|
144
99
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
|
145
100
|
|
|
@@ -148,13 +103,14 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
148
103
|
port: options.port ?? 80,
|
|
149
104
|
contactEmail: options.contactEmail ?? 'admin@example.com',
|
|
150
105
|
useProduction: options.useProduction ?? false, // Safer default: staging
|
|
151
|
-
renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
|
|
152
106
|
httpsRedirectPort: options.httpsRedirectPort ?? 443,
|
|
153
|
-
renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
|
|
154
107
|
enabled: options.enabled ?? true, // Enable by default
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 ?? []
|
|
158
114
|
};
|
|
159
115
|
}
|
|
160
116
|
|
|
@@ -175,13 +131,20 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
175
131
|
console.log('Port80Handler is disabled, skipping start');
|
|
176
132
|
return;
|
|
177
133
|
}
|
|
134
|
+
// Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
|
|
135
|
+
if (this.options.enabled) {
|
|
136
|
+
this.smartAcme = new plugins.smartacme.SmartAcme({
|
|
137
|
+
accountEmail: this.options.contactEmail,
|
|
138
|
+
certManager: new plugins.smartacme.MemoryCertManager(),
|
|
139
|
+
environment: this.options.useProduction ? 'production' : 'integration',
|
|
140
|
+
challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
|
|
141
|
+
challengePriority: ['http-01'],
|
|
142
|
+
});
|
|
143
|
+
await this.smartAcme.start();
|
|
144
|
+
}
|
|
178
145
|
|
|
179
146
|
return new Promise((resolve, reject) => {
|
|
180
147
|
try {
|
|
181
|
-
// Load certificates from store if enabled
|
|
182
|
-
if (this.options.certificateStore) {
|
|
183
|
-
this.loadCertificatesFromStore();
|
|
184
|
-
}
|
|
185
148
|
|
|
186
149
|
this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
|
|
187
150
|
|
|
@@ -197,7 +160,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
197
160
|
|
|
198
161
|
this.server.listen(this.options.port, () => {
|
|
199
162
|
console.log(`Port80Handler is listening on port ${this.options.port}`);
|
|
200
|
-
this.startRenewalTimer();
|
|
201
163
|
this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
|
|
202
164
|
|
|
203
165
|
// Start certificate process for domains with acmeMaintenance enabled
|
|
@@ -234,11 +196,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
234
196
|
|
|
235
197
|
this.isShuttingDown = true;
|
|
236
198
|
|
|
237
|
-
// Stop the renewal timer
|
|
238
|
-
if (this.renewalTimer) {
|
|
239
|
-
clearInterval(this.renewalTimer);
|
|
240
|
-
this.renewalTimer = null;
|
|
241
|
-
}
|
|
242
199
|
|
|
243
200
|
return new Promise<void>((resolve) => {
|
|
244
201
|
if (this.server) {
|
|
@@ -353,10 +310,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
353
310
|
|
|
354
311
|
console.log(`Certificate set for ${domain}`);
|
|
355
312
|
|
|
356
|
-
//
|
|
357
|
-
if (this.options.certificateStore) {
|
|
358
|
-
this.saveCertificateToStore(domain, certificate, privateKey);
|
|
359
|
-
}
|
|
313
|
+
// (Persistence of certificates moved to CertProvisioner)
|
|
360
314
|
|
|
361
315
|
// Emit certificate event
|
|
362
316
|
this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
|
|
@@ -391,134 +345,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
391
345
|
};
|
|
392
346
|
}
|
|
393
347
|
|
|
394
|
-
/**
|
|
395
|
-
* Saves a certificate to the filesystem store
|
|
396
|
-
* @param domain The domain for the certificate
|
|
397
|
-
* @param certificate The certificate (PEM format)
|
|
398
|
-
* @param privateKey The private key (PEM format)
|
|
399
|
-
* @private
|
|
400
|
-
*/
|
|
401
|
-
private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
|
|
402
|
-
// Skip if certificate store is not enabled
|
|
403
|
-
if (!this.options.certificateStore) return;
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
const storePath = this.options.certificateStore;
|
|
407
|
-
|
|
408
|
-
// Ensure the directory exists
|
|
409
|
-
if (!fs.existsSync(storePath)) {
|
|
410
|
-
fs.mkdirSync(storePath, { recursive: true });
|
|
411
|
-
console.log(`Created certificate store directory: ${storePath}`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const certPath = path.join(storePath, `${domain}.cert.pem`);
|
|
415
|
-
const keyPath = path.join(storePath, `${domain}.key.pem`);
|
|
416
|
-
|
|
417
|
-
// Write certificate and private key files
|
|
418
|
-
fs.writeFileSync(certPath, certificate);
|
|
419
|
-
fs.writeFileSync(keyPath, privateKey);
|
|
420
|
-
|
|
421
|
-
// Set secure permissions for private key
|
|
422
|
-
try {
|
|
423
|
-
fs.chmodSync(keyPath, 0o600);
|
|
424
|
-
} catch (err) {
|
|
425
|
-
console.log(`Warning: Could not set secure permissions on ${keyPath}`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
console.log(`Saved certificate for ${domain} to ${certPath}`);
|
|
429
|
-
} catch (err) {
|
|
430
|
-
console.error(`Error saving certificate for ${domain}:`, err);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
348
|
|
|
434
|
-
/**
|
|
435
|
-
* Loads certificates from the certificate store
|
|
436
|
-
* @private
|
|
437
|
-
*/
|
|
438
|
-
private loadCertificatesFromStore(): void {
|
|
439
|
-
if (!this.options.certificateStore) return;
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
const storePath = this.options.certificateStore;
|
|
443
|
-
|
|
444
|
-
// Ensure the directory exists
|
|
445
|
-
if (!fs.existsSync(storePath)) {
|
|
446
|
-
fs.mkdirSync(storePath, { recursive: true });
|
|
447
|
-
console.log(`Created certificate store directory: ${storePath}`);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Get list of certificate files
|
|
452
|
-
const files = fs.readdirSync(storePath);
|
|
453
|
-
const certFiles = files.filter(file => file.endsWith('.cert.pem'));
|
|
454
|
-
|
|
455
|
-
// Load each certificate
|
|
456
|
-
for (const certFile of certFiles) {
|
|
457
|
-
const domain = certFile.replace('.cert.pem', '');
|
|
458
|
-
const keyFile = `${domain}.key.pem`;
|
|
459
|
-
|
|
460
|
-
// Skip if key file doesn't exist
|
|
461
|
-
if (!files.includes(keyFile)) {
|
|
462
|
-
console.log(`Warning: Found certificate for ${domain} but no key file`);
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Skip if we should skip configured certs
|
|
467
|
-
if (this.options.skipConfiguredCerts) {
|
|
468
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
469
|
-
if (domainInfo && domainInfo.certObtained) {
|
|
470
|
-
console.log(`Skipping already configured certificate for ${domain}`);
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Load certificate and key
|
|
476
|
-
try {
|
|
477
|
-
const certificate = fs.readFileSync(path.join(storePath, certFile), 'utf8');
|
|
478
|
-
const privateKey = fs.readFileSync(path.join(storePath, keyFile), 'utf8');
|
|
479
|
-
|
|
480
|
-
// Extract expiry date
|
|
481
|
-
let expiryDate: Date | undefined;
|
|
482
|
-
try {
|
|
483
|
-
const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
|
|
484
|
-
if (matches && matches[1]) {
|
|
485
|
-
expiryDate = new Date(matches[1]);
|
|
486
|
-
}
|
|
487
|
-
} catch (err) {
|
|
488
|
-
console.log(`Warning: Could not extract expiry date from certificate for ${domain}`);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Check if domain is already registered
|
|
492
|
-
let domainInfo = this.domainCertificates.get(domain);
|
|
493
|
-
if (!domainInfo) {
|
|
494
|
-
// Register domain if not already registered
|
|
495
|
-
domainInfo = {
|
|
496
|
-
options: {
|
|
497
|
-
domainName: domain,
|
|
498
|
-
sslRedirect: true,
|
|
499
|
-
acmeMaintenance: true
|
|
500
|
-
},
|
|
501
|
-
certObtained: false,
|
|
502
|
-
obtainingInProgress: false
|
|
503
|
-
};
|
|
504
|
-
this.domainCertificates.set(domain, domainInfo);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Set certificate
|
|
508
|
-
domainInfo.certificate = certificate;
|
|
509
|
-
domainInfo.privateKey = privateKey;
|
|
510
|
-
domainInfo.certObtained = true;
|
|
511
|
-
domainInfo.expiryDate = expiryDate;
|
|
512
|
-
|
|
513
|
-
console.log(`Loaded certificate for ${domain} from store, valid until ${expiryDate?.toISOString() || 'unknown'}`);
|
|
514
|
-
} catch (err) {
|
|
515
|
-
console.error(`Error loading certificate for ${domain}:`, err);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
} catch (err) {
|
|
519
|
-
console.error('Error loading certificates from store:', err);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
349
|
|
|
523
350
|
/**
|
|
524
351
|
* Check if a domain is a glob pattern
|
|
@@ -579,38 +406,6 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
579
406
|
}
|
|
580
407
|
}
|
|
581
408
|
|
|
582
|
-
/**
|
|
583
|
-
* Lazy initialization of the ACME client
|
|
584
|
-
* @returns An ACME client instance
|
|
585
|
-
*/
|
|
586
|
-
private async getAcmeClient(): Promise<plugins.acme.Client> {
|
|
587
|
-
if (this.acmeClient) {
|
|
588
|
-
return this.acmeClient;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
try {
|
|
592
|
-
// Generate a new account key
|
|
593
|
-
this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
|
|
594
|
-
|
|
595
|
-
this.acmeClient = new plugins.acme.Client({
|
|
596
|
-
directoryUrl: this.options.useProduction
|
|
597
|
-
? plugins.acme.directory.letsencrypt.production
|
|
598
|
-
: plugins.acme.directory.letsencrypt.staging,
|
|
599
|
-
accountKey: this.accountKey,
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
// Create a new account
|
|
603
|
-
await this.acmeClient.createAccount({
|
|
604
|
-
termsOfServiceAgreed: true,
|
|
605
|
-
contact: [`mailto:${this.options.contactEmail}`],
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
return this.acmeClient;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
|
|
611
|
-
throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
409
|
|
|
615
410
|
/**
|
|
616
411
|
* Handles incoming HTTP requests
|
|
@@ -640,19 +435,32 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
640
435
|
const { domainInfo, pattern } = domainMatch;
|
|
641
436
|
const options = domainInfo.options;
|
|
642
437
|
|
|
643
|
-
//
|
|
644
|
-
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')
|
|
645
|
-
//
|
|
438
|
+
// Handle ACME HTTP-01 challenge requests or forwarding
|
|
439
|
+
if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
|
|
440
|
+
// Forward ACME requests if configured
|
|
646
441
|
if (options.acmeForward) {
|
|
647
442
|
this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
|
|
648
443
|
return;
|
|
649
444
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
445
|
+
// If not managing ACME for this domain, return 404
|
|
446
|
+
if (!options.acmeMaintenance) {
|
|
447
|
+
res.statusCode = 404;
|
|
448
|
+
res.end('Not found');
|
|
654
449
|
return;
|
|
655
450
|
}
|
|
451
|
+
// Serve challenge response from in-memory storage
|
|
452
|
+
const token = req.url.split('/').pop() || '';
|
|
453
|
+
const keyAuth = this.acmeHttp01Storage.get(token);
|
|
454
|
+
if (keyAuth) {
|
|
455
|
+
res.statusCode = 200;
|
|
456
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
457
|
+
res.end(keyAuth);
|
|
458
|
+
console.log(`Served ACME challenge response for ${domain}`);
|
|
459
|
+
} else {
|
|
460
|
+
res.statusCode = 404;
|
|
461
|
+
res.end('Challenge token not found');
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
656
464
|
}
|
|
657
465
|
|
|
658
466
|
// Check if we should forward non-ACME requests
|
|
@@ -762,292 +570,71 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
762
570
|
}
|
|
763
571
|
}
|
|
764
572
|
|
|
765
|
-
/**
|
|
766
|
-
* Serves the ACME HTTP-01 challenge response
|
|
767
|
-
* @param req The HTTP request
|
|
768
|
-
* @param res The HTTP response
|
|
769
|
-
* @param domain The domain for the challenge
|
|
770
|
-
*/
|
|
771
|
-
private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
|
|
772
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
773
|
-
if (!domainInfo) {
|
|
774
|
-
res.statusCode = 404;
|
|
775
|
-
res.end('Domain not configured');
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// The token is the last part of the URL
|
|
780
|
-
const urlParts = req.url?.split('/');
|
|
781
|
-
const token = urlParts ? urlParts[urlParts.length - 1] : '';
|
|
782
|
-
|
|
783
|
-
if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
|
|
784
|
-
res.statusCode = 200;
|
|
785
|
-
res.setHeader('Content-Type', 'text/plain');
|
|
786
|
-
res.end(domainInfo.challengeKeyAuthorization);
|
|
787
|
-
console.log(`Served ACME challenge response for ${domain}`);
|
|
788
|
-
} else {
|
|
789
|
-
res.statusCode = 404;
|
|
790
|
-
res.end('Challenge token not found');
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
573
|
|
|
794
574
|
/**
|
|
795
575
|
* Obtains a certificate for a domain using ACME HTTP-01 challenge
|
|
796
576
|
* @param domain The domain to obtain a certificate for
|
|
797
577
|
* @param isRenewal Whether this is a renewal attempt
|
|
798
578
|
*/
|
|
579
|
+
/**
|
|
580
|
+
* Obtains a certificate for a domain using SmartAcme HTTP-01 challenges
|
|
581
|
+
* @param domain The domain to obtain a certificate for
|
|
582
|
+
* @param isRenewal Whether this is a renewal attempt
|
|
583
|
+
*/
|
|
799
584
|
private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
|
|
800
|
-
// Don't allow certificate issuance for glob patterns
|
|
801
585
|
if (this.isGlobPattern(domain)) {
|
|
802
586
|
throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
|
|
803
587
|
}
|
|
804
|
-
|
|
805
|
-
// Get the domain info
|
|
806
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
807
|
-
if (!domainInfo) {
|
|
808
|
-
throw new CertificateError('Domain not found', domain, isRenewal);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// Verify that acmeMaintenance is enabled
|
|
588
|
+
const domainInfo = this.domainCertificates.get(domain)!;
|
|
812
589
|
if (!domainInfo.options.acmeMaintenance) {
|
|
813
590
|
console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
|
|
814
591
|
return;
|
|
815
592
|
}
|
|
816
|
-
|
|
817
|
-
// Prevent concurrent certificate issuance
|
|
818
593
|
if (domainInfo.obtainingInProgress) {
|
|
819
594
|
console.log(`Certificate issuance already in progress for ${domain}`);
|
|
820
595
|
return;
|
|
821
596
|
}
|
|
822
|
-
|
|
597
|
+
if (!this.smartAcme) {
|
|
598
|
+
throw new Port80HandlerError('SmartAcme is not initialized');
|
|
599
|
+
}
|
|
823
600
|
domainInfo.obtainingInProgress = true;
|
|
824
601
|
domainInfo.lastRenewalAttempt = new Date();
|
|
825
|
-
|
|
826
602
|
try {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// Get the authorizations for the order
|
|
835
|
-
const authorizations = await client.getAuthorizations(order);
|
|
836
|
-
|
|
837
|
-
// Process each authorization
|
|
838
|
-
await this.processAuthorizations(client, domain, authorizations);
|
|
839
|
-
|
|
840
|
-
// Generate a CSR and private key
|
|
841
|
-
const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
|
|
842
|
-
commonName: domain,
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
const csr = csrBuffer.toString();
|
|
846
|
-
const privateKey = privateKeyBuffer.toString();
|
|
847
|
-
|
|
848
|
-
// Finalize the order with our CSR
|
|
849
|
-
await client.finalizeOrder(order, csr);
|
|
850
|
-
|
|
851
|
-
// Get the certificate with the full chain
|
|
852
|
-
const certificate = await client.getCertificate(order);
|
|
853
|
-
|
|
854
|
-
// Store the certificate and key
|
|
603
|
+
// Request certificate via SmartAcme
|
|
604
|
+
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
|
605
|
+
const certificate = certObj.publicKey;
|
|
606
|
+
const privateKey = certObj.privateKey;
|
|
607
|
+
const expiryDate = new Date(certObj.validUntil);
|
|
855
608
|
domainInfo.certificate = certificate;
|
|
856
609
|
domainInfo.privateKey = privateKey;
|
|
857
610
|
domainInfo.certObtained = true;
|
|
858
|
-
|
|
859
|
-
// Clear challenge data
|
|
860
|
-
delete domainInfo.challengeToken;
|
|
861
|
-
delete domainInfo.challengeKeyAuthorization;
|
|
862
|
-
|
|
863
|
-
// Extract expiry date from certificate
|
|
864
|
-
domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
|
|
611
|
+
domainInfo.expiryDate = expiryDate;
|
|
865
612
|
|
|
866
613
|
console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
this.saveCertificateToStore(domain, certificate, privateKey);
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// Emit the appropriate event
|
|
874
|
-
const eventType = isRenewal
|
|
875
|
-
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
|
614
|
+
// Persistence moved to CertProvisioner
|
|
615
|
+
const eventType = isRenewal
|
|
616
|
+
? Port80HandlerEvents.CERTIFICATE_RENEWED
|
|
876
617
|
: Port80HandlerEvents.CERTIFICATE_ISSUED;
|
|
877
|
-
|
|
878
618
|
this.emitCertificateEvent(eventType, {
|
|
879
619
|
domain,
|
|
880
620
|
certificate,
|
|
881
621
|
privateKey,
|
|
882
|
-
expiryDate:
|
|
622
|
+
expiryDate: expiryDate || this.getDefaultExpiryDate()
|
|
883
623
|
});
|
|
884
|
-
|
|
885
624
|
} catch (error: any) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
error.message.includes('rateLimited') ||
|
|
889
|
-
error.message.includes('too many certificates') ||
|
|
890
|
-
error.message.includes('rate limit')
|
|
891
|
-
)) {
|
|
892
|
-
console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
|
|
893
|
-
} else {
|
|
894
|
-
console.error(`Error during certificate issuance for ${domain}:`, error);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// Emit failure event
|
|
625
|
+
const errorMsg = error?.message || 'Unknown error';
|
|
626
|
+
console.error(`Error during certificate issuance for ${domain}:`, error);
|
|
898
627
|
this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
|
|
899
628
|
domain,
|
|
900
|
-
error:
|
|
629
|
+
error: errorMsg,
|
|
901
630
|
isRenewal
|
|
902
631
|
} as ICertificateFailure);
|
|
903
|
-
|
|
904
|
-
throw new CertificateError(
|
|
905
|
-
error.message || 'Certificate issuance failed',
|
|
906
|
-
domain,
|
|
907
|
-
isRenewal
|
|
908
|
-
);
|
|
632
|
+
throw new CertificateError(errorMsg, domain, isRenewal);
|
|
909
633
|
} finally {
|
|
910
|
-
// Reset flag whether successful or not
|
|
911
634
|
domainInfo.obtainingInProgress = false;
|
|
912
635
|
}
|
|
913
636
|
}
|
|
914
637
|
|
|
915
|
-
/**
|
|
916
|
-
* Process ACME authorizations by verifying and completing challenges
|
|
917
|
-
* @param client ACME client
|
|
918
|
-
* @param domain Domain name
|
|
919
|
-
* @param authorizations Authorizations to process
|
|
920
|
-
*/
|
|
921
|
-
private async processAuthorizations(
|
|
922
|
-
client: plugins.acme.Client,
|
|
923
|
-
domain: string,
|
|
924
|
-
authorizations: plugins.acme.Authorization[]
|
|
925
|
-
): Promise<void> {
|
|
926
|
-
const domainInfo = this.domainCertificates.get(domain);
|
|
927
|
-
if (!domainInfo) {
|
|
928
|
-
throw new CertificateError('Domain not found during authorization', domain);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
for (const authz of authorizations) {
|
|
932
|
-
const challenge = authz.challenges.find(ch => ch.type === 'http-01');
|
|
933
|
-
if (!challenge) {
|
|
934
|
-
throw new CertificateError('HTTP-01 challenge not found', domain);
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
// Get the key authorization for the challenge
|
|
938
|
-
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
939
|
-
|
|
940
|
-
// Store the challenge data
|
|
941
|
-
domainInfo.challengeToken = challenge.token;
|
|
942
|
-
domainInfo.challengeKeyAuthorization = keyAuthorization;
|
|
943
|
-
|
|
944
|
-
// ACME client type definition workaround - use compatible approach
|
|
945
|
-
// First check if challenge verification is needed
|
|
946
|
-
const authzUrl = authz.url;
|
|
947
|
-
|
|
948
|
-
try {
|
|
949
|
-
// Check if authzUrl exists and perform verification
|
|
950
|
-
if (authzUrl) {
|
|
951
|
-
await client.verifyChallenge(authz, challenge);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Complete the challenge
|
|
955
|
-
await client.completeChallenge(challenge);
|
|
956
|
-
|
|
957
|
-
// Wait for validation
|
|
958
|
-
await client.waitForValidStatus(challenge);
|
|
959
|
-
console.log(`HTTP-01 challenge completed for ${domain}`);
|
|
960
|
-
} catch (error) {
|
|
961
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
|
|
962
|
-
console.error(`Challenge error for ${domain}:`, error);
|
|
963
|
-
throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* Starts the certificate renewal timer
|
|
970
|
-
*/
|
|
971
|
-
private startRenewalTimer(): void {
|
|
972
|
-
if (this.renewalTimer) {
|
|
973
|
-
clearInterval(this.renewalTimer);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// Convert hours to milliseconds
|
|
977
|
-
const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
|
|
978
|
-
|
|
979
|
-
this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
|
|
980
|
-
|
|
981
|
-
// Prevent the timer from keeping the process alive
|
|
982
|
-
if (this.renewalTimer.unref) {
|
|
983
|
-
this.renewalTimer.unref();
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/**
|
|
990
|
-
* Checks for certificates that need renewal
|
|
991
|
-
*/
|
|
992
|
-
private checkForRenewals(): void {
|
|
993
|
-
if (this.isShuttingDown) {
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Skip renewal if auto-renewal is disabled
|
|
998
|
-
if (this.options.autoRenew === false) {
|
|
999
|
-
console.log('Auto-renewal is disabled, skipping certificate renewal check');
|
|
1000
|
-
return;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
console.log('Checking for certificates that need renewal...');
|
|
1004
|
-
|
|
1005
|
-
const now = new Date();
|
|
1006
|
-
const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
|
|
1007
|
-
|
|
1008
|
-
for (const [domain, domainInfo] of this.domainCertificates.entries()) {
|
|
1009
|
-
// Skip glob patterns
|
|
1010
|
-
if (this.isGlobPattern(domain)) {
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Skip domains with acmeMaintenance disabled
|
|
1015
|
-
if (!domainInfo.options.acmeMaintenance) {
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
// Skip domains without certificates or already in renewal
|
|
1020
|
-
if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Skip domains without expiry dates
|
|
1025
|
-
if (!domainInfo.expiryDate) {
|
|
1026
|
-
continue;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
|
|
1030
|
-
|
|
1031
|
-
// Check if certificate is near expiry
|
|
1032
|
-
if (timeUntilExpiry <= renewThresholdMs) {
|
|
1033
|
-
console.log(`Certificate for ${domain} expires soon, renewing...`);
|
|
1034
|
-
|
|
1035
|
-
const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
|
|
1036
|
-
|
|
1037
|
-
this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
|
|
1038
|
-
domain,
|
|
1039
|
-
expiryDate: domainInfo.expiryDate,
|
|
1040
|
-
daysRemaining
|
|
1041
|
-
} as ICertificateExpiring);
|
|
1042
|
-
|
|
1043
|
-
// Start renewal process
|
|
1044
|
-
this.obtainCertificate(domain, true).catch(err => {
|
|
1045
|
-
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
|
|
1046
|
-
console.error(`Error renewing certificate for ${domain}:`, errorMessage);
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
638
|
|
|
1052
639
|
/**
|
|
1053
640
|
* Extract expiry date from certificate using a more robust approach
|
|
@@ -1173,7 +760,19 @@ export class Port80Handler extends plugins.EventEmitter {
|
|
|
1173
760
|
* Gets configuration details
|
|
1174
761
|
* @returns Current configuration
|
|
1175
762
|
*/
|
|
1176
|
-
public getConfig(): Required<
|
|
763
|
+
public getConfig(): Required<IAcmeOptions> {
|
|
1177
764
|
return { ...this.options };
|
|
1178
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
|
+
}
|
|
1179
778
|
}
|