@push.rocks/smartproxy 22.4.2 → 22.6.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.
Files changed (72) hide show
  1. package/changelog.md +28 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/index.d.ts +1 -5
  5. package/dist_ts/index.js +3 -9
  6. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  7. package/dist_ts/proxies/index.d.ts +1 -5
  8. package/dist_ts/proxies/index.js +2 -6
  9. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  10. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  11. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
  12. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  13. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  14. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  15. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  16. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  17. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  18. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  19. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
  22. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  23. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  24. package/dist_ts/routing/index.d.ts +1 -1
  25. package/dist_ts/routing/index.js +3 -3
  26. package/dist_ts/routing/models/http-types.d.ts +119 -4
  27. package/dist_ts/routing/models/http-types.js +93 -5
  28. package/package.json +1 -1
  29. package/readme.md +470 -219
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/index.ts +4 -12
  32. package/ts/protocols/common/fragment-handler.ts +4 -0
  33. package/ts/proxies/index.ts +1 -9
  34. package/ts/proxies/smart-proxy/index.ts +6 -13
  35. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
  36. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  37. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  38. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  39. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  40. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
  41. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  42. package/ts/routing/index.ts +2 -2
  43. package/ts/routing/models/http-types.ts +147 -4
  44. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  45. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  46. package/ts/proxies/http-proxy/default-certificates.ts +0 -150
  47. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  48. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  49. package/ts/proxies/http-proxy/http-proxy.ts +0 -669
  50. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  51. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  52. package/ts/proxies/http-proxy/index.ts +0 -18
  53. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  54. package/ts/proxies/http-proxy/models/index.ts +0 -5
  55. package/ts/proxies/http-proxy/models/types.ts +0 -125
  56. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  57. package/ts/proxies/http-proxy/security-manager.ts +0 -413
  58. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  59. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  60. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  61. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
  62. package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
  63. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
  64. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  65. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  66. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  67. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
  68. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  69. package/ts/proxies/smart-proxy/security-manager.ts +0 -269
  70. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  71. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  72. package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
