@push.rocks/smartproxy 4.2.3 → 4.2.6

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.
@@ -1,9 +1,58 @@
1
1
  import * as plugins from './plugins.js';
2
+ import { IncomingMessage, ServerResponse } from 'http';
2
3
 
3
4
  /**
4
- * Represents a domain certificate with various status information
5
+ * Custom error classes for better error handling
6
+ */
7
+ export class Port80HandlerError extends Error {
8
+ constructor(message: string) {
9
+ super(message);
10
+ this.name = 'Port80HandlerError';
11
+ }
12
+ }
13
+
14
+ export class CertificateError extends Port80HandlerError {
15
+ constructor(
16
+ message: string,
17
+ public readonly domain: string,
18
+ public readonly isRenewal: boolean = false
19
+ ) {
20
+ super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`);
21
+ this.name = 'CertificateError';
22
+ }
23
+ }
24
+
25
+ export class ServerError extends Port80HandlerError {
26
+ constructor(message: string, public readonly code?: string) {
27
+ super(message);
28
+ this.name = 'ServerError';
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Domain forwarding configuration
34
+ */
35
+ export interface IForwardConfig {
36
+ ip: string;
37
+ port: number;
38
+ }
39
+
40
+ /**
41
+ * Domain configuration options
42
+ */
43
+ export interface IDomainOptions {
44
+ domainName: string;
45
+ sslRedirect: boolean; // if true redirects the request to port 443
46
+ acmeMaintenance: boolean; // tries to always have a valid cert for this domain
47
+ forward?: IForwardConfig; // forwards all http requests to that target
48
+ acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
49
+ }
50
+
51
+ /**
52
+ * Represents a domain configuration with certificate status information
5
53
  */
6
54
  interface IDomainCertificate {
55
+ options: IDomainOptions;
7
56
  certObtained: boolean;
8
57
  obtainingInProgress: boolean;
9
58
  certificate?: string;
@@ -15,9 +64,9 @@ interface IDomainCertificate {
15
64
  }
16
65
 
17
66
  /**
18
- * Configuration options for the ACME Certificate Manager
67
+ * Configuration options for the Port80Handler
19
68
  */
20
- interface IAcmeCertManagerOptions {
69
+ interface IPort80HandlerOptions {
21
70
  port?: number;
22
71
  contactEmail?: string;
23
72
  useProduction?: boolean;
@@ -29,7 +78,7 @@ interface IAcmeCertManagerOptions {
29
78
  /**
30
79
  * Certificate data that can be emitted via events or set from outside
31
80
  */
32
- interface ICertificateData {
81
+ export interface ICertificateData {
33
82
  domain: string;
34
83
  certificate: string;
35
84
  privateKey: string;
@@ -37,34 +86,53 @@ interface ICertificateData {
37
86
  }
38
87
 
39
88
  /**
40
- * Events emitted by the ACME Certificate Manager
89
+ * Events emitted by the Port80Handler
41
90
  */
42
- export enum CertManagerEvents {
91
+ export enum Port80HandlerEvents {
43
92
  CERTIFICATE_ISSUED = 'certificate-issued',
44
93
  CERTIFICATE_RENEWED = 'certificate-renewed',
45
94
  CERTIFICATE_FAILED = 'certificate-failed',
46
95
  CERTIFICATE_EXPIRING = 'certificate-expiring',
47
96
  MANAGER_STARTED = 'manager-started',
48
97
  MANAGER_STOPPED = 'manager-stopped',
98
+ REQUEST_FORWARDED = 'request-forwarded',
99
+ }
100
+
101
+ /**
102
+ * Certificate failure payload type
103
+ */
104
+ export interface ICertificateFailure {
105
+ domain: string;
106
+ error: string;
107
+ isRenewal: boolean;
49
108
  }
50
109
 
51
110
  /**
52
- * Improved ACME Certificate Manager with event emission and external certificate management
111
+ * Certificate expiry payload type
53
112
  */
54
- export class AcmeCertManager extends plugins.EventEmitter {
113
+ export interface ICertificateExpiring {
114
+ domain: string;
115
+ expiryDate: Date;
116
+ daysRemaining: number;
117
+ }
118
+
119
+ /**
120
+ * Port80Handler with ACME certificate management and request forwarding capabilities
121
+ */
122
+ export class Port80Handler extends plugins.EventEmitter {
55
123
  private domainCertificates: Map<string, IDomainCertificate>;
56
124
  private server: plugins.http.Server | null = null;
57
125
  private acmeClient: plugins.acme.Client | null = null;
58
126
  private accountKey: string | null = null;
59
127
  private renewalTimer: NodeJS.Timeout | null = null;
60
128
  private isShuttingDown: boolean = false;
61
- private options: Required<IAcmeCertManagerOptions>;
129
+ private options: Required<IPort80HandlerOptions>;
62
130
 
63
131
  /**
64
- * Creates a new ACME Certificate Manager
132
+ * Creates a new Port80Handler
65
133
  * @param options Configuration options
66
134
  */
67
- constructor(options: IAcmeCertManagerOptions = {}) {
135
+ constructor(options: IPort80HandlerOptions = {}) {
68
136
  super();
69
137
  this.domainCertificates = new Map<string, IDomainCertificate>();
70
138
 
@@ -73,7 +141,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
73
141
  port: options.port ?? 80,
74
142
  contactEmail: options.contactEmail ?? 'admin@example.com',
75
143
  useProduction: options.useProduction ?? false, // Safer default: staging
76
- renewThresholdDays: options.renewThresholdDays ?? 30,
144
+ renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
77
145
  httpsRedirectPort: options.httpsRedirectPort ?? 443,
78
146
  renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
79
147
  };
@@ -84,11 +152,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
84
152
  */
85
153
  public async start(): Promise<void> {
86
154
  if (this.server) {
87
- throw new Error('Server is already running');
155
+ throw new ServerError('Server is already running');
88
156
  }
89
157
 
90
158
  if (this.isShuttingDown) {
91
- throw new Error('Server is shutting down');
159
+ throw new ServerError('Server is shutting down');
92
160
  }
93
161
 
94
162
  return new Promise((resolve, reject) => {
@@ -97,22 +165,33 @@ export class AcmeCertManager extends plugins.EventEmitter {
97
165
 
98
166
  this.server.on('error', (error: NodeJS.ErrnoException) => {
99
167
  if (error.code === 'EACCES') {
100
- reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`));
168
+ reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
101
169
  } else if (error.code === 'EADDRINUSE') {
102
- reject(new Error(`Port ${this.options.port} is already in use.`));
170
+ reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code));
103
171
  } else {
104
- reject(error);
172
+ reject(new ServerError(error.message, error.code));
105
173
  }
