@push.rocks/smartproxy 22.4.2 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +36 -0
- package/dist_rust/rustproxy +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +1 -6
- package/dist_ts/index.js +3 -11
- package/dist_ts/protocols/common/fragment-handler.js +5 -1
- package/dist_ts/proxies/index.d.ts +1 -6
- package/dist_ts/proxies/index.js +2 -8
- package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
- package/dist_ts/proxies/smart-proxy/index.js +7 -13
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
- package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
- package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
- package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
- package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
- package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
- package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
- package/dist_ts/routing/index.d.ts +1 -1
- package/dist_ts/routing/index.js +3 -3
- package/dist_ts/routing/models/http-types.d.ts +119 -4
- package/dist_ts/routing/models/http-types.js +93 -5
- package/package.json +1 -1
- package/readme.md +444 -219
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -15
- package/ts/protocols/common/fragment-handler.ts +4 -0
- package/ts/proxies/index.ts +1 -12
- package/ts/proxies/smart-proxy/index.ts +6 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
- package/ts/proxies/smart-proxy/models/route-types.ts +0 -2
- package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
- package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
- package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
- package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
- package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
- package/ts/routing/index.ts +2 -2
- package/ts/routing/models/http-types.ts +147 -4
- package/dist_ts/proxies/nftables-proxy/index.d.ts +0 -6
- package/dist_ts/proxies/nftables-proxy/index.js +0 -7
- package/dist_ts/proxies/nftables-proxy/models/errors.d.ts +0 -15
- package/dist_ts/proxies/nftables-proxy/models/errors.js +0 -28
- package/dist_ts/proxies/nftables-proxy/models/index.d.ts +0 -5
- package/dist_ts/proxies/nftables-proxy/models/index.js +0 -6
- package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +0 -75
- package/dist_ts/proxies/nftables-proxy/models/interfaces.js +0 -5
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +0 -124
- package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +0 -1374
- package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +0 -9
- package/dist_ts/proxies/nftables-proxy/utils/index.js +0 -12
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +0 -66
- package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +0 -131
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +0 -39
- package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +0 -112
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +0 -59
- package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +0 -130
- package/ts/proxies/http-proxy/connection-pool.ts +0 -228
- package/ts/proxies/http-proxy/context-creator.ts +0 -145
- package/ts/proxies/http-proxy/default-certificates.ts +0 -150
- package/ts/proxies/http-proxy/function-cache.ts +0 -279
- package/ts/proxies/http-proxy/handlers/index.ts +0 -5
- package/ts/proxies/http-proxy/http-proxy.ts +0 -669
- package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
- package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
- package/ts/proxies/http-proxy/index.ts +0 -18
- package/ts/proxies/http-proxy/models/http-types.ts +0 -148
- package/ts/proxies/http-proxy/models/index.ts +0 -5
- package/ts/proxies/http-proxy/models/types.ts +0 -125
- package/ts/proxies/http-proxy/request-handler.ts +0 -878
- package/ts/proxies/http-proxy/security-manager.ts +0 -413
- package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
- package/ts/proxies/nftables-proxy/index.ts +0 -6
- package/ts/proxies/nftables-proxy/models/errors.ts +0 -30
- package/ts/proxies/nftables-proxy/models/index.ts +0 -5
- package/ts/proxies/nftables-proxy/models/interfaces.ts +0 -94
- package/ts/proxies/nftables-proxy/nftables-proxy.ts +0 -1754
- package/ts/proxies/nftables-proxy/utils/index.ts +0 -38
- package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +0 -162
- package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +0 -125
- package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +0 -156
- package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
- package/ts/proxies/smart-proxy/cert-store.ts +0 -92
- package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
- package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
- package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
- package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
- package/ts/proxies/smart-proxy/port-manager.ts +0 -358
- package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
- package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
- package/ts/proxies/smart-proxy/security-manager.ts +0 -269
- package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
- package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
- 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
|
-
|