@push.rocks/smartproxy 21.1.7 → 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 (155) hide show
  1. package/changelog.md +109 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/core/utils/shared-security-manager.d.ts +17 -0
  5. package/dist_ts/core/utils/shared-security-manager.js +66 -1
  6. package/dist_ts/index.d.ts +1 -5
  7. package/dist_ts/index.js +3 -9
  8. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  9. package/dist_ts/proxies/http-proxy/default-certificates.d.ts +54 -0
  10. package/dist_ts/proxies/http-proxy/default-certificates.js +127 -0
  11. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -1
  12. package/dist_ts/proxies/http-proxy/http-proxy.js +9 -14
  13. package/dist_ts/proxies/http-proxy/index.d.ts +5 -1
  14. package/dist_ts/proxies/http-proxy/index.js +6 -2
  15. package/dist_ts/proxies/http-proxy/security-manager.d.ts +4 -12
  16. package/dist_ts/proxies/http-proxy/security-manager.js +66 -99
  17. package/dist_ts/proxies/index.d.ts +1 -5
  18. package/dist_ts/proxies/index.js +2 -6
  19. package/dist_ts/proxies/nftables-proxy/index.d.ts +1 -0
  20. package/dist_ts/proxies/nftables-proxy/index.js +2 -1
  21. package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +4 -26
  22. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +84 -236
  23. package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +9 -0
  24. package/dist_ts/proxies/nftables-proxy/utils/index.js +12 -0
  25. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +66 -0
  26. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +131 -0
  27. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +39 -0
  28. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +112 -0
  29. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +59 -0
  30. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +130 -0
  31. package/dist_ts/proxies/smart-proxy/certificate-manager.js +4 -3
  32. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +13 -2
  33. package/dist_ts/proxies/smart-proxy/connection-manager.js +16 -6
  34. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +35 -10
  35. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  36. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  37. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -3
  38. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +17 -0
  39. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +72 -9
  40. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  41. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  42. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  43. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  44. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  45. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  46. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  47. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  48. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +14 -12
  49. package/dist_ts/proxies/smart-proxy/security-manager.js +80 -74
  50. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  51. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -622
  52. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  53. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  54. package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +2 -9
  55. package/dist_ts/proxies/smart-proxy/tls-manager.js +3 -26
  56. package/dist_ts/proxies/smart-proxy/utils/index.d.ts +1 -1
  57. package/dist_ts/proxies/smart-proxy/utils/index.js +3 -4
  58. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.d.ts +49 -0
  59. package/dist_ts/proxies/smart-proxy/utils/route-helpers/api-helpers.js +108 -0
  60. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.d.ts +57 -0
  61. package/dist_ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.js +89 -0
  62. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.d.ts +17 -0
  63. package/dist_ts/proxies/smart-proxy/utils/route-helpers/http-helpers.js +32 -0
  64. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.d.ts +68 -0
  65. package/dist_ts/proxies/smart-proxy/utils/route-helpers/https-helpers.js +117 -0
  66. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.d.ts +17 -0
  67. package/dist_ts/proxies/smart-proxy/utils/route-helpers/index.js +27 -0
  68. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.d.ts +63 -0
  69. package/dist_ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.js +105 -0
  70. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.d.ts +83 -0
  71. package/dist_ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.js +126 -0
  72. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.d.ts +47 -0
  73. package/dist_ts/proxies/smart-proxy/utils/route-helpers/security-helpers.js +66 -0
  74. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.d.ts +70 -0
  75. package/dist_ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.js +287 -0
  76. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.d.ts +46 -0
  77. package/dist_ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.js +67 -0
  78. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +4 -457
  79. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +6 -950
  80. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +2 -2
  81. package/dist_ts/proxies/smart-proxy/utils/route-validator.d.ts +67 -1
  82. package/dist_ts/proxies/smart-proxy/utils/route-validator.js +251 -3
  83. package/dist_ts/routing/index.d.ts +1 -1
  84. package/dist_ts/routing/index.js +3 -3
  85. package/dist_ts/routing/models/http-types.d.ts +119 -4
  86. package/dist_ts/routing/models/http-types.js +93 -5
  87. package/npmextra.json +12 -6
  88. package/package.json +34 -24
  89. package/readme.hints.md +184 -1
  90. package/readme.md +580 -266
  91. package/ts/00_commitinfo_data.ts +1 -1
  92. package/ts/core/utils/shared-security-manager.ts +98 -13
  93. package/ts/index.ts +4 -12
  94. package/ts/protocols/common/fragment-handler.ts +4 -0
  95. package/ts/proxies/index.ts +1 -9
  96. package/ts/proxies/nftables-proxy/index.ts +1 -0
  97. package/ts/proxies/nftables-proxy/nftables-proxy.ts +116 -290
  98. package/ts/proxies/nftables-proxy/utils/index.ts +38 -0
  99. package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +162 -0
  100. package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +125 -0
  101. package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +156 -0
  102. package/ts/proxies/smart-proxy/index.ts +6 -13
  103. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -5
  104. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  105. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  106. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  107. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  108. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -800
  109. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  110. package/ts/proxies/smart-proxy/utils/index.ts +3 -5
  111. package/ts/proxies/smart-proxy/utils/route-helpers/api-helpers.ts +144 -0
  112. package/ts/proxies/smart-proxy/utils/route-helpers/dynamic-helpers.ts +124 -0
  113. package/ts/proxies/smart-proxy/utils/route-helpers/http-helpers.ts +40 -0
  114. package/ts/proxies/smart-proxy/utils/route-helpers/https-helpers.ts +163 -0
  115. package/ts/proxies/smart-proxy/utils/route-helpers/index.ts +62 -0
  116. package/ts/proxies/smart-proxy/utils/route-helpers/load-balancer-helpers.ts +154 -0
  117. package/ts/proxies/smart-proxy/utils/route-helpers/nftables-helpers.ts +202 -0
  118. package/ts/proxies/smart-proxy/utils/route-helpers/security-helpers.ts +96 -0
  119. package/ts/proxies/smart-proxy/utils/route-helpers/socket-handlers.ts +337 -0
  120. package/ts/proxies/smart-proxy/utils/route-helpers/websocket-helpers.ts +98 -0
  121. package/ts/proxies/smart-proxy/utils/route-helpers.ts +5 -1302
  122. package/ts/proxies/smart-proxy/utils/route-utils.ts +1 -1
  123. package/ts/proxies/smart-proxy/utils/route-validator.ts +274 -4
  124. package/ts/routing/index.ts +2 -2
  125. package/ts/routing/models/http-types.ts +147 -4
  126. package/ts/proxies/http-proxy/certificate-manager.ts +0 -244
  127. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  128. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  129. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  130. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  131. package/ts/proxies/http-proxy/http-proxy.ts +0 -675
  132. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  133. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  134. package/ts/proxies/http-proxy/index.ts +0 -13
  135. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  136. package/ts/proxies/http-proxy/models/index.ts +0 -5
  137. package/ts/proxies/http-proxy/models/types.ts +0 -125
  138. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  139. package/ts/proxies/http-proxy/security-manager.ts +0 -433
  140. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  141. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  142. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  143. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -894
  144. package/ts/proxies/smart-proxy/connection-manager.ts +0 -796
  145. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -187
  146. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  147. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  148. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  149. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1640
  150. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  151. package/ts/proxies/smart-proxy/security-manager.ts +0 -257
  152. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  153. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  154. package/ts/proxies/smart-proxy/tls-manager.ts +0 -207
  155. package/ts/proxies/smart-proxy/utils/route-validators.ts +0 -283