106
174
  });
107
175
 
108
176
  this.server.listen(this.options.port, () => {
109
- console.log(`AcmeCertManager is listening on port ${this.options.port}`);
177
+ console.log(`Port80Handler is listening on port ${this.options.port}`);
110
178
  this.startRenewalTimer();
111
- this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port);
179
+ this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
180
+
181
+ // Start certificate process for domains with acmeMaintenance enabled
182
+ for (const [domain, domainInfo] of this.domainCertificates.entries()) {
183
+ if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
184
+ this.obtainCertificate(domain).catch(err => {
185
+ console.error(`Error obtaining initial certificate for ${domain}:`, err);
186
+ });
187
+ }
188
+ }
189
+
112
190
  resolve();
113
191
  });
114
192
  } catch (error) {
115
- reject(error);
193
+ const message = error instanceof Error ? error.message : 'Unknown error starting server';
194
+ reject(new ServerError(message));
116
195
  }
117
196
  });
118
197
  }
@@ -138,7 +217,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
138
217
  this.server.close(() => {
139
218
  this.server = null;
140
219
  this.isShuttingDown = false;
141
- this.emit(CertManagerEvents.MANAGER_STOPPED);
220
+ this.emit(Port80HandlerEvents.MANAGER_STOPPED);
142
221
  resolve();
143
222
  });