@@ -1,895 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import { HttpProxy } from '../http-proxy/index.js';
3
- import type { IRouteConfig, IRouteTls } from './models/route-types.js';
4
- import type { IAcmeOptions } from './models/interfaces.js';
5
- import { CertStore } from './cert-store.js';
6
- import type { AcmeStateManager } from './acme-state-manager.js';
7
- import { logger } from '../../core/utils/logger.js';
8
- import { SocketHandlers } from './utils/route-helpers.js';
9
-
10
- export interface ICertStatus {
11
- domain: string;
12
- status: 'valid' | 'pending' | 'expired' | 'error';
13
- expiryDate?: Date;
14
- issueDate?: Date;
15
- source: 'static' | 'acme' | 'custom';
16
- error?: string;
17
- }
18
-
19
- export interface ICertificateData {
20
- cert: string;
21
- key: string;
22
- ca?: string;
23
- expiryDate: Date;
24
- issueDate: Date;
25
- source?: 'static' | 'acme' | 'custom';
26
- }
27
-
28
- export class SmartCertManager {
29
- private certStore: CertStore;
30
- private smartAcme: plugins.smartacme.SmartAcme | null = null;
31
- private httpProxy: HttpProxy | null = null;
32
- private renewalTimer: NodeJS.Timeout | null = null;
33
- private pendingChallenges: Map<string, string> = new Map();
34
- private challengeRoute: IRouteConfig | null = null;
35
-
36
- // Track certificate status by route name
37
- private certStatus: Map<string, ICertStatus> = new Map();
38
-
39
- // Global ACME defaults from top-level configuration
40
- private globalAcmeDefaults: IAcmeOptions | null = null;
41
-
42
- // Callback to update SmartProxy routes for challenges
43
- private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
44
-
45
- // Flag to track if challenge route is currently active
46
- private challengeRouteActive: boolean = false;
47
-
48
- // Flag to track if provisioning is in progress
49
- private isProvisioning: boolean = false;
50
-
51
- // ACME state manager reference
52
- private acmeStateManager: AcmeStateManager | null = null;
53
-
54
- // Custom certificate provision function
55
- private certProvisionFunction?: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>;
56
-
57
- // Whether to fallback to ACME if custom provision fails
58
- private certProvisionFallbackToAcme: boolean = true;
59
-
60
- constructor(
61
- private routes: IRouteConfig[],
62
- private certDir: string = './certs',
63
- private acmeOptions?: {
64
- email?: string;
65
- useProduction?: boolean;
66
- port?: number;
67
- },
68
- private initialState?: {
69
- challengeRouteActive?: boolean;
70
- }
71
- ) {
72
- this.certStore = new CertStore(certDir);
73
-
74
- // Apply initial state if provided
75
- if (initialState) {
76
- this.challengeRouteActive = initialState.challengeRouteActive || false;
77
- }
78
- }
79
-
80
- public setHttpProxy(httpProxy: HttpProxy): void {
81
- this.httpProxy = httpProxy;
82
- }
83
-
84
-
85
- /**
86
- * Set the ACME state manager
87
- */
88
- public setAcmeStateManager(stateManager: AcmeStateManager): void {
89
- this.acmeStateManager = stateManager;
90
- }
91
-
92
- /**
93
- * Set global ACME defaults from top-level configuration
94
- */
95
- public setGlobalAcmeDefaults(defaults: IAcmeOptions): void {
96
- this.globalAcmeDefaults = defaults;
97
- }
98
-
99
- /**
100
- * Set custom certificate provision function
101
- */
102
- public setCertProvisionFunction(fn: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>): void {
103
- this.certProvisionFunction = fn;
104
- }
105
-
106
- /**
107
- * Set whether to fallback to ACME if custom provision fails
108
- */
109
- public setCertProvisionFallbackToAcme(fallback: boolean): void {
110
- this.certProvisionFallbackToAcme = fallback;
111
- }
112
-
113
- /**
114
- * Update the routes array to keep it in sync with SmartProxy
115
- * This prevents stale route data when adding/removing challenge routes
116
- */
117
- public setRoutes(routes: IRouteConfig[]): void {
118
- this.routes = routes;
119
- }
120
-
121
- /**
122
- * Set callback for updating routes (used for challenge routes)
123
- */
124
- public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
125
- this.updateRoutesCallback = callback;
126
- try {
127
- logger.log('debug', 'Route update callback set successfully', { component: 'certificate-manager' });
128
- } catch (error) {
129
- // Silently handle logging errors
130
- console.log('[DEBUG] Route update callback set successfully');
131
- }
132
- }
133
-
134
- /**
135
- * Initialize certificate manager and provision certificates for all routes
136
- */
137
- public async initialize(): Promise<void> {
138
- // Create certificate directory if it doesn't exist
139
- await this.certStore.initialize();
140
-
141
- // Initialize SmartAcme if we have any ACME routes
142
- const hasAcmeRoutes = this.routes.some(r =>
143
- r.action.tls?.certificate === 'auto'
144
- );
145
-
146
- if (hasAcmeRoutes && this.acmeOptions?.email) {
147
- // Create HTTP-01 challenge handler
148
- const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
149
-
150
- // Set up challenge handler integration with our routing
151
- this.setupChallengeHandler(http01Handler);
152
-
153
- // Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler
154
- this.smartAcme = new plugins.smartacme.SmartAcme({
155
- accountEmail: this.acmeOptions.email,
156
- environment: this.acmeOptions.useProduction ? 'production' : 'integration',
157
- certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
158
- challengeHandlers: [http01Handler]
159
- });
160
-
161
- await this.smartAcme.start();
162
-
163
- // Add challenge route once at initialization if not already active
164
- if (!this.challengeRouteActive) {
165
- logger.log('info', 'Adding ACME challenge route during initialization', { component: 'certificate-manager' });
166
- await this.addChallengeRoute();
167
- } else {
168
- logger.log('info', 'Challenge route already active from previous instance', { component: 'certificate-manager' });
169
- }
170
- }
171
-
172
- // Skip automatic certificate provisioning during initialization
173
- // This will be called later after ports are listening
174
- logger.log('info', 'Certificate manager initialized. Deferring certificate provisioning until after ports are listening.', { component: 'certificate-manager' });
175
-
176
- // Start renewal timer
177
- this.startRenewalTimer();
178
- }
179
-
180
- /**
181
- * Provision certificates for all routes that need them
182
- */
183
- public async provisionAllCertificates(): Promise<void> {
184
- const certRoutes = this.routes.filter(r =>
185
- r.action.tls?.mode === 'terminate' ||
186
- r.action.tls?.mode === 'terminate-and-reencrypt'
187
- );
188
-
189
- // Set provisioning flag to prevent concurrent operations
190
- this.isProvisioning = true;
191
-
192
- try {
193
- for (const route of certRoutes) {
194
- try {
195
- await this.provisionCertificate(route, true); // Allow concurrent since we're managing it here
196
- } catch (error) {
197
- logger.log('error', `Failed to provision certificate for route ${route.name}`, { routeName: route.name, error, component: 'certificate-manager' });
198
- }
199
- }
200
- } finally {
201
- this.isProvisioning = false;
202
- }
203
- }
204
-
205
- /**
206
- * Provision certificate for a single route
207
- */
208
- public async provisionCertificate(route: IRouteConfig, allowConcurrent: boolean = false): Promise<void> {
209
- const tls = route.action.tls;
210
- if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
211
- return;
212
- }
213
-
214
- // Check if provisioning is already in progress (prevent concurrent provisioning)
215
- if (!allowConcurrent && this.isProvisioning) {
216
- logger.log('info', `Certificate provisioning already in progress, skipping ${route.name}`, { routeName: route.name, component: 'certificate-manager' });
217
- return;
218
- }
219
-
220
- const domains = this.extractDomainsFromRoute(route);
221
- if (domains.length === 0) {
222
- logger.log('warn', `Route ${route.name} has TLS termination but no domains`, { routeName: route.name, component: 'certificate-manager' });
223
- return;
224
- }
225
-
226
- const primaryDomain = domains[0];
227
-
228
- if (tls.certificate === 'auto') {
229
- // ACME certificate
230
- await this.provisionAcmeCertificate(route, domains);
231
- } else if (typeof tls.certificate === 'object') {
232
- // Static certificate
233
- await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
234
- }
235
- }
236
-
237
- /**
238
- * Provision ACME certificate
239
- */
240
- private async provisionAcmeCertificate(
241
- route: IRouteConfig,
242
- domains: string[]
243
- ): Promise<void> {
244
- const primaryDomain = domains[0];
245
- const routeName = route.name || primaryDomain;
246
-
247
- // Check if we already have a valid certificate
248
- const existingCert = await this.certStore.getCertificate(routeName);
249
- if (existingCert && this.isCertificateValid(existingCert)) {
250
- logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
251
- await this.applyCertificate(primaryDomain, existingCert);
252
- this.updateCertStatus(routeName, 'valid', existingCert.source || 'acme', existingCert);
253
- return;
254
- }
255
-
256
- // Check for custom provision function first
257
- if (this.certProvisionFunction) {
258
- try {
259
- logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
260
- const result = await this.certProvisionFunction(primaryDomain);
261
-
262
- if (result === 'http01') {
263
- logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
264
- // Continue with existing ACME logic below
265
- } else {
266
- // Use custom certificate
267
- const customCert = result as plugins.tsclass.network.ICert;
268
-
269
- // Convert to internal certificate format
270
- const certData: ICertificateData = {
271
- cert: customCert.publicKey,
272
- key: customCert.privateKey,
273
- ca: '',
274
- issueDate: new Date(),
275
- expiryDate: this.extractExpiryDate(customCert.publicKey),
276
- source: 'custom'
277
- };
278
-
279
- // Store and apply certificate
280
- await this.certStore.saveCertificate(routeName, certData);
281
- await this.applyCertificate(primaryDomain, certData);
282
- this.updateCertStatus(routeName, 'valid', 'custom', certData);
283
-
284
- logger.log('info', `Custom certificate applied for ${primaryDomain}`, {
285
- domain: primaryDomain,
286
- expiryDate: certData.expiryDate,
287
- component: 'certificate-manager'
288
- });
289
- return;
290
- }
291
- } catch (error) {
292
- logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, {
293
- domain: primaryDomain,
294
- error: error.message,
295
- component: 'certificate-manager'
296
- });
297
- // Check if we should fallback to ACME
298
- if (!this.certProvisionFallbackToAcme) {
299
- throw error;
300
- }
301
- logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
302
- }
303
- }
304
-
305
- if (!this.smartAcme) {
306
- throw new Error(
307
- 'SmartAcme not initialized. This usually means no ACME email was provided. ' +
308
- 'Please ensure you have configured ACME with an email address either:\n' +
309
- '1. In the top-level "acme" configuration\n' +
310
- '2. In the route\'s "tls.acme" configuration'
311
- );
312
- }
313
-
314
- // Apply renewal threshold from global defaults or route config
315
- const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
316
- this.globalAcmeDefaults?.renewThresholdDays ||
317
- 30;
318
-
319
- logger.log('info', `Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`, { domains: domains.join(', '), renewThreshold, component: 'certificate-manager' });
320
- this.updateCertStatus(routeName, 'pending', 'acme');
321
-
322
- try {
323
- // Challenge route should already be active from initialization
324
- // No need to add it for each certificate
325
-
326
- // Determine if we should request a wildcard certificate
327
- // Only request wildcards if:
328
- // 1. The primary domain is not already a wildcard
329
- // 2. The domain has multiple parts (can have subdomains)
330
- // 3. We have DNS-01 challenge support (required for wildcards)
331
- const hasDnsChallenge = (this.smartAcme as any).challengeHandlers?.some((handler: any) =>
332
- handler.getSupportedTypes && handler.getSupportedTypes().includes('dns-01')
333
- );
334
-
335
- const shouldIncludeWildcard = !primaryDomain.startsWith('*.') &&
336
- primaryDomain.includes('.') &&
337
- primaryDomain.split('.').length >= 2 &&
338
- hasDnsChallenge;
339
-
340
- if (shouldIncludeWildcard) {
341
- logger.log('info', `Requesting wildcard certificate for ${primaryDomain} (DNS-01 available)`, { domain: primaryDomain, challengeType: 'DNS-01', component: 'certificate-manager' });
342
- }
343
-
344
- // Use smartacme to get certificate with optional wildcard
345
- const cert = await this.smartAcme.getCertificateForDomain(
346
- primaryDomain,
347
- shouldIncludeWildcard ? { includeWildcard: true } : undefined
348
- );
349
-
350
- // SmartAcme's Cert object has these properties:
351
- // - publicKey: The certificate PEM string
352
- // - privateKey: The private key PEM string
353
- // - csr: Certificate signing request
354
- // - validUntil: Timestamp in milliseconds
355
- // - domainName: The domain name
356
- const certData: ICertificateData = {
357
- cert: cert.publicKey,
358
- key: cert.privateKey,
359
- ca: cert.publicKey, // Use same as cert for now
360
- expiryDate: new Date(cert.validUntil),
361
- issueDate: new Date(cert.created),
362
- source: 'acme'
363
- };
364
-
365
- await this.certStore.saveCertificate(routeName, certData);
366
- await this.applyCertificate(primaryDomain, certData);
367
- this.updateCertStatus(routeName, 'valid', 'acme', certData);
368
-
369
- logger.log('info', `Successfully provisioned ACME certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
370
- } catch (error) {
371
- logger.log('error', `Failed to provision ACME certificate for ${primaryDomain}: ${error.message}`, { domain: primaryDomain, error: error.message, component: 'certificate-manager' });
372
- this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
373
- throw error;
374
- }
375
- }
376
-
377
- /**
378
- * Provision static certificate
379
- */
380
- private async provisionStaticCertificate(
381
- route: IRouteConfig,
382
- domain: string,
383
- certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
384
- ): Promise<void> {
385
- const routeName = route.name || domain;
386
-
387
- try {
388
- let key: string = certConfig.key;
389
- let cert: string = certConfig.cert;
390
-
391
- // Load from files if paths are provided
392
- const smartFileFactory = plugins.smartfile.SmartFileFactory.nodeFs();
393
- if (certConfig.keyFile) {
394
- const keyFile = await smartFileFactory.fromFilePath(certConfig.keyFile);
395
- key = keyFile.contents.toString();
396
- }
397
- if (certConfig.certFile) {
398
- const certFile = await smartFileFactory.fromFilePath(certConfig.certFile);
399
- cert = certFile.contents.toString();
400
- }
401
-
402
- // Parse certificate to get dates
403
- const expiryDate = this.extractExpiryDate(cert);
404
- const issueDate = new Date(); // Current date as issue date
405
-
406
- const certData: ICertificateData = {
407
- cert,
408
- key,
409
- expiryDate,
410
- issueDate,
411
- source: 'static'
412
- };
413
-
414
- // Save to store for consistency
415
- await this.certStore.saveCertificate(routeName, certData);
416
- await this.applyCertificate(domain, certData);
417
- this.updateCertStatus(routeName, 'valid', 'static', certData);
418
-
419
- logger.log('info', `Successfully loaded static certificate for ${domain}`, { domain, component: 'certificate-manager' });
420
- } catch (error) {
421
- logger.log('error', `Failed to provision static certificate for ${domain}: ${error.message}`, { domain, error: error.message, component: 'certificate-manager' });
422
- this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
423
- throw error;
424
- }
425
- }
426
-
427
- /**
428
- * Apply certificate to HttpProxy
429
- */
430
- private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
431
- if (!this.httpProxy) {
432
- logger.log('warn', `HttpProxy not set, cannot apply certificate for domain ${domain}`, { domain, component: 'certificate-manager' });
433
- return;
434
- }
435
-
436
- // Apply certificate to HttpProxy
437
- this.httpProxy.updateCertificate(domain, certData.cert, certData.key);
438
-
439
- // Also apply for wildcard if it's a subdomain
440
- if (domain.includes('.') && !domain.startsWith('*.')) {
441
- const parts = domain.split('.');
442
- if (parts.length >= 2) {
443
- const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
444
- this.httpProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
445
- }
446
- }
447
- }
448
-
449
- /**
450
- * Extract domains from route configuration
451
- */
452
- private extractDomainsFromRoute(route: IRouteConfig): string[] {
453
- if (!route.match.domains) {
454
- return [];
455
- }
456
-
457
- const domains = Array.isArray(route.match.domains)
458
- ? route.match.domains
459
- : [route.match.domains];
460
-
461
- // Filter out wildcards and patterns
462
- return domains.filter(d =>
463
- !d.includes('*') &&
464
- !d.includes('{') &&
465
- d.includes('.')
466
- );
467
- }
468
-
469
- /**
470
- * Check if certificate is valid
471
- */
472
- private isCertificateValid(cert: ICertificateData): boolean {
473
- const now = new Date();
474
-
475
- // Use renewal threshold from global defaults or fallback to 30 days
476
- const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30;
477
- const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000);
478
-
479
- return cert.expiryDate > expiryThreshold;
480
- }
481
-
482
- /**
483
- * Extract expiry date from a PEM certificate
484
- */
485
- private extractExpiryDate(_certPem: string): Date {
486
- // For now, we'll default to 90 days for custom certificates
487
- // In production, you might want to use a proper X.509 parser
488
- // or require the custom cert provider to include expiry info
489
- logger.log('info', 'Using default 90-day expiry for custom certificate', {
490
- component: 'certificate-manager'
491
- });
492
- return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
493
- }
494
-
495
-
496
- /**
497
- * Add challenge route to SmartProxy
498
- *
499
- * This method adds a special route for ACME HTTP-01 challenges, which typically uses port 80.
500
- * Since we may already be listening on port 80 for regular routes, we need to be
501
- * careful about how we add this route to avoid binding conflicts.
502
- */
503
- private async addChallengeRoute(): Promise<void> {
504
- // Check with state manager first - avoid duplication
505
- if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) {
506
- try {
507
- logger.log('info', 'Challenge route already active in global state, skipping', { component: 'certificate-manager' });
508
- } catch (error) {
509
- // Silently handle logging errors
510
- console.log('[INFO] Challenge route already active in global state, skipping');
511
- }
512
- this.challengeRouteActive = true;
513
- return;
514
- }
515
-
516
- if (this.challengeRouteActive) {
517
- try {
518
- logger.log('info', 'Challenge route already active locally, skipping', { component: 'certificate-manager' });
519
- } catch (error) {
520
- // Silently handle logging errors
521
- console.log('[INFO] Challenge route already active locally, skipping');
522
- }
523
- return;
524
- }
525
-
526
- if (!this.updateRoutesCallback) {
527
- throw new Error('No route update callback set');
528
- }
529
-
530
- if (!this.challengeRoute) {
531
- throw new Error('Challenge route not initialized');
532
- }
533
-
534
- // Get the challenge port
535
- const challengePort = this.globalAcmeDefaults?.port || 80;
536
-
537
- // Check if any existing routes are already using this port
538
- // This helps us determine if we need to create a new binding or can reuse existing one
539
- const portInUseByRoutes = this.routes.some(route => {
540
- const routePorts = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
541
- return routePorts.some(p => {
542
- // Handle both number and port range objects
543
- if (typeof p === 'number') {
544
- return p === challengePort;
545
- } else if (typeof p === 'object' && 'from' in p && 'to' in p) {
546
- // Port range case - check if challengePort is in range
547
- return challengePort >= p.from && challengePort <= p.to;
548
- }
549
- return false;
550
- });
551
- });
552
-
553
- try {
554
- // Log whether port is already in use by other routes
555
- if (portInUseByRoutes) {
556
- try {
557
- logger.log('info', `Port ${challengePort} is already used by another route, merging ACME challenge route`, {
558
- port: challengePort,
559
- component: 'certificate-manager'
560
- });
561
- } catch (error) {
562
- // Silently handle logging errors
563
- console.log(`[INFO] Port ${challengePort} is already used by another route, merging ACME challenge route`);
564
- }
565
- } else {
566
- try {
567
- logger.log('info', `Adding new ACME challenge route on port ${challengePort}`, {
568
- port: challengePort,
569
- component: 'certificate-manager'
570
- });
571
- } catch (error) {
572
- // Silently handle logging errors
573
- console.log(`[INFO] Adding new ACME challenge route on port ${challengePort}`);
574
- }
575
- }
576
-
577
- // Add the challenge route to the existing routes
578
- const challengeRoute = this.challengeRoute;
579
- const updatedRoutes = [...this.routes, challengeRoute];
580
-
581
- // With the re-ordering of start(), port binding should already be done
582
- // This updateRoutes call should just add the route without binding again
583
- await this.updateRoutesCallback(updatedRoutes);
584
- // Keep local routes in sync after updating
585
- this.routes = updatedRoutes;
586
- this.challengeRouteActive = true;
587
-
588
- // Register with state manager
589
- if (this.acmeStateManager) {
590
- this.acmeStateManager.addChallengeRoute(challengeRoute);
591
- }
592
-
593
- try {
594
- logger.log('info', 'ACME challenge route successfully added', { component: 'certificate-manager' });
595
- } catch (error) {
596
- // Silently handle logging errors
597
- console.log('[INFO] ACME challenge route successfully added');
598
- }
599
- } catch (error) {
600
- // Enhanced error handling based on error type
601
- if ((error as any).code === 'EADDRINUSE') {
602
- try {
603
- logger.log('warn', `Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`, {
604
- port: challengePort,
605
- error: (error as Error).message,
606
- component: 'certificate-manager'
607
- });
608
- } catch (logError) {
609
- // Silently handle logging errors
610
- console.log(`[WARN] Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`);
611
- }
612
-
613
- // Provide a more informative and actionable error message
614
- throw new Error(
615
- `ACME HTTP-01 challenge port ${challengePort} is already in use by another process. ` +
616
- `Please configure a different port using the acme.port setting (e.g., 8080).`
617
- );
618
- } else if (error.message && error.message.includes('EADDRINUSE')) {
619
- // Some Node.js versions embed the error code in the message rather than the code property
620
- try {
621
- logger.log('warn', `Port ${challengePort} conflict detected: ${error.message}`, {
622
- port: challengePort,
623
- component: 'certificate-manager'
624
- });
625
- } catch (logError) {
626
- // Silently handle logging errors
627
- console.log(`[WARN] Port ${challengePort} conflict detected: ${error.message}`);
628
- }
629
-
630
- // More detailed error message with suggestions
631
- throw new Error(
632
- `ACME HTTP challenge port ${challengePort} conflict detected. ` +
633
- `To resolve this issue, try one of these approaches:\n` +
634
- `1. Configure a different port in ACME settings (acme.port)\n` +
635
- `2. Add a regular route that uses port ${challengePort} before initializing the certificate manager\n` +
636
- `3. Stop any other services that might be using port ${challengePort}`
637
- );
638
- }
639
-
640
- // Log and rethrow other types of errors
641
- try {
642
- logger.log('error', `Failed to add challenge route: ${(error as Error).message}`, {
643
- error: (error as Error).message,
644
- component: 'certificate-manager'
645
- });
646
- } catch (logError) {
647
- // Silently handle logging errors
648
- console.log(`[ERROR] Failed to add challenge route: ${(error as Error).message}`);
649
- }
650
- throw error;
651
- }
652
- }
653
-
654
- /**
655
- * Remove challenge route from SmartProxy
656
- */
657
- private async removeChallengeRoute(): Promise<void> {
658
- if (!this.challengeRouteActive) {
659
- try {
660
- logger.log('info', 'Challenge route not active, skipping removal', { component: 'certificate-manager' });
661
- } catch (error) {
662
- // Silently handle logging errors
663
- console.log('[INFO] Challenge route not active, skipping removal');
664
- }
665
- return;
666
- }
667
-
668
- if (!this.updateRoutesCallback) {
669
- return;
670
- }
671
-
672
- try {
673
- const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
674
- await this.updateRoutesCallback(filteredRoutes);
675
- // Keep local routes in sync after updating
676
- this.routes = filteredRoutes;
677
- this.challengeRouteActive = false;
678
-
679
- // Remove from state manager
680
- if (this.acmeStateManager) {
681
- this.acmeStateManager.removeChallengeRoute('acme-challenge');
682
- }
683
-
684
- try {
685
- logger.log('info', 'ACME challenge route successfully removed', { component: 'certificate-manager' });
686
- } catch (error) {
687
- // Silently handle logging errors
688
- console.log('[INFO] ACME challenge route successfully removed');
689
- }
690
- } catch (error) {
691
- try {
692
- logger.log('error', `Failed to remove challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' });
693
- } catch (logError) {
694
- // Silently handle logging errors
695
- console.log(`[ERROR] Failed to remove challenge route: ${error.message}`);
696
- }
697
- // Reset the flag even on error to avoid getting stuck
698
- this.challengeRouteActive = false;
699
- throw error;
700
- }
701
- }
702
-
703
- /**
704
- * Start renewal timer
705
- */
706
- private startRenewalTimer(): void {
707
- // Check for renewals every 12 hours
708
- this.renewalTimer = setInterval(() => {
709
- this.checkAndRenewCertificates();
710
- }, 12 * 60 * 60 * 1000);
711
-
712
- // Unref the timer so it doesn't keep the process alive
713
- if (this.renewalTimer.unref) {
714
- this.renewalTimer.unref();
715
- }
716
-
717
- // Also do an immediate check
718
- this.checkAndRenewCertificates();
719
- }
720
-
721
- /**
722
- * Check and renew certificates that are expiring
723
- */
724
- private async checkAndRenewCertificates(): Promise<void> {
725
- for (const route of this.routes) {
726
- if (route.action.tls?.certificate === 'auto') {
727
- const routeName = route.name || this.extractDomainsFromRoute(route)[0];
728
- const cert = await this.certStore.getCertificate(routeName);
729
-
730
- if (cert && !this.isCertificateValid(cert)) {
731
- logger.log('info', `Certificate for ${routeName} needs renewal`, { routeName, component: 'certificate-manager' });
732
- try {
733
- await this.provisionCertificate(route);
734
- } catch (error) {
735
- logger.log('error', `Failed to renew certificate for ${routeName}: ${error.message}`, { routeName, error: error.message, component: 'certificate-manager' });
736
- }
737
- }
738
- }
739
- }
740
- }
741
-
742
- /**
743
- * Update certificate status
744
- */
745
- private updateCertStatus(
746
- routeName: string,
747
- status: ICertStatus['status'],
748
- source: ICertStatus['source'],
749
- certData?: ICertificateData,
750
- error?: string
751
- ): void {
752
- this.certStatus.set(routeName, {
753
- domain: routeName,
754
- status,
755
- source,
756
- expiryDate: certData?.expiryDate,
757
- issueDate: certData?.issueDate,
758
- error
759
- });
760
- }
761
-
762
- /**
763
- * Get certificate status for a route
764
- */
765
- public getCertificateStatus(routeName: string): ICertStatus | undefined {
766
- return this.certStatus.get(routeName);
767
- }
768
-
769
- /**
770
- * Force renewal of a certificate
771
- */
772
- public async renewCertificate(routeName: string): Promise<void> {
773
- const route = this.routes.find(r => r.name === routeName);
774
- if (!route) {
775
- throw new Error(`Route ${routeName} not found`);
776
- }
777
-
778
- // Remove existing certificate to force renewal
779
- await this.certStore.deleteCertificate(routeName);
780
- await this.provisionCertificate(route);
781
- }
782
-
783
- /**
784
- * Setup challenge handler integration with SmartProxy routing
785
- */
786
- private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
787
- // Use challenge port from global config or default to 80
788
- const challengePort = this.globalAcmeDefaults?.port || 80;
789
-
790
- // Create a challenge route that delegates to SmartAcme's HTTP-01 handler
791
- const challengeRoute: IRouteConfig = {
792
- name: 'acme-challenge',
793
- priority: 1000, // High priority
794
- match: {
795
- ports: challengePort,
796
- path: '/.well-known/acme-challenge/*'
797
- },
798
- action: {
799
- type: 'socket-handler',
800
- socketHandler: SocketHandlers.httpServer((req, res) => {
801
- // Extract the token from the path
802
- const token = req.url?.split('/').pop();
803
- if (!token) {
804
- res.status(404);
805
- res.send('Not found');
806
- return;
807
- }
808
-
809
- // Create mock request/response objects for SmartAcme
810
- let responseData: any = null;
811
- const mockReq = {
812
- url: req.url,
813
- method: req.method,
814
- headers: req.headers
815
- };
816
-
817
- const mockRes = {
818
- statusCode: 200,
819
- setHeader: (name: string, value: string) => {},
820
- end: (data: any) => {
821
- responseData = data;
822
- }
823
- };
824
-
825
- // Use SmartAcme's handler
826
- const handleAcme = () => {
827
- http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
828
- // Not handled by ACME
829
- res.status(404);
830
- res.send('Not found');
831
- });
832
-
833
- // Give it a moment to process, then send response
834
- setTimeout(() => {
835
- if (responseData) {
836
- res.header('Content-Type', 'text/plain');
837
- res.send(String(responseData));
838
- } else {
839
- res.status(404);
840
- res.send('Not found');
841
- }
842
- }, 100);
843
- };
844
-
845
- handleAcme();
846
- })
847
- }
848
- };
849
-
850
- // Store the challenge route to add it when needed
851
- this.challengeRoute = challengeRoute;
852
- }
853
-
854
- /**
855
- * Stop certificate manager
856
- */
857
- public async stop(): Promise<void> {
858
- if (this.renewalTimer) {
859
- clearInterval(this.renewalTimer);
860
- this.renewalTimer = null;
861
- }
862
-
863
- // Always remove challenge route on shutdown
864
- if (this.challengeRoute) {
865
- logger.log('info', 'Removing ACME challenge route during shutdown', { component: 'certificate-manager' });
866
- await this.removeChallengeRoute();
867
- }
868
-
869
- if (this.smartAcme) {
870
- await this.smartAcme.stop();
871
- }
872
-
873
- // Clear any pending challenges
874
- if (this.pendingChallenges.size > 0) {
875
- this.pendingChallenges.clear();
876
- }
877
- }
878
-
879
- /**
880
- * Get ACME options (for recreating after route updates)
881
- */
882
- public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
883
- return this.acmeOptions;
884
- }
885
-
886
- /**
887
- * Get certificate manager state
888
- */
889
- public getState(): { challengeRouteActive: boolean } {
890
- return {
891
- challengeRouteActive: this.challengeRouteActive
892
- };
893
- }
894
- }
895
-