@@ -1,894 +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
- if (certConfig.keyFile) {
393
- const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile);
394
- key = keyFile.contents.toString();
395
- }
396
- if (certConfig.certFile) {
397
- const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile);
398
- cert = certFile.contents.toString();
399
- }
400
-
401
- // Parse certificate to get dates
402
- const expiryDate = this.extractExpiryDate(cert);
403
- const issueDate = new Date(); // Current date as issue date
404
-
405
- const certData: ICertificateData = {
406
- cert,
407
- key,
408
- expiryDate,
409
- issueDate,
410
- source: 'static'
411
- };
412
-
413
- // Save to store for consistency
414
- await this.certStore.saveCertificate(routeName, certData);
415
- await this.applyCertificate(domain, certData);
416
- this.updateCertStatus(routeName, 'valid', 'static', certData);
417
-
418
- logger.log('info', `Successfully loaded static certificate for ${domain}`, { domain, component: 'certificate-manager' });
419
- } catch (error) {
420
- logger.log('error', `Failed to provision static certificate for ${domain}: ${error.message}`, { domain, error: error.message, component: 'certificate-manager' });
421
- this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
422
- throw error;
423
- }
424
- }
425
-
426
- /**
427
- * Apply certificate to HttpProxy
428
- */
429
- private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
430
- if (!this.httpProxy) {
431
- logger.log('warn', `HttpProxy not set, cannot apply certificate for domain ${domain}`, { domain, component: 'certificate-manager' });
432
- return;
433
- }
434
-
435
- // Apply certificate to HttpProxy
436
- this.httpProxy.updateCertificate(domain, certData.cert, certData.key);
437
-
438
- // Also apply for wildcard if it's a subdomain
439
- if (domain.includes('.') && !domain.startsWith('*.')) {
440
- const parts = domain.split('.');
441
- if (parts.length >= 2) {
442
- const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
443
- this.httpProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
444
- }
445
- }
446
- }
447
-
448
- /**
449
- * Extract domains from route configuration
450
- */
451
- private extractDomainsFromRoute(route: IRouteConfig): string[] {
452
- if (!route.match.domains) {
453
- return [];
454
- }
455
-
456
- const domains = Array.isArray(route.match.domains)
457
- ? route.match.domains
458
- : [route.match.domains];
459
-
460
- // Filter out wildcards and patterns
461
- return domains.filter(d =>
462
- !d.includes('*') &&
463
- !d.includes('{') &&
464
- d.includes('.')
465
- );
466
- }
467
-
468
- /**
469
- * Check if certificate is valid
470
- */
471
- private isCertificateValid(cert: ICertificateData): boolean {
472
- const now = new Date();
473
-
474
- // Use renewal threshold from global defaults or fallback to 30 days
475
- const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30;
476
- const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000);
477
-
478
- return cert.expiryDate > expiryThreshold;
479
- }
480
-
481
- /**
482
- * Extract expiry date from a PEM certificate
483
- */
484
- private extractExpiryDate(_certPem: string): Date {
485
- // For now, we'll default to 90 days for custom certificates
486
- // In production, you might want to use a proper X.509 parser
487
- // or require the custom cert provider to include expiry info
488
- logger.log('info', 'Using default 90-day expiry for custom certificate', {
489
- component: 'certificate-manager'
490
- });
491
- return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
492
- }
493
-
494
-
495
- /**
496
- * Add challenge route to SmartProxy
497
- *
498
- * This method adds a special route for ACME HTTP-01 challenges, which typically uses port 80.
499
- * Since we may already be listening on port 80 for regular routes, we need to be
500
- * careful about how we add this route to avoid binding conflicts.
501
- */
502
- private async addChallengeRoute(): Promise<void> {
503
- // Check with state manager first - avoid duplication
504
- if (this.acmeStateManager && this.acmeStateManager.isChallengeRouteActive()) {
505
- try {
506
- logger.log('info', 'Challenge route already active in global state, skipping', { component: 'certificate-manager' });
507
- } catch (error) {
508
- // Silently handle logging errors
509
- console.log('[INFO] Challenge route already active in global state, skipping');
510
- }
511
- this.challengeRouteActive = true;
512
- return;
513
- }
514
-
515
- if (this.challengeRouteActive) {
516
- try {
517
- logger.log('info', 'Challenge route already active locally, skipping', { component: 'certificate-manager' });
518
- } catch (error) {
519
- // Silently handle logging errors
520
- console.log('[INFO] Challenge route already active locally, skipping');
521
- }
522
- return;
523
- }
524
-
525
- if (!this.updateRoutesCallback) {
526
- throw new Error('No route update callback set');
527
- }
528
-
529
- if (!this.challengeRoute) {
530
- throw new Error('Challenge route not initialized');
531
- }
532
-
533
- // Get the challenge port
534
- const challengePort = this.globalAcmeDefaults?.port || 80;
535
-
536
- // Check if any existing routes are already using this port
537
- // This helps us determine if we need to create a new binding or can reuse existing one
538
- const portInUseByRoutes = this.routes.some(route => {
539
- const routePorts = Array.isArray(route.match.ports) ? route.match.ports : [route.match.ports];
540
- return routePorts.some(p => {
541
- // Handle both number and port range objects
542
- if (typeof p === 'number') {
543
- return p === challengePort;
544
- } else if (typeof p === 'object' && 'from' in p && 'to' in p) {
545
- // Port range case - check if challengePort is in range
546
- return challengePort >= p.from && challengePort <= p.to;
547
- }
548
- return false;
549
- });
550
- });
551
-
552
- try {
553
- // Log whether port is already in use by other routes
554
- if (portInUseByRoutes) {
555
- try {
556
- logger.log('info', `Port ${challengePort} is already used by another route, merging ACME challenge route`, {
557
- port: challengePort,
558
- component: 'certificate-manager'
559
- });
560
- } catch (error) {
561
- // Silently handle logging errors
562
- console.log(`[INFO] Port ${challengePort} is already used by another route, merging ACME challenge route`);
563
- }
564
- } else {
565
- try {
566
- logger.log('info', `Adding new ACME challenge route on port ${challengePort}`, {
567
- port: challengePort,
568
- component: 'certificate-manager'
569
- });
570
- } catch (error) {
571
- // Silently handle logging errors
572
- console.log(`[INFO] Adding new ACME challenge route on port ${challengePort}`);
573
- }
574
- }
575
-
576
- // Add the challenge route to the existing routes
577
- const challengeRoute = this.challengeRoute;
578
- const updatedRoutes = [...this.routes, challengeRoute];
579
-
580
- // With the re-ordering of start(), port binding should already be done
581
- // This updateRoutes call should just add the route without binding again
582
- await this.updateRoutesCallback(updatedRoutes);
583
- // Keep local routes in sync after updating
584
- this.routes = updatedRoutes;
585
- this.challengeRouteActive = true;
586
-
587
- // Register with state manager
588
- if (this.acmeStateManager) {
589
- this.acmeStateManager.addChallengeRoute(challengeRoute);
590
- }
591
-
592
- try {
593
- logger.log('info', 'ACME challenge route successfully added', { component: 'certificate-manager' });
594
- } catch (error) {
595
- // Silently handle logging errors
596
- console.log('[INFO] ACME challenge route successfully added');
597
- }
598
- } catch (error) {
599
- // Enhanced error handling based on error type
600
- if ((error as any).code === 'EADDRINUSE') {
601
- try {
602
- logger.log('warn', `Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`, {
603
- port: challengePort,
604
- error: (error as Error).message,
605
- component: 'certificate-manager'
606
- });
607
- } catch (logError) {
608
- // Silently handle logging errors
609
- console.log(`[WARN] Challenge port ${challengePort} is unavailable - it's already in use by another process. Consider configuring a different ACME port.`);
610
- }
611
-
612
- // Provide a more informative and actionable error message
613
- throw new Error(
614
- `ACME HTTP-01 challenge port ${challengePort} is already in use by another process. ` +
615
- `Please configure a different port using the acme.port setting (e.g., 8080).`
616
- );
617
- } else if (error.message && error.message.includes('EADDRINUSE')) {
618
- // Some Node.js versions embed the error code in the message rather than the code property
619
- try {
620
- logger.log('warn', `Port ${challengePort} conflict detected: ${error.message}`, {
621
- port: challengePort,
622
- component: 'certificate-manager'
623
- });
624
- } catch (logError) {
625
- // Silently handle logging errors
626
- console.log(`[WARN] Port ${challengePort} conflict detected: ${error.message}`);
627
- }
628
-
629
- // More detailed error message with suggestions
630
- throw new Error(
631
- `ACME HTTP challenge port ${challengePort} conflict detected. ` +
632
- `To resolve this issue, try one of these approaches:\n` +
633
- `1. Configure a different port in ACME settings (acme.port)\n` +
634
- `2. Add a regular route that uses port ${challengePort} before initializing the certificate manager\n` +
635
- `3. Stop any other services that might be using port ${challengePort}`
636
- );
637
- }
638
-
639
- // Log and rethrow other types of errors
640
- try {
641
- logger.log('error', `Failed to add challenge route: ${(error as Error).message}`, {
642
- error: (error as Error).message,
643
- component: 'certificate-manager'
644
- });
645
- } catch (logError) {
646
- // Silently handle logging errors
647
- console.log(`[ERROR] Failed to add challenge route: ${(error as Error).message}`);
648
- }
649
- throw error;
650
- }
651
- }
652
-
653
- /**
654
- * Remove challenge route from SmartProxy
655
- */
656
- private async removeChallengeRoute(): Promise<void> {
657
- if (!this.challengeRouteActive) {
658
- try {
659
- logger.log('info', 'Challenge route not active, skipping removal', { component: 'certificate-manager' });
660
- } catch (error) {
661
- // Silently handle logging errors
662
- console.log('[INFO] Challenge route not active, skipping removal');
663
- }
664
- return;
665
- }
666
-
667
- if (!this.updateRoutesCallback) {
668
- return;
669
- }
670
-
671
- try {
672
- const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
673
- await this.updateRoutesCallback(filteredRoutes);
674
- // Keep local routes in sync after updating
675
- this.routes = filteredRoutes;
676
- this.challengeRouteActive = false;
677
-
678
- // Remove from state manager
679
- if (this.acmeStateManager) {
680
- this.acmeStateManager.removeChallengeRoute('acme-challenge');
681
- }
682
-
683
- try {
684
- logger.log('info', 'ACME challenge route successfully removed', { component: 'certificate-manager' });
685
- } catch (error) {
686
- // Silently handle logging errors
687
- console.log('[INFO] ACME challenge route successfully removed');
688
- }
689
- } catch (error) {
690
- try {
691
- logger.log('error', `Failed to remove challenge route: ${error.message}`, { error: error.message, component: 'certificate-manager' });
692
- } catch (logError) {
693
- // Silently handle logging errors
694
- console.log(`[ERROR] Failed to remove challenge route: ${error.message}`);
695
- }
696
- // Reset the flag even on error to avoid getting stuck
697
- this.challengeRouteActive = false;
698
- throw error;
699
- }
700
- }
701
-
702
- /**
703
- * Start renewal timer
704
- */
705
- private startRenewalTimer(): void {
706
- // Check for renewals every 12 hours
707
- this.renewalTimer = setInterval(() => {
708
- this.checkAndRenewCertificates();
709
- }, 12 * 60 * 60 * 1000);
710
-
711
- // Unref the timer so it doesn't keep the process alive
712
- if (this.renewalTimer.unref) {
713
- this.renewalTimer.unref();
714
- }
715
-
716
- // Also do an immediate check
717
- this.checkAndRenewCertificates();
718
- }
719
-
720
- /**
721
- * Check and renew certificates that are expiring
722
- */
723
- private async checkAndRenewCertificates(): Promise<void> {
724
- for (const route of this.routes) {
725
- if (route.action.tls?.certificate === 'auto') {
726
- const routeName = route.name || this.extractDomainsFromRoute(route)[0];
727
- const cert = await this.certStore.getCertificate(routeName);
728
-
729
- if (cert && !this.isCertificateValid(cert)) {
730
- logger.log('info', `Certificate for ${routeName} needs renewal`, { routeName, component: 'certificate-manager' });
731
- try {
732
- await this.provisionCertificate(route);
733
- } catch (error) {
734
- logger.log('error', `Failed to renew certificate for ${routeName}: ${error.message}`, { routeName, error: error.message, component: 'certificate-manager' });
735
- }
736
- }
737
- }
738
- }
739
- }
740
-
741
- /**
742
- * Update certificate status
743
- */
744
- private updateCertStatus(
745
- routeName: string,
746
- status: ICertStatus['status'],
747
- source: ICertStatus['source'],
748
- certData?: ICertificateData,
749
- error?: string
750
- ): void {
751
- this.certStatus.set(routeName, {
752
- domain: routeName,
753
- status,
754
- source,
755
- expiryDate: certData?.expiryDate,
756
- issueDate: certData?.issueDate,
757
- error
758
- });
759
- }
760
-
761
- /**
762
- * Get certificate status for a route
763
- */
764
- public getCertificateStatus(routeName: string): ICertStatus | undefined {
765
- return this.certStatus.get(routeName);
766
- }
767
-
768
- /**
769
- * Force renewal of a certificate
770
- */
771
- public async renewCertificate(routeName: string): Promise<void> {
772
- const route = this.routes.find(r => r.name === routeName);
773
- if (!route) {
774
- throw new Error(`Route ${routeName} not found`);
775
- }
776
-
777
- // Remove existing certificate to force renewal
778
- await this.certStore.deleteCertificate(routeName);
779
- await this.provisionCertificate(route);
780
- }
781
-
782
- /**
783
- * Setup challenge handler integration with SmartProxy routing
784
- */
785
- private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
786
- // Use challenge port from global config or default to 80
787
- const challengePort = this.globalAcmeDefaults?.port || 80;
788
-
789
- // Create a challenge route that delegates to SmartAcme's HTTP-01 handler
790
- const challengeRoute: IRouteConfig = {
791
- name: 'acme-challenge',
792
- priority: 1000, // High priority
793
- match: {
794
- ports: challengePort,
795
- path: '/.well-known/acme-challenge/*'
796
- },
797
- action: {
798
- type: 'socket-handler',
799
- socketHandler: SocketHandlers.httpServer((req, res) => {
800
- // Extract the token from the path
801
- const token = req.url?.split('/').pop();
802
- if (!token) {
803
- res.status(404);
804
- res.send('Not found');
805
- return;
806
- }
807
-
808
- // Create mock request/response objects for SmartAcme
809
- let responseData: any = null;
810
- const mockReq = {
811
- url: req.url,
812
- method: req.method,
813
- headers: req.headers
814
- };
815
-
816
- const mockRes = {
817
- statusCode: 200,
818
- setHeader: (name: string, value: string) => {},
819
- end: (data: any) => {
820
- responseData = data;
821
- }
822
- };
823
-
824
- // Use SmartAcme's handler
825
- const handleAcme = () => {
826
- http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
827
- // Not handled by ACME
828
- res.status(404);
829
- res.send('Not found');
830
- });
831
-
832
- // Give it a moment to process, then send response
833
- setTimeout(() => {
834
- if (responseData) {
835
- res.header('Content-Type', 'text/plain');
836
- res.send(String(responseData));
837
- } else {
838
- res.status(404);
839
- res.send('Not found');
840
- }
841
- }, 100);
842
- };
843
-
844
- handleAcme();
845
- })
846
- }
847
- };
848
-
849
- // Store the challenge route to add it when needed
850
- this.challengeRoute = challengeRoute;
851
- }
852
-
853
- /**
854
- * Stop certificate manager
855
- */
856
- public async stop(): Promise<void> {
857
- if (this.renewalTimer) {
858
- clearInterval(this.renewalTimer);
859
- this.renewalTimer = null;
860
- }
861
-
862
- // Always remove challenge route on shutdown
863
- if (this.challengeRoute) {
864
- logger.log('info', 'Removing ACME challenge route during shutdown', { component: 'certificate-manager' });
865
- await this.removeChallengeRoute();
866
- }
867
-
868
- if (this.smartAcme) {
869
- await this.smartAcme.stop();
870
- }
871
-
872
- // Clear any pending challenges
873
- if (this.pendingChallenges.size > 0) {
874
- this.pendingChallenges.clear();
875
- }
876
- }
877
-
878
- /**
879
- * Get ACME options (for recreating after route updates)
880
- */
881
- public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
882
- return this.acmeOptions;
883
- }
884
-
885
- /**
886
- * Get certificate manager state
887
- */
888
- public getState(): { challengeRouteActive: boolean } {
889
- return {
890
- challengeRouteActive: this.challengeRouteActive
891
- };
892
- }
893
- }
894
-