144
223
  } else {
@@ -149,13 +228,41 @@ export class AcmeCertManager extends plugins.EventEmitter {
149
228
  }
150
229
 
151
230
  /**
152
- * Adds a domain to be managed for certificates
153
- * @param domain The domain to add
231
+ * Adds a domain with configuration options
232
+ * @param options Domain configuration options
154
233
  */
155
- public addDomain(domain: string): void {
156
- if (!this.domainCertificates.has(domain)) {
157
- this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false });
158
- console.log(`Domain added: ${domain}`);
234
+ public addDomain(options: IDomainOptions): void {
235
+ if (!options.domainName || typeof options.domainName !== 'string') {
236
+ throw new Port80HandlerError('Invalid domain name');
237
+ }
238
+
239
+ const domainName = options.domainName;
240
+
241
+ if (!this.domainCertificates.has(domainName)) {
242
+ this.domainCertificates.set(domainName, {
243
+ options,
244
+ certObtained: false,
245
+ obtainingInProgress: false
246
+ });
247
+
248
+ console.log(`Domain added: ${domainName} with configuration:`, {
249
+ sslRedirect: options.sslRedirect,
250
+ acmeMaintenance: options.acmeMaintenance,
251
+ hasForward: !!options.forward,
252
+ hasAcmeForward: !!options.acmeForward
253
+ });
254
+
255
+ // If acmeMaintenance is enabled, start certificate process immediately
256
+ if (options.acmeMaintenance && this.server) {
257
+ this.obtainCertificate(domainName).catch(err => {
258
+ console.error(`Error obtaining initial certificate for ${domainName}:`, err);
259
+ });
260
+ }
261
+ } else {
262
+ // Update existing domain with new options
263
+ const existing = this.domainCertificates.get(domainName)!;
264
+ existing.options = options;
265
+ console.log(`Domain ${domainName} configuration updated`);
159
266
  }
160
267
  }
161
268
 
@@ -177,10 +284,25 @@ export class AcmeCertManager extends plugins.EventEmitter {
177
284
  * @param expiryDate Optional expiry date
178
285
  */
179
286
  public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void {
287
+ if (!domain || !certificate || !privateKey) {
288
+ throw new Port80HandlerError('Domain, certificate and privateKey are required');
289
+ }
290
+
180
291
  let domainInfo = this.domainCertificates.get(domain);
181
292
 
182
293
  if (!domainInfo) {
183
- domainInfo = { certObtained: false, obtainingInProgress: false };
294
+ // Create default domain options if not already configured
295
+ const defaultOptions: IDomainOptions = {
296
+ domainName: domain,
297
+ sslRedirect: true,
298
+ acmeMaintenance: true
299
+ };
300
+
301
+ domainInfo = {
302
+ options: defaultOptions,
303
+ certObtained: false,
304
+ obtainingInProgress: false
305
+ };
184
306
  this.domainCertificates.set(domain, domainInfo);
185
307
  }
186
308
 
@@ -192,27 +314,18 @@ export class AcmeCertManager extends plugins.EventEmitter {
192
314
  if (expiryDate) {
193
315
  domainInfo.expiryDate = expiryDate;
194
316
  } else {
195
- // Try to extract expiry date from certificate
196
- try {
197
- // This is a simplistic approach - in a real implementation, use a proper
198
- // certificate parsing library like node-forge or x509
199
- const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
200
- if (matches && matches[1]) {
201
- domainInfo.expiryDate = new Date(matches[1]);
202
- }
203
- } catch (error) {
204
- console.warn(`Failed to extract expiry date from certificate for ${domain}`);
205
- }
317
+ // Extract expiry date from certificate
318
+ domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
206
319
  }
207
320
 
208
321
  console.log(`Certificate set for ${domain}`);
209
322
 
210
323
  // Emit certificate event
211
- this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, {
324
+ this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
212
325
  domain,
213
326
  certificate,
214
327
  privateKey,
215
- expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
328
+ expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
216
329
  });
217
330
  }
218
331
 
@@ -231,7 +344,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
231
344
  domain,
232
345
  certificate: domainInfo.certificate,
233
346
  privateKey: domainInfo.privateKey,
234
- expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
347
+ expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
235
348
  };
236
349
  }
