@push.rocks/smartproxy 18.2.0 → 19.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/common/eventUtils.d.ts +1 -2
  3. package/dist_ts/common/eventUtils.js +2 -1
  4. package/dist_ts/core/models/common-types.d.ts +1 -1
  5. package/dist_ts/core/models/common-types.js +1 -1
  6. package/dist_ts/core/utils/event-utils.d.ts +9 -9
  7. package/dist_ts/core/utils/event-utils.js +6 -14
  8. package/dist_ts/http/models/http-types.d.ts +13 -1
  9. package/dist_ts/http/models/http-types.js +1 -1
  10. package/dist_ts/index.d.ts +4 -6
  11. package/dist_ts/index.js +4 -10
  12. package/dist_ts/proxies/index.d.ts +3 -2
  13. package/dist_ts/proxies/index.js +4 -5
  14. package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +31 -49
  15. package/dist_ts/proxies/network-proxy/certificate-manager.js +77 -374
  16. package/dist_ts/proxies/network-proxy/models/types.d.ts +12 -1
  17. package/dist_ts/proxies/network-proxy/models/types.js +1 -1
  18. package/dist_ts/proxies/network-proxy/network-proxy.d.ts +2 -7
  19. package/dist_ts/proxies/network-proxy/network-proxy.js +10 -19
  20. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +6 -0
  21. package/dist_ts/proxies/smart-proxy/certificate-manager.js +24 -5
  22. package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -1
  23. package/dist_ts/proxies/smart-proxy/models/index.js +1 -5
  24. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +30 -1
  25. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +4 -0
  26. package/dist_ts/proxies/smart-proxy/route-manager.js +7 -1
  27. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +4 -0
  28. package/dist_ts/proxies/smart-proxy/smart-proxy.js +112 -26
  29. package/package.json +1 -2
  30. package/readme.hints.md +31 -1
  31. package/readme.md +82 -6
  32. package/readme.plan.md +109 -1417
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/common/eventUtils.ts +2 -2
  35. package/ts/core/models/common-types.ts +1 -1
  36. package/ts/core/utils/event-utils.ts +12 -21
  37. package/ts/http/models/http-types.ts +8 -4
  38. package/ts/index.ts +11 -14
  39. package/ts/proxies/index.ts +7 -4
  40. package/ts/proxies/network-proxy/certificate-manager.ts +92 -417
  41. package/ts/proxies/network-proxy/models/types.ts +14 -2
  42. package/ts/proxies/network-proxy/network-proxy.ts +10 -19
  43. package/ts/proxies/smart-proxy/certificate-manager.ts +31 -4
  44. package/ts/proxies/smart-proxy/models/index.ts +2 -1
  45. package/ts/proxies/smart-proxy/models/interfaces.ts +31 -2
  46. package/ts/proxies/smart-proxy/models/route-types.ts +1 -1
  47. package/ts/proxies/smart-proxy/route-manager.ts +7 -0
  48. package/ts/proxies/smart-proxy/smart-proxy.ts +142 -25
  49. package/ts/certificate/acme/acme-factory.ts +0 -48
  50. package/ts/certificate/acme/challenge-handler.ts +0 -110
  51. package/ts/certificate/acme/index.ts +0 -3
  52. package/ts/certificate/events/certificate-events.ts +0 -36
  53. package/ts/certificate/index.ts +0 -75
  54. package/ts/certificate/models/certificate-types.ts +0 -109
  55. package/ts/certificate/providers/cert-provisioner.ts +0 -519
  56. package/ts/certificate/providers/index.ts +0 -3
  57. package/ts/certificate/storage/file-storage.ts +0 -234
  58. package/ts/certificate/storage/index.ts +0 -3
  59. package/ts/certificate/utils/certificate-helpers.ts +0 -50
  60. package/ts/http/port80/acme-interfaces.ts +0 -169
  61. package/ts/http/port80/challenge-responder.ts +0 -246
  62. package/ts/http/port80/index.ts +0 -13
  63. package/ts/http/port80/port80-handler.ts +0 -728
