@push.rocks/smartproxy 18.0.2 → 18.2.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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/certificate/certificate-manager.d.ts +150 -0
- package/dist_ts/certificate/certificate-manager.js +505 -0
- package/dist_ts/certificate/events/simplified-events.d.ts +56 -0
- package/dist_ts/certificate/events/simplified-events.js +13 -0
- package/dist_ts/certificate/models/certificate-errors.d.ts +69 -0
- package/dist_ts/certificate/models/certificate-errors.js +141 -0
- package/dist_ts/certificate/models/certificate-strategy.d.ts +60 -0
- package/dist_ts/certificate/models/certificate-strategy.js +73 -0
- package/dist_ts/certificate/simplified-certificate-manager.d.ts +150 -0
- package/dist_ts/certificate/simplified-certificate-manager.js +501 -0
- package/dist_ts/http/index.d.ts +1 -9
- package/dist_ts/http/index.js +5 -11
- package/dist_ts/plugins.d.ts +3 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/proxies/network-proxy/network-proxy.js +3 -1
- package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.d.ts +48 -0
- package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.js +76 -0
- package/dist_ts/proxies/network-proxy/websocket-handler.js +41 -4
- package/dist_ts/proxies/smart-proxy/cert-store.d.ts +10 -0
- package/dist_ts/proxies/smart-proxy/cert-store.js +70 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +116 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +401 -0
- package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.d.ts +168 -0
- package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.js +642 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +26 -0
- package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
- package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.d.ts +65 -0
- package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.js +31 -0
- package/dist_ts/proxies/smart-proxy/models/smartproxy-options.d.ts +102 -0
- package/dist_ts/proxies/smart-proxy/models/smartproxy-options.js +73 -0
- package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +10 -44
- package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +66 -202
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +62 -2
- package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.d.ts +41 -0
- package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.js +132 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +18 -13
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +79 -196
- package/package.json +7 -5
- package/readme.md +224 -10
- package/readme.plan.md +1405 -617
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/http/index.ts +5 -12
- package/ts/plugins.ts +4 -1
- package/ts/proxies/network-proxy/network-proxy.ts +3 -0
- package/ts/proxies/network-proxy/websocket-handler.ts +38 -3
- package/ts/proxies/smart-proxy/cert-store.ts +86 -0
- package/ts/proxies/smart-proxy/certificate-manager.ts +506 -0
- package/ts/proxies/smart-proxy/models/route-types.ts +33 -3
- package/ts/proxies/smart-proxy/network-proxy-bridge.ts +86 -239
- package/ts/proxies/smart-proxy/route-connection-handler.ts +74 -1
- package/ts/proxies/smart-proxy/smart-proxy.ts +105 -222
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import { NetworkProxy } from '../network-proxy/index.js';
|
|
3
|
+
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
|
|
4
|
+
import { CertStore } from './cert-store.js';
|
|
5
|
+
|
|
6
|
+
export interface ICertStatus {
|
|
7
|
+
domain: string;
|
|
8
|
+
status: 'valid' | 'pending' | 'expired' | 'error';
|
|
9
|
+
expiryDate?: Date;
|
|
10
|
+
issueDate?: Date;
|
|
11
|
+
source: 'static' | 'acme';
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ICertificateData {
|
|
16
|
+
cert: string;
|
|
17
|
+
key: string;
|
|
18
|
+
ca?: string;
|
|
19
|
+
expiryDate: Date;
|
|
20
|
+
issueDate: Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SmartCertManager {
|
|
24
|
+
private certStore: CertStore;
|
|
25
|
+
private smartAcme: plugins.smartacme.SmartAcme | null = null;
|
|
26
|
+
private networkProxy: NetworkProxy | null = null;
|
|
27
|
+
private renewalTimer: NodeJS.Timeout | null = null;
|
|
28
|
+
private pendingChallenges: Map<string, string> = new Map();
|
|
29
|
+
private challengeRoute: IRouteConfig | null = null;
|
|
30
|
+
|
|
31
|
+
// Track certificate status by route name
|
|
32
|
+
private certStatus: Map<string, ICertStatus> = new Map();
|
|
33
|
+
|
|
34
|
+
// Callback to update SmartProxy routes for challenges
|
|
35
|
+
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private routes: IRouteConfig[],
|
|
39
|
+
private certDir: string = './certs',
|
|
40
|
+
private acmeOptions?: {
|
|
41
|
+
email?: string;
|
|
42
|
+
useProduction?: boolean;
|
|
43
|
+
port?: number;
|
|
44
|
+
}
|
|
45
|
+
) {
|
|
46
|
+
this.certStore = new CertStore(certDir);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public setNetworkProxy(networkProxy: NetworkProxy): void {
|
|
50
|
+
this.networkProxy = networkProxy;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set callback for updating routes (used for challenge routes)
|
|
55
|
+
*/
|
|
56
|
+
public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
|
|
57
|
+
this.updateRoutesCallback = callback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize certificate manager and provision certificates for all routes
|
|
62
|
+
*/
|
|
63
|
+
public async initialize(): Promise<void> {
|
|
64
|
+
// Create certificate directory if it doesn't exist
|
|
65
|
+
await this.certStore.initialize();
|
|
66
|
+
|
|
67
|
+
// Initialize SmartAcme if we have any ACME routes
|
|
68
|
+
const hasAcmeRoutes = this.routes.some(r =>
|
|
69
|
+
r.action.tls?.certificate === 'auto'
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (hasAcmeRoutes && this.acmeOptions?.email) {
|
|
73
|
+
// Create HTTP-01 challenge handler
|
|
74
|
+
const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
|
|
75
|
+
|
|
76
|
+
// Set up challenge handler integration with our routing
|
|
77
|
+
this.setupChallengeHandler(http01Handler);
|
|
78
|
+
|
|
79
|
+
// Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler
|
|
80
|
+
this.smartAcme = new plugins.smartacme.SmartAcme({
|
|
81
|
+
accountEmail: this.acmeOptions.email,
|
|
82
|
+
environment: this.acmeOptions.useProduction ? 'production' : 'integration',
|
|
83
|
+
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
|
84
|
+
challengeHandlers: [http01Handler]
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await this.smartAcme.start();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Provision certificates for all routes
|
|
91
|
+
await this.provisionAllCertificates();
|
|
92
|
+
|
|
93
|
+
// Start renewal timer
|
|
94
|
+
this.startRenewalTimer();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Provision certificates for all routes that need them
|
|
99
|
+
*/
|
|
100
|
+
private async provisionAllCertificates(): Promise<void> {
|
|
101
|
+
const certRoutes = this.routes.filter(r =>
|
|
102
|
+
r.action.tls?.mode === 'terminate' ||
|
|
103
|
+
r.action.tls?.mode === 'terminate-and-reencrypt'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (const route of certRoutes) {
|
|
107
|
+
try {
|
|
108
|
+
await this.provisionCertificate(route);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Provision certificate for a single route
|
|
117
|
+
*/
|
|
118
|
+
public async provisionCertificate(route: IRouteConfig): Promise<void> {
|
|
119
|
+
const tls = route.action.tls;
|
|
120
|
+
if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const domains = this.extractDomainsFromRoute(route);
|
|
125
|
+
if (domains.length === 0) {
|
|
126
|
+
console.warn(`Route ${route.name} has TLS termination but no domains`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const primaryDomain = domains[0];
|
|
131
|
+
|
|
132
|
+
if (tls.certificate === 'auto') {
|
|
133
|
+
// ACME certificate
|
|
134
|
+
await this.provisionAcmeCertificate(route, domains);
|
|
135
|
+
} else if (typeof tls.certificate === 'object') {
|
|
136
|
+
// Static certificate
|
|
137
|
+
await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Provision ACME certificate
|
|
143
|
+
*/
|
|
144
|
+
private async provisionAcmeCertificate(
|
|
145
|
+
route: IRouteConfig,
|
|
146
|
+
domains: string[]
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
if (!this.smartAcme) {
|
|
149
|
+
throw new Error('SmartAcme not initialized');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const primaryDomain = domains[0];
|
|
153
|
+
const routeName = route.name || primaryDomain;
|
|
154
|
+
|
|
155
|
+
// Check if we already have a valid certificate
|
|
156
|
+
const existingCert = await this.certStore.getCertificate(routeName);
|
|
157
|
+
if (existingCert && this.isCertificateValid(existingCert)) {
|
|
158
|
+
console.log(`Using existing valid certificate for ${primaryDomain}`);
|
|
159
|
+
await this.applyCertificate(primaryDomain, existingCert);
|
|
160
|
+
this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
|
|
165
|
+
this.updateCertStatus(routeName, 'pending', 'acme');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Add challenge route before requesting certificate
|
|
169
|
+
await this.addChallengeRoute();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Use smartacme to get certificate
|
|
173
|
+
const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
|
|
174
|
+
|
|
175
|
+
// SmartAcme's Cert object has these properties:
|
|
176
|
+
// - publicKey: The certificate PEM string
|
|
177
|
+
// - privateKey: The private key PEM string
|
|
178
|
+
// - csr: Certificate signing request
|
|
179
|
+
// - validUntil: Timestamp in milliseconds
|
|
180
|
+
// - domainName: The domain name
|
|
181
|
+
const certData: ICertificateData = {
|
|
182
|
+
cert: cert.publicKey,
|
|
183
|
+
key: cert.privateKey,
|
|
184
|
+
ca: cert.publicKey, // Use same as cert for now
|
|
185
|
+
expiryDate: new Date(cert.validUntil),
|
|
186
|
+
issueDate: new Date(cert.created)
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
await this.certStore.saveCertificate(routeName, certData);
|
|
190
|
+
await this.applyCertificate(primaryDomain, certData);
|
|
191
|
+
this.updateCertStatus(routeName, 'valid', 'acme', certData);
|
|
192
|
+
|
|
193
|
+
console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
|
|
196
|
+
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
|
|
197
|
+
throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
// Always remove challenge route after provisioning
|
|
200
|
+
await this.removeChallengeRoute();
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Handle outer try-catch from adding challenge route
|
|
204
|
+
console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`);
|
|
205
|
+
this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Provision static certificate
|
|
212
|
+
*/
|
|
213
|
+
private async provisionStaticCertificate(
|
|
214
|
+
route: IRouteConfig,
|
|
215
|
+
domain: string,
|
|
216
|
+
certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
const routeName = route.name || domain;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
let key: string = certConfig.key;
|
|
222
|
+
let cert: string = certConfig.cert;
|
|
223
|
+
|
|
224
|
+
// Load from files if paths are provided
|
|
225
|
+
if (certConfig.keyFile) {
|
|
226
|
+
const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile);
|
|
227
|
+
key = keyFile.contents.toString();
|
|
228
|
+
}
|
|
229
|
+
if (certConfig.certFile) {
|
|
230
|
+
const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile);
|
|
231
|
+
cert = certFile.contents.toString();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse certificate to get dates
|
|
235
|
+
// Parse certificate to get dates - for now just use defaults
|
|
236
|
+
// TODO: Implement actual certificate parsing if needed
|
|
237
|
+
const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() };
|
|
238
|
+
|
|
239
|
+
const certData: ICertificateData = {
|
|
240
|
+
cert,
|
|
241
|
+
key,
|
|
242
|
+
expiryDate: certInfo.validTo,
|
|
243
|
+
issueDate: certInfo.validFrom
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Save to store for consistency
|
|
247
|
+
await this.certStore.saveCertificate(routeName, certData);
|
|
248
|
+
await this.applyCertificate(domain, certData);
|
|
249
|
+
this.updateCertStatus(routeName, 'valid', 'static', certData);
|
|
250
|
+
|
|
251
|
+
console.log(`Successfully loaded static certificate for ${domain}`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`Failed to provision static certificate for ${domain}: ${error}`);
|
|
254
|
+
this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Apply certificate to NetworkProxy
|
|
261
|
+
*/
|
|
262
|
+
private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
|
|
263
|
+
if (!this.networkProxy) {
|
|
264
|
+
console.warn('NetworkProxy not set, cannot apply certificate');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Apply certificate to NetworkProxy
|
|
269
|
+
this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
|
|
270
|
+
|
|
271
|
+
// Also apply for wildcard if it's a subdomain
|
|
272
|
+
if (domain.includes('.') && !domain.startsWith('*.')) {
|
|
273
|
+
const parts = domain.split('.');
|
|
274
|
+
if (parts.length >= 2) {
|
|
275
|
+
const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
|
|
276
|
+
this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Extract domains from route configuration
|
|
283
|
+
*/
|
|
284
|
+
private extractDomainsFromRoute(route: IRouteConfig): string[] {
|
|
285
|
+
if (!route.match.domains) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const domains = Array.isArray(route.match.domains)
|
|
290
|
+
? route.match.domains
|
|
291
|
+
: [route.match.domains];
|
|
292
|
+
|
|
293
|
+
// Filter out wildcards and patterns
|
|
294
|
+
return domains.filter(d =>
|
|
295
|
+
!d.includes('*') &&
|
|
296
|
+
!d.includes('{') &&
|
|
297
|
+
d.includes('.')
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if certificate is valid
|
|
303
|
+
*/
|
|
304
|
+
private isCertificateValid(cert: ICertificateData): boolean {
|
|
305
|
+
const now = new Date();
|
|
306
|
+
const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
|
307
|
+
|
|
308
|
+
return cert.expiryDate > expiryThreshold;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Add challenge route to SmartProxy
|
|
314
|
+
*/
|
|
315
|
+
private async addChallengeRoute(): Promise<void> {
|
|
316
|
+
if (!this.updateRoutesCallback) {
|
|
317
|
+
throw new Error('No route update callback set');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!this.challengeRoute) {
|
|
321
|
+
throw new Error('Challenge route not initialized');
|
|
322
|
+
}
|
|
323
|
+
const challengeRoute = this.challengeRoute;
|
|
324
|
+
|
|
325
|
+
const updatedRoutes = [...this.routes, challengeRoute];
|
|
326
|
+
await this.updateRoutesCallback(updatedRoutes);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Remove challenge route from SmartProxy
|
|
331
|
+
*/
|
|
332
|
+
private async removeChallengeRoute(): Promise<void> {
|
|
333
|
+
if (!this.updateRoutesCallback) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
|
|
338
|
+
await this.updateRoutesCallback(filteredRoutes);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Start renewal timer
|
|
343
|
+
*/
|
|
344
|
+
private startRenewalTimer(): void {
|
|
345
|
+
// Check for renewals every 12 hours
|
|
346
|
+
this.renewalTimer = setInterval(() => {
|
|
347
|
+
this.checkAndRenewCertificates();
|
|
348
|
+
}, 12 * 60 * 60 * 1000);
|
|
349
|
+
|
|
350
|
+
// Also do an immediate check
|
|
351
|
+
this.checkAndRenewCertificates();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check and renew certificates that are expiring
|
|
356
|
+
*/
|
|
357
|
+
private async checkAndRenewCertificates(): Promise<void> {
|
|
358
|
+
for (const route of this.routes) {
|
|
359
|
+
if (route.action.tls?.certificate === 'auto') {
|
|
360
|
+
const routeName = route.name || this.extractDomainsFromRoute(route)[0];
|
|
361
|
+
const cert = await this.certStore.getCertificate(routeName);
|
|
362
|
+
|
|
363
|
+
if (cert && !this.isCertificateValid(cert)) {
|
|
364
|
+
console.log(`Certificate for ${routeName} needs renewal`);
|
|
365
|
+
try {
|
|
366
|
+
await this.provisionCertificate(route);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error(`Failed to renew certificate for ${routeName}: ${error}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Update certificate status
|
|
377
|
+
*/
|
|
378
|
+
private updateCertStatus(
|
|
379
|
+
routeName: string,
|
|
380
|
+
status: ICertStatus['status'],
|
|
381
|
+
source: ICertStatus['source'],
|
|
382
|
+
certData?: ICertificateData,
|
|
383
|
+
error?: string
|
|
384
|
+
): void {
|
|
385
|
+
this.certStatus.set(routeName, {
|
|
386
|
+
domain: routeName,
|
|
387
|
+
status,
|
|
388
|
+
source,
|
|
389
|
+
expiryDate: certData?.expiryDate,
|
|
390
|
+
issueDate: certData?.issueDate,
|
|
391
|
+
error
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get certificate status for a route
|
|
397
|
+
*/
|
|
398
|
+
public getCertificateStatus(routeName: string): ICertStatus | undefined {
|
|
399
|
+
return this.certStatus.get(routeName);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Force renewal of a certificate
|
|
404
|
+
*/
|
|
405
|
+
public async renewCertificate(routeName: string): Promise<void> {
|
|
406
|
+
const route = this.routes.find(r => r.name === routeName);
|
|
407
|
+
if (!route) {
|
|
408
|
+
throw new Error(`Route ${routeName} not found`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Remove existing certificate to force renewal
|
|
412
|
+
await this.certStore.deleteCertificate(routeName);
|
|
413
|
+
await this.provisionCertificate(route);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Setup challenge handler integration with SmartProxy routing
|
|
418
|
+
*/
|
|
419
|
+
private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
|
|
420
|
+
// Create a challenge route that delegates to SmartAcme's HTTP-01 handler
|
|
421
|
+
const challengeRoute: IRouteConfig = {
|
|
422
|
+
name: 'acme-challenge',
|
|
423
|
+
priority: 1000, // High priority
|
|
424
|
+
match: {
|
|
425
|
+
ports: 80,
|
|
426
|
+
path: '/.well-known/acme-challenge/*'
|
|
427
|
+
},
|
|
428
|
+
action: {
|
|
429
|
+
type: 'static',
|
|
430
|
+
handler: async (context) => {
|
|
431
|
+
// Extract the token from the path
|
|
432
|
+
const token = context.path?.split('/').pop();
|
|
433
|
+
if (!token) {
|
|
434
|
+
return { status: 404, body: 'Not found' };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Create mock request/response objects for SmartAcme
|
|
438
|
+
const mockReq = {
|
|
439
|
+
url: context.path,
|
|
440
|
+
method: 'GET',
|
|
441
|
+
headers: context.headers || {}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
let responseData: any = null;
|
|
445
|
+
const mockRes = {
|
|
446
|
+
statusCode: 200,
|
|
447
|
+
setHeader: (name: string, value: string) => {},
|
|
448
|
+
end: (data: any) => {
|
|
449
|
+
responseData = data;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Use SmartAcme's handler
|
|
454
|
+
const handled = await new Promise<boolean>((resolve) => {
|
|
455
|
+
http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
|
|
456
|
+
resolve(false);
|
|
457
|
+
});
|
|
458
|
+
// Give it a moment to process
|
|
459
|
+
setTimeout(() => resolve(true), 100);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (handled && responseData) {
|
|
463
|
+
return {
|
|
464
|
+
status: mockRes.statusCode,
|
|
465
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
466
|
+
body: responseData
|
|
467
|
+
};
|
|
468
|
+
} else {
|
|
469
|
+
return { status: 404, body: 'Not found' };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Store the challenge route to add it when needed
|
|
476
|
+
this.challengeRoute = challengeRoute;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Stop certificate manager
|
|
481
|
+
*/
|
|
482
|
+
public async stop(): Promise<void> {
|
|
483
|
+
if (this.renewalTimer) {
|
|
484
|
+
clearInterval(this.renewalTimer);
|
|
485
|
+
this.renewalTimer = null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (this.smartAcme) {
|
|
489
|
+
await this.smartAcme.stop();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Remove any active challenge routes
|
|
493
|
+
if (this.pendingChallenges.size > 0) {
|
|
494
|
+
this.pendingChallenges.clear();
|
|
495
|
+
await this.removeChallengeRoute();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Get ACME options (for recreating after route updates)
|
|
501
|
+
*/
|
|
502
|
+
public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
|
|
503
|
+
return this.acmeOptions;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
@@ -73,15 +73,42 @@ export interface IRouteTarget {
|
|
|
73
73
|
port: number | 'preserve' | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* ACME configuration for automatic certificate provisioning
|
|
78
|
+
*/
|
|
79
|
+
export interface IRouteAcme {
|
|
80
|
+
email: string; // Contact email for ACME account
|
|
81
|
+
useProduction?: boolean; // Use production ACME servers (default: false)
|
|
82
|
+
challengePort?: number; // Port for HTTP-01 challenges (default: 80)
|
|
83
|
+
renewBeforeDays?: number; // Days before expiry to renew (default: 30)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Static route handler response
|
|
88
|
+
*/
|
|
89
|
+
export interface IStaticResponse {
|
|
90
|
+
status: number;
|
|
91
|
+
headers?: Record<string, string>;
|
|
92
|
+
body: string | Buffer;
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
/**
|
|
77
96
|
* TLS configuration for route actions
|
|
78
97
|
*/
|
|
79
98
|
export interface IRouteTls {
|
|
80
99
|
mode: TTlsMode;
|
|
81
|
-
certificate?: 'auto' | {
|
|
82
|
-
key: string;
|
|
83
|
-
cert: string;
|
|
100
|
+
certificate?: 'auto' | { // Auto = use ACME
|
|
101
|
+
key: string; // PEM-encoded private key
|
|
102
|
+
cert: string; // PEM-encoded certificate
|
|
103
|
+
ca?: string; // PEM-encoded CA chain
|
|
104
|
+
keyFile?: string; // Path to key file (overrides key)
|
|
105
|
+
certFile?: string; // Path to cert file (overrides cert)
|
|
84
106
|
};
|
|
107
|
+
acme?: IRouteAcme; // ACME options when certificate is 'auto'
|
|
108
|
+
versions?: string[]; // Allowed TLS versions (e.g., ['TLSv1.2', 'TLSv1.3'])
|
|
109
|
+
ciphers?: string; // OpenSSL cipher string
|
|
110
|
+
honorCipherOrder?: boolean; // Use server's cipher preferences
|
|
111
|
+
sessionTimeout?: number; // TLS session timeout in seconds
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
/**
|
|
@@ -266,6 +293,9 @@ export interface IRouteAction {
|
|
|
266
293
|
|
|
267
294
|
// NFTables-specific options
|
|
268
295
|
nftables?: INfTablesOptions;
|
|
296
|
+
|
|
297
|
+
// Handler function for static routes
|
|
298
|
+
handler?: (context: IRouteContext) => Promise<IStaticResponse>;
|
|
269
299
|
}
|
|
270
300
|
|
|
271
301
|
/**
|