237
350
 
@@ -244,23 +357,28 @@ export class AcmeCertManager extends plugins.EventEmitter {
244
357
  return this.acmeClient;
245
358
  }
246
359
 
247
- // Generate a new account key
248
- this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
249
-
250
- this.acmeClient = new plugins.acme.Client({
251
- directoryUrl: this.options.useProduction
252
- ? plugins.acme.directory.letsencrypt.production
253
- : plugins.acme.directory.letsencrypt.staging,
254
- accountKey: this.accountKey,
255
- });
256
-
257
- // Create a new account
258
- await this.acmeClient.createAccount({
259
- termsOfServiceAgreed: true,
260
- contact: [`mailto:${this.options.contactEmail}`],
261
- });
262
-
263
- return this.acmeClient;
360
+ try {
361
+ // Generate a new account key
362
+ this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
363
+
364
+ this.acmeClient = new plugins.acme.Client({
365
+ directoryUrl: this.options.useProduction
366
+ ? plugins.acme.directory.letsencrypt.production
367
+ : plugins.acme.directory.letsencrypt.staging,
368
+ accountKey: this.accountKey,
369
+ });
370
+
371
+ // Create a new account
372
+ await this.acmeClient.createAccount({
373
+ termsOfServiceAgreed: true,
374
+ contact: [`mailto:${this.options.contactEmail}`],
375
+ });
376
+
377
+ return this.acmeClient;
378
+ } catch (error) {
379
+ const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
380
+ throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
381
+ }
264
382
  }
265
383
 