@@ -1,728 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import { IncomingMessage, ServerResponse } from 'http';
3
- import { CertificateEvents } from '../../certificate/events/certificate-events.js';
4
- import type {
5
- IDomainOptions, // Kept for backward compatibility
6
- ICertificateData,
7
- ICertificateFailure,
8
- ICertificateExpiring,
9
- IAcmeOptions,
10
- IRouteForwardConfig
11
- } from '../../certificate/models/certificate-types.js';
12
- import {
13
- HttpEvents,
14
- HttpStatus,
15
- HttpError,
16
- CertificateError,
17
- ServerError,
18
- } from '../models/http-types.js';
19
- import type { IDomainCertificate } from '../models/http-types.js';
20
- import { ChallengeResponder } from './challenge-responder.js';
21
- import { extractPort80RoutesFromRoutes } from './acme-interfaces.js';
22
- import type { IPort80RouteOptions } from './acme-interfaces.js';
23
- import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
24
-
25
- // Re-export for backward compatibility
26
- export {
27
- HttpError as Port80HandlerError,
28
- CertificateError,
29
- ServerError
30
- }
31
-
32
- // Port80Handler events enum for backward compatibility
33
- export const Port80HandlerEvents = CertificateEvents;
34
-
35
- /**
36
- * Configuration options for the Port80Handler
37
- */
38
- // Port80Handler options moved to common types
39
-
40
-
41
- /**
42
- * Port80Handler with ACME certificate management and request forwarding capabilities
43
- * Now with glob pattern support for domain matching
44
- */
45
- export class Port80Handler extends plugins.EventEmitter {
46
- private domainCertificates: Map<string, IDomainCertificate>;
47
- private challengeResponder: ChallengeResponder | null = null;
48
- private server: plugins.http.Server | null = null;
49
-
50
- // Renewal scheduling is handled externally by SmartProxy
51
- private isShuttingDown: boolean = false;
52
- private options: Required<IAcmeOptions>;
53
-
54
- /**
55
- * Creates a new Port80Handler
56
- * @param options Configuration options
57
- */
58
- constructor(options: IAcmeOptions = {}) {
59
- super();
60
- this.domainCertificates = new Map<string, IDomainCertificate>();
61
-
62
- // Default options
63
- this.options = {
64
- port: options.port ?? 80,
65
- accountEmail: options.accountEmail ?? 'admin@example.com',
66
- useProduction: options.useProduction ?? false, // Safer default: staging
67
- httpsRedirectPort: options.httpsRedirectPort ?? 443,
68
- enabled: options.enabled ?? true, // Enable by default
69
- certificateStore: options.certificateStore ?? './certs',
70
- skipConfiguredCerts: options.skipConfiguredCerts ?? false,
71
- renewThresholdDays: options.renewThresholdDays ?? 30,
72
- renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
73
- autoRenew: options.autoRenew ?? true,
74
- routeForwards: options.routeForwards ?? []
75
- };
76
-
77
- // Initialize challenge responder
78
- if (this.options.enabled) {
79
- this.challengeResponder = new ChallengeResponder(
80
- this.options.useProduction,
81
- this.options.accountEmail,
82
- this.options.certificateStore
83
- );
84
-
85
- // Forward certificate events from the challenge responder
86
- this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
87
- this.emit(CertificateEvents.CERTIFICATE_ISSUED, data);
88
- });
89
-
90
- this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
91
- this.emit(CertificateEvents.CERTIFICATE_RENEWED, data);
92
- });
93
-
94
- this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: ICertificateFailure) => {
95
- this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
96
- });
97
-
98
- this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: ICertificateExpiring) => {
99
- this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry);
100
- });
101
- }
102
- }
103
-
104
- /**
105
- * Starts the HTTP server for ACME challenges
106
- */
107
- public async start(): Promise<void> {
108
- if (this.server) {
109
- throw new ServerError('Server is already running');
110
- }
111
-
112
- if (this.isShuttingDown) {
113
- throw new ServerError('Server is shutting down');
114
- }
115
-
116
- // Skip if disabled
117
- if (this.options.enabled === false) {
118
- console.log('Port80Handler is disabled, skipping start');
119
- return;
120
- }
121
-
122
- // Initialize the challenge responder if enabled
123
- if (this.options.enabled && this.challengeResponder) {
124
- try {
125
- await this.challengeResponder.initialize();
126
- } catch (error) {
127
- throw new ServerError(`Failed to initialize challenge responder: ${
128
- error instanceof Error ? error.message : String(error)
129
- }`);
130
- }
131
- }
132
-
133
- return new Promise((resolve, reject) => {
134
- try {
135
- this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
136
-
137
- this.server.on('error', (error: NodeJS.ErrnoException) => {
138
- if (error.code === 'EACCES') {
139
- reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code));
140
- } else if (error.code === 'EADDRINUSE') {
141
- reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code));
142
- } else {
143
- reject(new ServerError(error.message, error.code));
144
- }
145
- });
146
-
147
- this.server.listen(this.options.port, () => {
148
- console.log(`Port80Handler is listening on port ${this.options.port}`);
149
- this.emit(CertificateEvents.MANAGER_STARTED, this.options.port);
150
-
151
- // Start certificate process for domains with acmeMaintenance enabled
152
- for (const [domain, domainInfo] of this.domainCertificates.entries()) {
153
- // Skip glob patterns for certificate issuance
154
- if (this.isGlobPattern(domain)) {
155
- console.log(`Skipping initial certificate for glob pattern: ${domain}`);
156
- continue;
157
- }
158
-
159
- if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) {
160
- this.obtainCertificate(domain).catch(err => {
161
- console.error(`Error obtaining initial certificate for ${domain}:`, err);
162
- });
163
- }
164
- }
165
-
166
- resolve();
167
- });
168
- } catch (error) {
169
- const message = error instanceof Error ? error.message : 'Unknown error starting server';
170
- reject(new ServerError(message));
171
- }
172
- });
173
- }
174
-
175
- /**
176
- * Stops the HTTP server and cleanup resources
177
- */
178
- public async stop(): Promise<void> {
179
- if (!this.server) {
180
- return;
181
- }
182
-
183
- this.isShuttingDown = true;
184
-
185
- return new Promise<void>((resolve) => {
186
- if (this.server) {
187
- this.server.close(() => {
188
- this.server = null;
189
- this.isShuttingDown = false;
190
- this.emit(CertificateEvents.MANAGER_STOPPED);
191
- resolve();
192
- });
193
- } else {
194
- this.isShuttingDown = false;
195
- resolve();
196
- }
197
- });
198
- }
199
-
200
- /**
201
- * Adds a domain with configuration options
202
- * @param options Domain configuration options
203
- */
204
- public addDomain(options: IDomainOptions | IPort80RouteOptions): void {
205
- // Normalize options format (handle both IDomainOptions and IPort80RouteOptions)
206
- const normalizedOptions: IDomainOptions = this.normalizeOptions(options);
207
-
208
- if (!normalizedOptions.domainName || typeof normalizedOptions.domainName !== 'string') {
209
- throw new HttpError('Invalid domain name');
210
- }
211
-
212
- const domainName = normalizedOptions.domainName;
213
-
214
- if (!this.domainCertificates.has(domainName)) {
215
- this.domainCertificates.set(domainName, {
216
- options: normalizedOptions,
217
- certObtained: false,
218
- obtainingInProgress: false
219
- });
220
-
221
- console.log(`Domain added: ${domainName} with configuration:`, {
222
- sslRedirect: normalizedOptions.sslRedirect,
223
- acmeMaintenance: normalizedOptions.acmeMaintenance,
224
- hasForward: !!normalizedOptions.forward,
225
- hasAcmeForward: !!normalizedOptions.acmeForward,
226
- routeReference: normalizedOptions.routeReference
227
- });
228
-
229
- // If acmeMaintenance is enabled and not a glob pattern, start certificate process immediately
230
- if (normalizedOptions.acmeMaintenance && this.server && !this.isGlobPattern(domainName)) {
231
- this.obtainCertificate(domainName).catch(err => {
232
- console.error(`Error obtaining initial certificate for ${domainName}:`, err);
233
- });
234
- }
235
- } else {
236
- // Update existing domain with new options
237
- const existing = this.domainCertificates.get(domainName)!;
238
- existing.options = normalizedOptions;
239
- console.log(`Domain ${domainName} configuration updated`);
240
- }
241
- }
242
-
243
- /**
244
- * Add domains from route configurations
245
- * @param routes Array of route configurations
246
- */
247
- public addDomainsFromRoutes(routes: IRouteConfig[]): void {
248
- // Extract Port80RouteOptions from routes
249
- const routeOptions = extractPort80RoutesFromRoutes(routes);
250
-
251
- // Add each domain
252
- for (const options of routeOptions) {
253
- this.addDomain(options);
254
- }
255
-
256
- console.log(`Added ${routeOptions.length} domains from routes for certificate management`);
257
- }
258
-
259
- /**
260
- * Normalize options from either IDomainOptions or IPort80RouteOptions
261
- * @param options Options to normalize
262
- * @returns Normalized IDomainOptions
263
- * @private
264
- */
265
- private normalizeOptions(options: IDomainOptions | IPort80RouteOptions): IDomainOptions {
266
- // Handle IPort80RouteOptions format
267
- if ('domain' in options) {
268
- return {
269
- domainName: options.domain,
270
- sslRedirect: options.sslRedirect,
271
- acmeMaintenance: options.acmeMaintenance,
272
- forward: options.forward,
273
- acmeForward: options.acmeForward,
274
- routeReference: options.routeReference
275
- };
276
- }
277
-
278
- // Already in IDomainOptions format
279
- return options;
280
- }
281
-
282
- /**
283
- * Removes a domain from management
284
- * @param domain The domain to remove
285
- */
286
- public removeDomain(domain: string): void {
287
- if (this.domainCertificates.delete(domain)) {
288
- console.log(`Domain removed: ${domain}`);
289
- }
290
- }
291
-
292
- /**
293
- * Gets the certificate for a domain if it exists
294
- * @param domain The domain to get the certificate for
295
- */
296
- public getCertificate(domain: string): ICertificateData | null {
297
- // Can't get certificates for glob patterns
298
- if (this.isGlobPattern(domain)) {
299
- return null;
300
- }
301
-
302
- const domainInfo = this.domainCertificates.get(domain);
303
-
304
- if (!domainInfo || !domainInfo.certObtained || !domainInfo.certificate || !domainInfo.privateKey) {
305
- return null;
306
- }
307
-
308
- return {
309
- domain,
310
- certificate: domainInfo.certificate,
311
- privateKey: domainInfo.privateKey,
312
- expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
313
- };
314
- }
315
-
316
-
317
-
318
- /**
319
- * Check if a domain is a glob pattern
320
- * @param domain Domain to check
321
- * @returns True if the domain is a glob pattern
322
- */
323
- private isGlobPattern(domain: string): boolean {
324
- return domain.includes('*');
325
- }
326
-
327
- /**
328
- * Get domain info for a specific domain, using glob pattern matching if needed
329
- * @param requestDomain The actual domain from the request
330
- * @returns The domain info or null if not found
331
- */
332
- private getDomainInfoForRequest(requestDomain: string): { domainInfo: IDomainCertificate, pattern: string } | null {
333
- // Try direct match first
334
- if (this.domainCertificates.has(requestDomain)) {
335
- return {
336
- domainInfo: this.domainCertificates.get(requestDomain)!,
337
- pattern: requestDomain
338
- };
339
- }
340
-
341
- // Then try glob patterns
342
- for (const [pattern, domainInfo] of this.domainCertificates.entries()) {
343
- if (this.isGlobPattern(pattern) && this.domainMatchesPattern(requestDomain, pattern)) {
344
- return { domainInfo, pattern };
345
- }
346
- }
347
-
348
- return null;
349
- }
350
-
351
- /**
352
- * Check if a domain matches a glob pattern
353
- * @param domain The domain to check
354
- * @param pattern The pattern to match against
355
- * @returns True if the domain matches the pattern
356
- */
357
- private domainMatchesPattern(domain: string, pattern: string): boolean {
358
- // Handle different glob pattern styles
359
- if (pattern.startsWith('*.')) {
360
- // *.example.com matches any subdomain
361
- const suffix = pattern.substring(2);
362
- return domain.endsWith(suffix) && domain.includes('.') && domain !== suffix;
363
- } else if (pattern.endsWith('.*')) {
364
- // example.* matches any TLD
365
- const prefix = pattern.substring(0, pattern.length - 2);
366
- const domainParts = domain.split('.');
367
- return domain.startsWith(prefix + '.') && domainParts.length >= 2;
368
- } else if (pattern === '*') {
369
- // Wildcard matches everything
370
- return true;
371
- } else {
372
- // Exact match (shouldn't reach here as we check exact matches first)
373
- return domain === pattern;
374
- }
375
- }
376
-
377
-
378
- /**
379
- * Handles incoming HTTP requests
380
- * @param req The HTTP request
381
- * @param res The HTTP response
382
- */
383
- private handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
384
- // Emit request received event with basic info
385
- this.emit(HttpEvents.REQUEST_RECEIVED, {
386
- url: req.url,
387
- method: req.method,
388
- headers: req.headers
389
- });
390
-
391
- const hostHeader = req.headers.host;
392
- if (!hostHeader) {
393
- res.statusCode = HttpStatus.BAD_REQUEST;
394
- res.end('Bad Request: Host header is missing');
395
- return;
396
- }
397
-
398
- // Extract domain (ignoring any port in the Host header)
399
- const domain = hostHeader.split(':')[0];
400
-
401
- // Check if this is an ACME challenge request that our ChallengeResponder can handle
402
- if (this.challengeResponder && req.url?.startsWith('/.well-known/acme-challenge/')) {
403
- // Handle ACME HTTP-01 challenge with the challenge responder
404
- const domainMatch = this.getDomainInfoForRequest(domain);
405
-
406
- // If there's a specific ACME forwarding config for this domain, use that instead
407
- if (domainMatch?.domainInfo.options.acmeForward) {
408
- this.forwardRequest(req, res, domainMatch.domainInfo.options.acmeForward, 'ACME challenge');
409
- return;
410
- }
411
-
412
- // If domain exists and has acmeMaintenance enabled, or we don't have the domain yet
413
- // (for auto-provisioning), try to handle the ACME challenge
414
- if (!domainMatch || domainMatch.domainInfo.options.acmeMaintenance) {
415
- // Let the challenge responder try to handle this request
416
- if (this.challengeResponder.handleRequest(req, res)) {
417
- // Challenge was handled
418
- return;
419
- }
420
- }
421
- }
422
-
423
- // Dynamic provisioning: if domain not yet managed, register for ACME and return 503
424
- if (!this.domainCertificates.has(domain)) {
425
- try {
426
- this.addDomain({ domainName: domain, sslRedirect: false, acmeMaintenance: true });
427
- } catch (err) {
428
- console.error(`Error registering domain for on-demand provisioning: ${err}`);
429
- }
430
- res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
431
- res.end('Certificate issuance in progress');
432
- return;
433
- }
434
-
435
- // Get domain config, using glob pattern matching if needed
436
- const domainMatch = this.getDomainInfoForRequest(domain);
437
- if (!domainMatch) {
438
- res.statusCode = HttpStatus.NOT_FOUND;
439
- res.end('Domain not configured');
440
- return;
441
- }
442
-
443
- const { domainInfo, pattern } = domainMatch;
444
- const options = domainInfo.options;
445
-
446
- // Check if we should forward non-ACME requests
447
- if (options.forward) {
448
- this.forwardRequest(req, res, options.forward, 'HTTP');
449
- return;
450
- }
451
-
452
- // If certificate exists and sslRedirect is enabled, redirect to HTTPS
453
- // (Skip for glob patterns as they won't have certificates)
454
- if (!this.isGlobPattern(pattern) && domainInfo.certObtained && options.sslRedirect) {
455
- const httpsPort = this.options.httpsRedirectPort;
456
- const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`;
457
- const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`;
458
-
459
- res.statusCode = HttpStatus.MOVED_PERMANENTLY;
460
- res.setHeader('Location', redirectUrl);
461
- res.end(`Redirecting to ${redirectUrl}`);
462
- return;
463
- }
464
-
465
- // Handle case where certificate maintenance is enabled but not yet obtained
466
- // (Skip for glob patterns as they can't have certificates)
467
- if (!this.isGlobPattern(pattern) && options.acmeMaintenance && !domainInfo.certObtained) {
468
- // Trigger certificate issuance if not already running
469
- if (!domainInfo.obtainingInProgress) {
470
- this.obtainCertificate(domain).catch(err => {
471
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
472
- this.emit(CertificateEvents.CERTIFICATE_FAILED, {
473
- domain,
474
- error: errorMessage,
475
- isRenewal: false
476
- });
477
- console.error(`Error obtaining certificate for ${domain}:`, err);
478
- });
479
- }
480
-
481
- res.statusCode = HttpStatus.SERVICE_UNAVAILABLE;
482
- res.end('Certificate issuance in progress, please try again later.');
483
- return;
484
- }
485
-
486
- // Default response for unhandled request
487
- res.statusCode = HttpStatus.NOT_FOUND;
488
- res.end('No handlers configured for this request');
489
-
490
- // Emit request handled event
491
- this.emit(HttpEvents.REQUEST_HANDLED, {
492
- domain,
493
- url: req.url,
494
- statusCode: res.statusCode
495
- });
496
- }
497
-
498
- /**
499
- * Forwards an HTTP request to the specified target
500
- * @param req The original request
501
- * @param res The response object
502
- * @param target The forwarding target (IP and port)
503
- * @param requestType Type of request for logging
504
- */
505
- private forwardRequest(
506
- req: plugins.http.IncomingMessage,
507
- res: plugins.http.ServerResponse,
508
- target: { ip: string; port: number },
509
- requestType: string
510
- ): void {
511
- const options = {
512
- hostname: target.ip,
513
- port: target.port,
514
- path: req.url,
515
- method: req.method,
516
- headers: { ...req.headers }
517
- };
518
-
519
- const domain = req.headers.host?.split(':')[0] || 'unknown';
520
- console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`);
521
-
522
- const proxyReq = plugins.http.request(options, (proxyRes) => {
523
- // Copy status code
524
- res.statusCode = proxyRes.statusCode || HttpStatus.INTERNAL_SERVER_ERROR;
525
-
526
- // Copy headers
527
- for (const [key, value] of Object.entries(proxyRes.headers)) {
528
- if (value) res.setHeader(key, value);
529
- }
530
-
531
- // Pipe response data
532
- proxyRes.pipe(res);
533
-
534
- this.emit(HttpEvents.REQUEST_FORWARDED, {
535
- domain,
536
- requestType,
537
- target: `${target.ip}:${target.port}`,
538
- statusCode: proxyRes.statusCode
539
- });
540
- });
541
-
542
- proxyReq.on('error', (error) => {
543
- console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error);
544
-
545
- this.emit(HttpEvents.REQUEST_ERROR, {
546
- domain,
547
- error: error.message,
548
- target: `${target.ip}:${target.port}`
549
- });
550
-
551
- if (!res.headersSent) {
552
- res.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
553
- res.end(`Proxy error: ${error.message}`);
554
- } else {
555
- res.end();
556
- }
557
- });
558
-
559
- // Pipe original request to proxy request
560
- if (req.readable) {
561
- req.pipe(proxyReq);
562
- } else {
563
- proxyReq.end();
564
- }
565
- }
566
-
567
-
568
- /**
569
- * Obtains a certificate for a domain using ACME HTTP-01 challenge
570
- * @param domain The domain to obtain a certificate for
571
- * @param isRenewal Whether this is a renewal attempt
572
- */
573
- private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
574
- if (this.isGlobPattern(domain)) {
575
- throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
576
- }
577
-
578
- const domainInfo = this.domainCertificates.get(domain)!;
579
-
580
- if (!domainInfo.options.acmeMaintenance) {
581
- console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
582
- return;
583
- }
584
-
585
- if (domainInfo.obtainingInProgress) {
586
- console.log(`Certificate issuance already in progress for ${domain}`);
587
- return;
588
- }
589
-
590
- if (!this.challengeResponder) {
591
- throw new HttpError('Challenge responder is not initialized');
592
- }
593
-
594
- domainInfo.obtainingInProgress = true;
595
- domainInfo.lastRenewalAttempt = new Date();
596
-
597
- try {
598
- // Request certificate via ChallengeResponder
599
- // The ChallengeResponder handles all ACME client interactions and will emit events
600
- const certData = await this.challengeResponder.requestCertificate(domain, isRenewal);
601
-
602
- // Update domain info with certificate data
603
- domainInfo.certificate = certData.certificate;
604
- domainInfo.privateKey = certData.privateKey;
605
- domainInfo.certObtained = true;
606
- domainInfo.expiryDate = certData.expiryDate;
607
-
608
- console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
609
- } catch (error: any) {
610
- const errorMsg = error instanceof Error ? error.message : String(error);
611
- console.error(`Error during certificate issuance for ${domain}:`, error);
612
- throw new CertificateError(errorMsg, domain, isRenewal);
613
- } finally {
614
- domainInfo.obtainingInProgress = false;
615
- }
616
- }
617
-
618
-
619
- /**
620
- * Extract expiry date from certificate using a more robust approach
621
- * @param certificate Certificate PEM string
622
- * @param domain Domain for logging
623
- * @returns Extracted expiry date or default
624
- */
625
- private extractExpiryDateFromCertificate(certificate: string, domain: string): Date {
626
- try {
627
- // This is still using regex, but in a real implementation you would use
628
- // a library like node-forge or x509 to properly parse the certificate
629
- const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
630
- if (matches && matches[1]) {
631
- const expiryDate = new Date(matches[1]);
632
-
633
- // Validate that we got a valid date
634
- if (!isNaN(expiryDate.getTime())) {
635
- console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`);
636
- return expiryDate;
637
- }
638
- }
639
-
640
- console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`);
641
- return this.getDefaultExpiryDate();
642
- } catch (error) {
643
- console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`);
644
- return this.getDefaultExpiryDate();
645
- }
646
- }
647
-
648
- /**
649
- * Get a default expiry date (90 days from now)
650
- * @returns Default expiry date
651
- */
652
- private getDefaultExpiryDate(): Date {
653
- return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default
654
- }
655
-
656
- /**
657
- * Emits a certificate event with the certificate data
658
- * @param eventType The event type to emit
659
- * @param data The certificate data
660
- */
661
- private emitCertificateEvent(eventType: CertificateEvents, data: ICertificateData): void {
662
- this.emit(eventType, data);
663
- }
664
-
665
- /**
666
- * Gets all domains and their certificate status
667
- * @returns Map of domains to certificate status
668
- */
669
- public getDomainCertificateStatus(): Map<string, {
670
- certObtained: boolean;
671
- expiryDate?: Date;
672
- daysRemaining?: number;
673
- obtainingInProgress: boolean;
674
- lastRenewalAttempt?: Date;
675
- }> {
676
- const result = new Map<string, {
677
- certObtained: boolean;
678
- expiryDate?: Date;
679
- daysRemaining?: number;
680
- obtainingInProgress: boolean;
681
- lastRenewalAttempt?: Date;
682
- }>();
683
-
684
- const now = new Date();
685
-
686
- for (const [domain, domainInfo] of this.domainCertificates.entries()) {
687
- // Skip glob patterns
688
- if (this.isGlobPattern(domain)) continue;
689
-
690
- const status: {
691
- certObtained: boolean;
692
- expiryDate?: Date;
693
- daysRemaining?: number;
694
- obtainingInProgress: boolean;
695
- lastRenewalAttempt?: Date;
696
- } = {
697
- certObtained: domainInfo.certObtained,
698
- expiryDate: domainInfo.expiryDate,
699
- obtainingInProgress: domainInfo.obtainingInProgress,
700
- lastRenewalAttempt: domainInfo.lastRenewalAttempt
701
- };
702
-
703
- // Calculate days remaining if expiry date is available
704
- if (domainInfo.expiryDate) {
705
- const daysRemaining = Math.ceil(
706
- (domainInfo.expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)
707
- );
708
- status.daysRemaining = daysRemaining;
709
- }
710
-
711
- result.set(domain, status);
712
- }
713
-
714
- return result;
715
- }
716
-
717
- /**
718
- * Request a certificate renewal for a specific domain.
719
- * @param domain The domain to renew.
720
- */
721
- public async renewCertificate(domain: string): Promise<void> {
722
- if (!this.domainCertificates.has(domain)) {
723
- throw new HttpError(`Domain not managed: ${domain}`);
724
- }
725
- // Trigger renewal via ACME
726
- await this.obtainCertificate(domain, true);
727
- }
728
- }