266
384
  /**
@@ -279,12 +397,7 @@ export class AcmeCertManager extends plugins.EventEmitter {
279
397
  // Extract domain (ignoring any port in the Host header)
280
398
  const domain = hostHeader.split(':')[0];
281
399
 
282
- // If the request is for an ACME HTTP-01 challenge, handle it
283
- if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
284
- this.handleAcmeChallenge(req, res, domain);
285
- return;
286
- }
287
-
400
+ // Check if domain is configured
288
401
  if (!this.domainCertificates.has(domain)) {
289
402
  res.statusCode = 404;
290
403
  res.end('Domain not configured');
@@ -292,9 +405,28 @@ export class AcmeCertManager extends plugins.EventEmitter {
292
405
  }
293
406
 
294
407
  const domainInfo = this.domainCertificates.get(domain)!;
408
+ const options = domainInfo.options;
295
409
 
296
- // If certificate exists, redirect to HTTPS
297
- if (domainInfo.certObtained) {
410
+ // If the request is for an ACME HTTP-01 challenge, handle it
411
+ if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
412
+ // Check if we should forward ACME requests
413
+ if (options.acmeForward) {
414
+ this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
415
+ return;
416
+ }
417
+
418
+ this.handleAcmeChallenge(req, res, domain);
419
+ return;
420
+ }
421
+
422
+ // Check if we should forward non-ACME requests
423
+ if (options.forward) {
424
+ this.forwardRequest(req, res, options.forward, 'HTTP');
425
+ return;
426
+ }
427
+
428
+ // If certificate exists and sslRedirect is enabled, redirect to HTTPS
429
+ if (domainInfo.certObtained && options.sslRedirect) {
298
430
  const httpsPort = this.options.httpsRedirectPort;
299
431
  const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
300
432
  const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
@@ -302,17 +434,93 @@ export class AcmeCertManager extends plugins.EventEmitter {
302
434
  res.statusCode = 301;
303
435
  res.setHeader('Location', redirectUrl);
304
436
  res.end(`Redirecting to ${redirectUrl}`);
305
- } else {
437
+ return;
438
+ }
439
+
440
+ // Handle case where certificate maintenance is enabled but not yet obtained
441
+ if (options.acmeMaintenance && !domainInfo.certObtained) {
306
442
  // Trigger certificate issuance if not already running
307
443
  if (!domainInfo.obtainingInProgress) {
308
444
  this.obtainCertificate(domain).catch(err => {
309
- this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message });
445
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
446
+ this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
447
+ domain,
448
+ error: errorMessage,
449
+ isRenewal: false
450
+ });
310
451
  console.error(`Error obtaining certificate for ${domain}:`, err);
311
452
  });
312
453
  }
313
454
 
314
455
  res.statusCode = 503;
315
456
  res.end('Certificate issuance in progress, please try again later.');
457
+ return;
458
+ }
459
+
460
+ // Default response for unhandled request
461
+ res.statusCode = 404;
462
+ res.end('No handlers configured for this request');
463
+ }
464
+
465
+ /**
466
+ * Forwards an HTTP request to the specified target
467
+ * @param req The original request
468
+ * @param res The response object
469
+ * @param target The forwarding target (IP and port)
470
+ * @param requestType Type of request for logging
471
+ */
472
+ private forwardRequest(
473
+ req: plugins.http.IncomingMessage,
474
+ res: plugins.http.ServerResponse,
475
+ target: IForwardConfig,
476
+ requestType: string
477
+ ): void {
478
+ const options = {
479
+ hostname: target.ip,
480
+ port: target.port,
481
+ path: req.url,
482
+ method: req.method,
483
+ headers: { ...req.headers }
484
+ };
485
+
486
+ const domain = req.headers.host?.split(':')[0] || 'unknown';
487
+ console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`);
488
+
489
+ const proxyReq = plugins.http.request(options, (proxyRes) => {
490
+ // Copy status code
491
+ res.statusCode = proxyRes.statusCode || 500;
492
+
493
+ // Copy headers
494
+ for (const [key, value] of Object.entries(proxyRes.headers)) {
495
+ if (value) res.setHeader(key, value);
496
+ }
497
+
498
+ // Pipe response data
499
+ proxyRes.pipe(res);
500
+
501
+ this.emit(Port80HandlerEvents.REQUEST_FORWARDED, {
502
+ domain,
503
+ requestType,
504
+ target: `${target.ip}:${target.port}`,
505
+ statusCode: proxyRes.statusCode
506
+ });
507
+ });
508
+
509
+ proxyReq.on('error', (error) => {
510
+ console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error);
511
+ if (!res.headersSent) {
512
+ res.statusCode = 502;
513
+ res.end(`Proxy error: ${error.message}`);
514
+ } else {
515
+ res.end();
516
+ }
517
+ });
518
+
519
+ // Pipe original request to proxy request
520
+ if (req.readable) {
521
+ req.pipe(proxyReq);
522
+ } else {
523
+ proxyReq.end();
316
524
  }
317
525
  }
318
526
 
@@ -354,7 +562,13 @@ export class AcmeCertManager extends plugins.EventEmitter {
354
562
  // Get the domain info
355
563
  const domainInfo = this.domainCertificates.get(domain);
356
564
  if (!domainInfo) {
357
- throw new Error(`Domain not found: ${domain}`);
565
+ throw new CertificateError('Domain not found', domain, isRenewal);
566
+ }
567
+
568
+ // Verify that acmeMaintenance is enabled
569
+ if (!domainInfo.options.acmeMaintenance) {
570
+ console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
571
+ return;
358
572
  }
359
573
 
360
574
  // Prevent concurrent certificate issuance
@@ -377,40 +591,8 @@ export class AcmeCertManager extends plugins.EventEmitter {
377
591
  // Get the authorizations for the order
378
592
  const authorizations = await client.getAuthorizations(order);
379
593
 
380
- for (const authz of authorizations) {
381
- const challenge = authz.challenges.find(ch => ch.type === 'http-01');
382
- if (!challenge) {
383
- throw new Error('HTTP-01 challenge not found');
384
- }
385
-
386
- // Get the key authorization for the challenge
387
- const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
388
-
389
- // Store the challenge data
390
- domainInfo.challengeToken = challenge.token;
391
- domainInfo.challengeKeyAuthorization = keyAuthorization;
392
-
393
- // ACME client type definition workaround - use compatible approach
394
- // First check if challenge verification is needed
395
- const authzUrl = authz.url;
396
-
397
- try {
398
- // Check if authzUrl exists and perform verification
399
- if (authzUrl) {
400
- await client.verifyChallenge(authz, challenge);
401
- }
402
-
403
- // Complete the challenge
404
- await client.completeChallenge(challenge);
405
-
406
- // Wait for validation
407
- await client.waitForValidStatus(challenge);
408
- console.log(`HTTP-01 challenge completed for ${domain}`);
409
- } catch (error) {
410
- console.error(`Challenge error for ${domain}:`, error);
411
- throw error;
412
- }
413
- }
594
+ // Process each authorization
595
+ await this.processAuthorizations(client, domain, authorizations);
414
596
 
415
597
  // Generate a CSR and private key
416
598
  const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
@@ -436,28 +618,20 @@ export class AcmeCertManager extends plugins.EventEmitter {
436
618
  delete domainInfo.challengeKeyAuthorization;
437
619
 
438
620
  // Extract expiry date from certificate
439
- try {
440
- const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
441
- if (matches && matches[1]) {
442
- domainInfo.expiryDate = new Date(matches[1]);
443
- console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`);
444
- }
445
- } catch (error) {
446
- console.warn(`Failed to extract expiry date from certificate for ${domain}`);
447
- }
621
+ domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
448
622
 
449
623
  console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
450
624
 
451
625
  // Emit the appropriate event
452
626
  const eventType = isRenewal
453
- ? CertManagerEvents.CERTIFICATE_RENEWED
454
- : CertManagerEvents.CERTIFICATE_ISSUED;
627
+ ? Port80HandlerEvents.CERTIFICATE_RENEWED
628
+ : Port80HandlerEvents.CERTIFICATE_ISSUED;
455
629
 
456
630
  this.emitCertificateEvent(eventType, {
457
631
  domain,
458
632
  certificate,
459
633
  privateKey,
460
- expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default
634
+ expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
461
635
  });
462
636
 
463
637
  } catch (error: any) {
@@ -473,17 +647,76 @@ export class AcmeCertManager extends plugins.EventEmitter {
473
647
  }
474
648
 
475
649
  // Emit failure event
476
- this.emit(CertManagerEvents.CERTIFICATE_FAILED, {
650
+ this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
477
651
  domain,
478
652
  error: error.message || 'Unknown error',
479
653
  isRenewal
480
- });
654
+ } as ICertificateFailure);
655
+
656
+ throw new CertificateError(
657
+ error.message || 'Certificate issuance failed',
658
+ domain,
659
+ isRenewal
660
+ );
481
661
  } finally {
482
662
  // Reset flag whether successful or not
483
663
  domainInfo.obtainingInProgress = false;
484
664
  }
485
665
  }
486
666
 
667
+ /**
668
+ * Process ACME authorizations by verifying and completing challenges
669
+ * @param client ACME client
670
+ * @param domain Domain name
671
+ * @param authorizations Authorizations to process
672
+ */
673
+ private async processAuthorizations(
674
+ client: plugins.acme.Client,
675
+ domain: string,
676
+ authorizations: plugins.acme.Authorization[]
677
+ ): Promise<void> {
678
+ const domainInfo = this.domainCertificates.get(domain);
679
+ if (!domainInfo) {
680
+ throw new CertificateError('Domain not found during authorization', domain);
681
+ }
682
+
683
+ for (const authz of authorizations) {
684
+ const challenge = authz.challenges.find(ch => ch.type === 'http-01');
685
+ if (!challenge) {
686
+ throw new CertificateError('HTTP-01 challenge not found', domain);
687
+ }
688
+
689
+ // Get the key authorization for the challenge
690
+ const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
691
+
692
+ // Store the challenge data
693
+ domainInfo.challengeToken = challenge.token;
694
+ domainInfo.challengeKeyAuthorization = keyAuthorization;
695
+
696
+ // ACME client type definition workaround - use compatible approach
697
+ // First check if challenge verification is needed
698
+ const authzUrl = authz.url;
699
+
700
+ try {
701
+ // Check if authzUrl exists and perform verification
702
+ if (authzUrl) {
703
+ await client.verifyChallenge(authz, challenge);
704
+ }
705
+
706
+ // Complete the challenge
707
+ await client.completeChallenge(challenge);
708
+
709
+ // Wait for validation
710
+ await client.waitForValidStatus(challenge);
711
+ console.log(`HTTP-01 challenge completed for ${domain}`);
712
+ } catch (error) {
713
+ const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
714
+ console.error(`Challenge error for ${domain}:`, error);
715
+ throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
716
+ }
717
+ }
718
+ }
719
+
487
720
  /**
488
721
  * Starts the certificate renewal timer
489
722
  */
@@ -519,6 +752,11 @@ export class AcmeCertManager extends plugins.EventEmitter {
519
752
  const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
520
753
 
521
754
  for (const [domain, domainInfo] of this.domainCertificates.entries()) {
755
+ // Skip domains with acmeMaintenance disabled
756
+ if (!domainInfo.options.acmeMaintenance) {
757
+ continue;
758
+ }
759
+
522
760
  // Skip domains without certificates or already in renewal
523
761
  if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
524
762
  continue;
@@ -534,26 +772,67 @@ export class AcmeCertManager extends plugins.EventEmitter {
534
772
  // Check if certificate is near expiry
535
773
  if (timeUntilExpiry <= renewThresholdMs) {
536
774
  console.log(`Certificate for ${domain} expires soon, renewing...`);
537
- this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, {
775
+
776
+ const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
777
+
778
+ this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
538
779
  domain,
539
780
  expiryDate: domainInfo.expiryDate,
540
- daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000))
541
- });
781
+ daysRemaining
782
+ } as ICertificateExpiring);
542
783
 
543
784
  // Start renewal process
544
785
  this.obtainCertificate(domain, true).catch(err => {
545
- console.error(`Error renewing certificate for ${domain}:`, err);
786
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
787
+ console.error(`Error renewing certificate for ${domain}:`, errorMessage);
546
788
  });
547
789
  }
548
790
  }
549
791
  }
550
792
 
793
+ /**
794
+ * Extract expiry date from certificate using a more robust approach
795
+ * @param certificate Certificate PEM string
796
+ * @param domain Domain for logging
797
+ * @returns Extracted expiry date or default
798
+ */
799
+ private extractExpiryDateFromCertificate(certificate: string, domain: string): Date {
800
+ try {
801
+ // This is still using regex, but in a real implementation you would use
802
+ // a library like node-forge or x509 to properly parse the certificate
803
+ const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
804
+ if (matches && matches[1]) {
805
+ const expiryDate = new Date(matches[1]);
806
+
807
+ // Validate that we got a valid date
808
+ if (!isNaN(expiryDate.getTime())) {
809
+ console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`);
810
+ return expiryDate;
811
+ }
812
+ }
813
+
814
+ console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`);
815
+ return this.getDefaultExpiryDate();
816
+ } catch (error) {
817
+ console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`);
818
+ return this.getDefaultExpiryDate();
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Get a default expiry date (90 days from now)
824
+ * @returns Default expiry date
825
+ */
826
+ private getDefaultExpiryDate(): Date {
827
+ return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default
828
+ }
829
+
551
830
  /**
552
831
  * Emits a certificate event with the certificate data
553
832
  * @param eventType The event type to emit
554
833
  * @param data The certificate data
555
834
  */
556
- private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void {
835
+ private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void {
557
836
  this.emit(eventType, data);
558
837
  }
559
838
  }