@push.rocks/smartproxy 18.2.0 → 19.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/common/eventUtils.d.ts +1 -2
- package/dist_ts/common/eventUtils.js +2 -1
- package/dist_ts/core/models/common-types.d.ts +1 -1
- package/dist_ts/core/models/common-types.js +1 -1
- package/dist_ts/core/utils/event-utils.d.ts +9 -9
- package/dist_ts/core/utils/event-utils.js +6 -14
- package/dist_ts/http/models/http-types.d.ts +13 -1
- package/dist_ts/http/models/http-types.js +1 -1
- package/dist_ts/index.d.ts +4 -6
- package/dist_ts/index.js +4 -10
- package/dist_ts/proxies/index.d.ts +3 -2
- package/dist_ts/proxies/index.js +4 -5
- package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +31 -49
- package/dist_ts/proxies/network-proxy/certificate-manager.js +77 -374
- package/dist_ts/proxies/network-proxy/models/types.d.ts +12 -1
- package/dist_ts/proxies/network-proxy/models/types.js +1 -1
- package/dist_ts/proxies/network-proxy/network-proxy.d.ts +2 -7
- package/dist_ts/proxies/network-proxy/network-proxy.js +10 -19
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +6 -0
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +24 -5
- package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -1
- package/dist_ts/proxies/smart-proxy/models/index.js +1 -5
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +30 -1
- package/dist_ts/proxies/smart-proxy/route-manager.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/route-manager.js +7 -1
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +4 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +112 -26
- package/package.json +1 -2
- package/readme.hints.md +31 -1
- package/readme.md +82 -6
- package/readme.plan.md +109 -1417
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/common/eventUtils.ts +2 -2
- package/ts/core/models/common-types.ts +1 -1
- package/ts/core/utils/event-utils.ts +12 -21
- package/ts/http/models/http-types.ts +8 -4
- package/ts/index.ts +11 -14
- package/ts/proxies/index.ts +7 -4
- package/ts/proxies/network-proxy/certificate-manager.ts +92 -417
- package/ts/proxies/network-proxy/models/types.ts +14 -2
- package/ts/proxies/network-proxy/network-proxy.ts +10 -19
- package/ts/proxies/smart-proxy/certificate-manager.ts +31 -4
- package/ts/proxies/smart-proxy/models/index.ts +2 -1
- package/ts/proxies/smart-proxy/models/interfaces.ts +31 -2
- package/ts/proxies/smart-proxy/models/route-types.ts +1 -1
- package/ts/proxies/smart-proxy/route-manager.ts +7 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +142 -25
- package/ts/certificate/acme/acme-factory.ts +0 -48
- package/ts/certificate/acme/challenge-handler.ts +0 -110
- package/ts/certificate/acme/index.ts +0 -3
- package/ts/certificate/events/certificate-events.ts +0 -36
- package/ts/certificate/index.ts +0 -75
- package/ts/certificate/models/certificate-types.ts +0 -109
- package/ts/certificate/providers/cert-provisioner.ts +0 -519
- package/ts/certificate/providers/index.ts +0 -3
- package/ts/certificate/storage/file-storage.ts +0 -234
- package/ts/certificate/storage/index.ts +0 -3
- package/ts/certificate/utils/certificate-helpers.ts +0 -50
- package/ts/http/port80/acme-interfaces.ts +0 -169
- package/ts/http/port80/challenge-responder.ts +0 -246
- package/ts/http/port80/index.ts +0 -13
- package/ts/http/port80/port80-handler.ts +0 -728
|
@@ -18,7 +18,6 @@ import { RequestHandler, type IMetricsTracker } from './request-handler.js';
|
|
|
18
18
|
import { WebSocketHandler } from './websocket-handler.js';
|
|
19
19
|
import { ProxyRouter } from '../../http/router/index.js';
|
|
20
20
|
import { RouteRouter } from '../../http/router/route-router.js';
|
|
21
|
-
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
|
22
21
|
import { FunctionCache } from './function-cache.js';
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -221,15 +220,10 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
/**
|
|
224
|
-
*
|
|
225
|
-
* This allows the NetworkProxy to use a centrally managed Port80Handler
|
|
226
|
-
* instead of creating its own
|
|
227
|
-
*
|
|
228
|
-
* @param handler The Port80Handler instance to use
|
|
223
|
+
* @deprecated Use SmartCertManager instead
|
|
229
224
|
*/
|
|
230
|
-
public setExternalPort80Handler(handler:
|
|
231
|
-
|
|
232
|
-
this.certificateManager.setExternalPort80Handler(handler);
|
|
225
|
+
public setExternalPort80Handler(handler: any): void {
|
|
226
|
+
this.logger.warn('Port80Handler is deprecated - use SmartCertManager instead');
|
|
233
227
|
}
|
|
234
228
|
|
|
235
229
|
/**
|
|
@@ -238,10 +232,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
238
232
|
public async start(): Promise<void> {
|
|
239
233
|
this.startTime = Date.now();
|
|
240
234
|
|
|
241
|
-
//
|
|
242
|
-
if (this.options.acme?.enabled && !this.options.useExternalPort80Handler) {
|
|
243
|
-
await this.certificateManager.initializePort80Handler();
|
|
244
|
-
}
|
|
235
|
+
// Certificate management is now handled by SmartCertManager
|
|
245
236
|
|
|
246
237
|
// Create HTTP/2 server with HTTP/1 fallback
|
|
247
238
|
this.httpsServer = plugins.http2.createSecureServer(
|
|
@@ -385,7 +376,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
385
376
|
|
|
386
377
|
// Directly update the certificate manager with the new routes
|
|
387
378
|
// This will extract domains and handle certificate provisioning
|
|
388
|
-
this.certificateManager.
|
|
379
|
+
this.certificateManager.updateRoutes(routes);
|
|
389
380
|
|
|
390
381
|
// Collect all domains and certificates for configuration
|
|
391
382
|
const currentHostnames = new Set<string>();
|
|
@@ -425,7 +416,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
425
416
|
// Update certificate cache with any static certificates
|
|
426
417
|
for (const [domain, certData] of certificateUpdates.entries()) {
|
|
427
418
|
try {
|
|
428
|
-
this.certificateManager.
|
|
419
|
+
this.certificateManager.updateCertificate(
|
|
429
420
|
domain,
|
|
430
421
|
certData.cert,
|
|
431
422
|
certData.key
|
|
@@ -547,8 +538,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
547
538
|
// Close all connection pool connections
|
|
548
539
|
this.connectionPool.closeAllConnections();
|
|
549
540
|
|
|
550
|
-
//
|
|
551
|
-
await this.certificateManager.stopPort80Handler();
|
|
541
|
+
// Certificate management cleanup is handled by SmartCertManager
|
|
552
542
|
|
|
553
543
|
// Close the HTTPS server
|
|
554
544
|
return new Promise((resolve) => {
|
|
@@ -566,7 +556,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
566
556
|
* @returns A promise that resolves when the request is submitted (not when the certificate is issued)
|
|
567
557
|
*/
|
|
568
558
|
public async requestCertificate(domain: string): Promise<boolean> {
|
|
569
|
-
|
|
559
|
+
this.logger.warn('requestCertificate is deprecated - use SmartCertManager instead');
|
|
560
|
+
return false;
|
|
570
561
|
}
|
|
571
562
|
|
|
572
563
|
/**
|
|
@@ -587,7 +578,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|
|
587
578
|
expiryDate?: Date
|
|
588
579
|
): void {
|
|
589
580
|
this.logger.info(`Updating certificate for ${domain}`);
|
|
590
|
-
this.certificateManager.
|
|
581
|
+
this.certificateManager.updateCertificate(domain, certificate, privateKey);
|
|
591
582
|
}
|
|
592
583
|
|
|
593
584
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { NetworkProxy } from '../network-proxy/index.js';
|
|
3
3
|
import type { IRouteConfig, IRouteTls } from './models/route-types.js';
|
|
4
|
+
import type { IAcmeOptions } from './models/interfaces.js';
|
|
4
5
|
import { CertStore } from './cert-store.js';
|
|
5
6
|
|
|
6
7
|
export interface ICertStatus {
|
|
@@ -31,6 +32,9 @@ export class SmartCertManager {
|
|
|
31
32
|
// Track certificate status by route name
|
|
32
33
|
private certStatus: Map<string, ICertStatus> = new Map();
|
|
33
34
|
|
|
35
|
+
// Global ACME defaults from top-level configuration
|
|
36
|
+
private globalAcmeDefaults: IAcmeOptions | null = null;
|
|
37
|
+
|
|
34
38
|
// Callback to update SmartProxy routes for challenges
|
|
35
39
|
private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
|
|
36
40
|
|
|
@@ -50,6 +54,13 @@ export class SmartCertManager {
|
|
|
50
54
|
this.networkProxy = networkProxy;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Set global ACME defaults from top-level configuration
|
|
59
|
+
*/
|
|
60
|
+
public setGlobalAcmeDefaults(defaults: IAcmeOptions): void {
|
|
61
|
+
this.globalAcmeDefaults = defaults;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* Set callback for updating routes (used for challenge routes)
|
|
55
66
|
*/
|
|
@@ -146,7 +157,12 @@ export class SmartCertManager {
|
|
|
146
157
|
domains: string[]
|
|
147
158
|
): Promise<void> {
|
|
148
159
|
if (!this.smartAcme) {
|
|
149
|
-
throw new Error(
|
|
160
|
+
throw new Error(
|
|
161
|
+
'SmartAcme not initialized. This usually means no ACME email was provided. ' +
|
|
162
|
+
'Please ensure you have configured ACME with an email address either:\n' +
|
|
163
|
+
'1. In the top-level "acme" configuration\n' +
|
|
164
|
+
'2. In the route\'s "tls.acme" configuration'
|
|
165
|
+
);
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
const primaryDomain = domains[0];
|
|
@@ -161,7 +177,12 @@ export class SmartCertManager {
|
|
|
161
177
|
return;
|
|
162
178
|
}
|
|
163
179
|
|
|
164
|
-
|
|
180
|
+
// Apply renewal threshold from global defaults or route config
|
|
181
|
+
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
|
|
182
|
+
this.globalAcmeDefaults?.renewThresholdDays ||
|
|
183
|
+
30;
|
|
184
|
+
|
|
185
|
+
console.log(`Requesting ACME certificate for ${domains.join(', ')} (renew ${renewThreshold} days before expiry)`);
|
|
165
186
|
this.updateCertStatus(routeName, 'pending', 'acme');
|
|
166
187
|
|
|
167
188
|
try {
|
|
@@ -303,7 +324,10 @@ export class SmartCertManager {
|
|
|
303
324
|
*/
|
|
304
325
|
private isCertificateValid(cert: ICertificateData): boolean {
|
|
305
326
|
const now = new Date();
|
|
306
|
-
|
|
327
|
+
|
|
328
|
+
// Use renewal threshold from global defaults or fallback to 30 days
|
|
329
|
+
const renewThresholdDays = this.globalAcmeDefaults?.renewThresholdDays || 30;
|
|
330
|
+
const expiryThreshold = new Date(now.getTime() + renewThresholdDays * 24 * 60 * 60 * 1000);
|
|
307
331
|
|
|
308
332
|
return cert.expiryDate > expiryThreshold;
|
|
309
333
|
}
|
|
@@ -417,12 +441,15 @@ export class SmartCertManager {
|
|
|
417
441
|
* Setup challenge handler integration with SmartProxy routing
|
|
418
442
|
*/
|
|
419
443
|
private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
|
|
444
|
+
// Use challenge port from global config or default to 80
|
|
445
|
+
const challengePort = this.globalAcmeDefaults?.port || 80;
|
|
446
|
+
|
|
420
447
|
// Create a challenge route that delegates to SmartAcme's HTTP-01 handler
|
|
421
448
|
const challengeRoute: IRouteConfig = {
|
|
422
449
|
name: 'acme-challenge',
|
|
423
450
|
priority: 1000, // High priority
|
|
424
451
|
match: {
|
|
425
|
-
ports:
|
|
452
|
+
ports: challengePort,
|
|
426
453
|
path: '/.well-known/acme-challenge/*'
|
|
427
454
|
},
|
|
428
455
|
action: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SmartProxy models
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
// Export everything except IAcmeOptions from interfaces
|
|
5
|
+
export type { ISmartProxyOptions, IConnectionRecord, TSmartProxyCertProvisionObject } from './interfaces.js';
|
|
5
6
|
export * from './route-types.js';
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import * as plugins from '../../../plugins.js';
|
|
2
|
-
|
|
2
|
+
// Certificate types removed - define IAcmeOptions locally
|
|
3
|
+
export interface IAcmeOptions {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
email?: string; // Required when any route uses certificate: 'auto'
|
|
6
|
+
environment?: 'production' | 'staging';
|
|
7
|
+
accountEmail?: string; // Alias for email
|
|
8
|
+
port?: number; // Port for HTTP-01 challenges (default: 80)
|
|
9
|
+
useProduction?: boolean; // Use Let's Encrypt production (default: false)
|
|
10
|
+
renewThresholdDays?: number; // Days before expiry to renew (default: 30)
|
|
11
|
+
autoRenew?: boolean; // Enable automatic renewal (default: true)
|
|
12
|
+
certificateStore?: string; // Directory to store certificates (default: './certs')
|
|
13
|
+
skipConfiguredCerts?: boolean;
|
|
14
|
+
renewCheckIntervalHours?: number; // How often to check for renewals (default: 24)
|
|
15
|
+
routeForwards?: any[];
|
|
16
|
+
}
|
|
3
17
|
import type { IRouteConfig } from './route-types.js';
|
|
4
18
|
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
|
5
19
|
|
|
@@ -84,7 +98,22 @@ export interface ISmartProxyOptions {
|
|
|
84
98
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
85
99
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Global ACME configuration options for SmartProxy
|
|
103
|
+
*
|
|
104
|
+
* When set, these options will be used as defaults for all routes
|
|
105
|
+
* with certificate: 'auto' that don't have their own ACME configuration.
|
|
106
|
+
* Route-specific ACME settings will override these defaults.
|
|
107
|
+
*
|
|
108
|
+
* Example:
|
|
109
|
+
* ```ts
|
|
110
|
+
* acme: {
|
|
111
|
+
* email: 'ssl@example.com',
|
|
112
|
+
* useProduction: false,
|
|
113
|
+
* port: 80
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
88
117
|
acme?: IAcmeOptions;
|
|
89
118
|
|
|
90
119
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../../../plugins.js';
|
|
2
|
-
|
|
2
|
+
// Certificate types removed - use local definition
|
|
3
3
|
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
|
4
4
|
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
|
|
5
5
|
|
|
@@ -173,6 +173,13 @@ export class RouteManager extends plugins.EventEmitter {
|
|
|
173
173
|
return this.portMap.get(port) || [];
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Get all routes
|
|
178
|
+
*/
|
|
179
|
+
public getAllRoutes(): IRouteConfig[] {
|
|
180
|
+
return [...this.routes];
|
|
181
|
+
}
|
|
182
|
+
|
|
176
183
|
/**
|
|
177
184
|
* Test if a pattern matches a domain using glob matching
|
|
178
185
|
*/
|
|
@@ -115,21 +115,26 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
115
115
|
networkProxyPort: settingsArg.networkProxyPort || 8443,
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// Normalize ACME options if provided (support both email and accountEmail)
|
|
119
|
+
if (this.settings.acme) {
|
|
120
|
+
// Support both 'email' and 'accountEmail' fields
|
|
121
|
+
if (this.settings.acme.accountEmail && !this.settings.acme.email) {
|
|
122
|
+
this.settings.acme.email = this.settings.acme.accountEmail;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Set reasonable defaults for commonly used fields
|
|
121
126
|
this.settings.acme = {
|
|
122
|
-
enabled: false,
|
|
123
|
-
port: 80,
|
|
124
|
-
|
|
125
|
-
useProduction: false,
|
|
126
|
-
renewThresholdDays: 30,
|
|
127
|
-
autoRenew:
|
|
128
|
-
certificateStore: './certs',
|
|
129
|
-
skipConfiguredCerts: false,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
enabled: this.settings.acme.enabled !== false, // Enable by default if acme object exists
|
|
128
|
+
port: this.settings.acme.port || 80,
|
|
129
|
+
email: this.settings.acme.email,
|
|
130
|
+
useProduction: this.settings.acme.useProduction || false,
|
|
131
|
+
renewThresholdDays: this.settings.acme.renewThresholdDays || 30,
|
|
132
|
+
autoRenew: this.settings.acme.autoRenew !== false, // Enable by default
|
|
133
|
+
certificateStore: this.settings.acme.certificateStore || './certs',
|
|
134
|
+
skipConfiguredCerts: this.settings.acme.skipConfiguredCerts || false,
|
|
135
|
+
renewCheckIntervalHours: this.settings.acme.renewCheckIntervalHours || 24,
|
|
136
|
+
routeForwards: this.settings.acme.routeForwards || [],
|
|
137
|
+
...this.settings.acme // Preserve any additional fields
|
|
133
138
|
};
|
|
134
139
|
}
|
|
135
140
|
|
|
@@ -187,19 +192,55 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
187
192
|
return;
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
//
|
|
191
|
-
|
|
195
|
+
// Prepare ACME options with priority:
|
|
196
|
+
// 1. Use top-level ACME config if available
|
|
197
|
+
// 2. Fall back to first auto route's ACME config
|
|
198
|
+
// 3. Otherwise use undefined
|
|
199
|
+
let acmeOptions: { email?: string; useProduction?: boolean; port?: number } | undefined;
|
|
200
|
+
|
|
201
|
+
if (this.settings.acme?.email) {
|
|
202
|
+
// Use top-level ACME config
|
|
203
|
+
acmeOptions = {
|
|
204
|
+
email: this.settings.acme.email,
|
|
205
|
+
useProduction: this.settings.acme.useProduction || false,
|
|
206
|
+
port: this.settings.acme.port || 80
|
|
207
|
+
};
|
|
208
|
+
console.log(`Using top-level ACME configuration with email: ${acmeOptions.email}`);
|
|
209
|
+
} else if (autoRoutes.length > 0) {
|
|
210
|
+
// Check for route-level ACME config
|
|
211
|
+
const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email);
|
|
212
|
+
if (routeWithAcme?.action.tls?.acme) {
|
|
213
|
+
const routeAcme = routeWithAcme.action.tls.acme;
|
|
214
|
+
acmeOptions = {
|
|
215
|
+
email: routeAcme.email,
|
|
216
|
+
useProduction: routeAcme.useProduction || false,
|
|
217
|
+
port: routeAcme.challengePort || 80
|
|
218
|
+
};
|
|
219
|
+
console.log(`Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate we have required configuration
|
|
224
|
+
if (autoRoutes.length > 0 && !acmeOptions?.email) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
'ACME email is required for automatic certificate provisioning. ' +
|
|
227
|
+
'Please provide email in either:\n' +
|
|
228
|
+
'1. Top-level "acme" configuration\n' +
|
|
229
|
+
'2. Individual route\'s "tls.acme" configuration'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
192
232
|
|
|
193
233
|
this.certManager = new SmartCertManager(
|
|
194
234
|
this.settings.routes,
|
|
195
|
-
'./certs',
|
|
196
|
-
|
|
197
|
-
email: defaultAcme.email,
|
|
198
|
-
useProduction: defaultAcme.useProduction,
|
|
199
|
-
port: defaultAcme.challengePort || 80
|
|
200
|
-
} : undefined
|
|
235
|
+
this.settings.acme?.certificateStore || './certs',
|
|
236
|
+
acmeOptions
|
|
201
237
|
);
|
|
202
238
|
|
|
239
|
+
// Pass down the global ACME config to the cert manager
|
|
240
|
+
if (this.settings.acme) {
|
|
241
|
+
this.certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
242
|
+
}
|
|
243
|
+
|
|
203
244
|
// Connect with NetworkProxy
|
|
204
245
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
205
246
|
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
@@ -250,9 +291,14 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
250
291
|
|
|
251
292
|
// Validate the route configuration
|
|
252
293
|
const configWarnings = this.routeManager.validateConfiguration();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
294
|
+
|
|
295
|
+
// Also validate ACME configuration
|
|
296
|
+
const acmeWarnings = this.validateAcmeConfiguration();
|
|
297
|
+
const allWarnings = [...configWarnings, ...acmeWarnings];
|
|
298
|
+
|
|
299
|
+
if (allWarnings.length > 0) {
|
|
300
|
+
console.log("Configuration warnings:");
|
|
301
|
+
for (const warning of allWarnings) {
|
|
256
302
|
console.log(` - ${warning}`);
|
|
257
303
|
}
|
|
258
304
|
}
|
|
@@ -664,5 +710,76 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
664
710
|
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
|
665
711
|
return this.nftablesManager.getStatus();
|
|
666
712
|
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Validate ACME configuration
|
|
716
|
+
*/
|
|
717
|
+
private validateAcmeConfiguration(): string[] {
|
|
718
|
+
const warnings: string[] = [];
|
|
719
|
+
|
|
720
|
+
// Check for routes with certificate: 'auto'
|
|
721
|
+
const autoRoutes = this.settings.routes.filter(r =>
|
|
722
|
+
r.action.tls?.certificate === 'auto'
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
if (autoRoutes.length === 0) {
|
|
726
|
+
return warnings;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Check if we have ACME email configuration
|
|
730
|
+
const hasTopLevelEmail = this.settings.acme?.email;
|
|
731
|
+
const routesWithEmail = autoRoutes.filter(r => r.action.tls?.acme?.email);
|
|
732
|
+
|
|
733
|
+
if (!hasTopLevelEmail && routesWithEmail.length === 0) {
|
|
734
|
+
warnings.push(
|
|
735
|
+
'Routes with certificate: "auto" require ACME email configuration. ' +
|
|
736
|
+
'Add email to either top-level "acme" config or individual route\'s "tls.acme" config.'
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Check for port 80 availability for challenges
|
|
741
|
+
if (autoRoutes.length > 0) {
|
|
742
|
+
const challengePort = this.settings.acme?.port || 80;
|
|
743
|
+
const portsInUse = this.routeManager.getListeningPorts();
|
|
744
|
+
|
|
745
|
+
if (!portsInUse.includes(challengePort)) {
|
|
746
|
+
warnings.push(
|
|
747
|
+
`Port ${challengePort} is not configured for any routes but is needed for ACME challenges. ` +
|
|
748
|
+
`Add a route listening on port ${challengePort} or ensure it's accessible for HTTP-01 challenges.`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Check for mismatched environments
|
|
754
|
+
if (this.settings.acme?.useProduction) {
|
|
755
|
+
const stagingRoutes = autoRoutes.filter(r =>
|
|
756
|
+
r.action.tls?.acme?.useProduction === false
|
|
757
|
+
);
|
|
758
|
+
if (stagingRoutes.length > 0) {
|
|
759
|
+
warnings.push(
|
|
760
|
+
'Top-level ACME uses production but some routes use staging. ' +
|
|
761
|
+
'Consider aligning environments to avoid certificate issues.'
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Check for wildcard domains with auto certificates
|
|
767
|
+
for (const route of autoRoutes) {
|
|
768
|
+
const domains = Array.isArray(route.match.domains)
|
|
769
|
+
? route.match.domains
|
|
770
|
+
: [route.match.domains];
|
|
771
|
+
|
|
772
|
+
const wildcardDomains = domains.filter(d => d?.includes('*'));
|
|
773
|
+
if (wildcardDomains.length > 0) {
|
|
774
|
+
warnings.push(
|
|
775
|
+
`Route "${route.name}" has wildcard domain(s) ${wildcardDomains.join(', ')} ` +
|
|
776
|
+
'with certificate: "auto". Wildcard certificates require DNS-01 challenges, ' +
|
|
777
|
+
'which are not currently supported. Use static certificates instead.'
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return warnings;
|
|
783
|
+
}
|
|
667
784
|
|
|
668
785
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import type { IAcmeOptions } from '../models/certificate-types.js';
|
|
4
|
-
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
|
5
|
-
// We'll need to update this import when we move the Port80Handler
|
|
6
|
-
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Factory to create a Port80Handler with common setup.
|
|
10
|
-
* Ensures the certificate store directory exists and instantiates the handler.
|
|
11
|
-
* @param options Port80Handler configuration options
|
|
12
|
-
* @returns A new Port80Handler instance
|
|
13
|
-
*/
|
|
14
|
-
export function buildPort80Handler(
|
|
15
|
-
options: IAcmeOptions
|
|
16
|
-
): Port80Handler {
|
|
17
|
-
if (options.certificateStore) {
|
|
18
|
-
ensureCertificateDirectory(options.certificateStore);
|
|
19
|
-
console.log(`Ensured certificate store directory: ${options.certificateStore}`);
|
|
20
|
-
}
|
|
21
|
-
return new Port80Handler(options);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Creates default ACME options with sensible defaults
|
|
26
|
-
* @param email Account email for ACME provider
|
|
27
|
-
* @param certificateStore Path to store certificates
|
|
28
|
-
* @param useProduction Whether to use production ACME servers
|
|
29
|
-
* @returns Configured ACME options
|
|
30
|
-
*/
|
|
31
|
-
export function createDefaultAcmeOptions(
|
|
32
|
-
email: string,
|
|
33
|
-
certificateStore: string,
|
|
34
|
-
useProduction: boolean = false
|
|
35
|
-
): IAcmeOptions {
|
|
36
|
-
return {
|
|
37
|
-
accountEmail: email,
|
|
38
|
-
enabled: true,
|
|
39
|
-
port: 80,
|
|
40
|
-
useProduction,
|
|
41
|
-
httpsRedirectPort: 443,
|
|
42
|
-
renewThresholdDays: 30,
|
|
43
|
-
renewCheckIntervalHours: 24,
|
|
44
|
-
autoRenew: true,
|
|
45
|
-
certificateStore,
|
|
46
|
-
skipConfiguredCerts: false
|
|
47
|
-
};
|
|
48
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import * as plugins from '../../plugins.js';
|
|
2
|
-
import type { IAcmeOptions, ICertificateData } from '../models/certificate-types.js';
|
|
3
|
-
import { CertificateEvents } from '../events/certificate-events.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Manages ACME challenges and certificate validation
|
|
7
|
-
*/
|
|
8
|
-
export class AcmeChallengeHandler extends plugins.EventEmitter {
|
|
9
|
-
private options: IAcmeOptions;
|
|
10
|
-
private client: any; // ACME client from plugins
|
|
11
|
-
private pendingChallenges: Map<string, any>;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Creates a new ACME challenge handler
|
|
15
|
-
* @param options ACME configuration options
|
|
16
|
-
*/
|
|
17
|
-
constructor(options: IAcmeOptions) {
|
|
18
|
-
super();
|
|
19
|
-
this.options = options;
|
|
20
|
-
this.pendingChallenges = new Map();
|
|
21
|
-
|
|
22
|
-
// Initialize ACME client if needed
|
|
23
|
-
// This is just a placeholder implementation since we don't use the actual
|
|
24
|
-
// client directly in this implementation - it's handled by Port80Handler
|
|
25
|
-
this.client = null;
|
|
26
|
-
console.log('Created challenge handler with options:',
|
|
27
|
-
options.accountEmail,
|
|
28
|
-
options.useProduction ? 'production' : 'staging'
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Gets or creates the ACME account key
|
|
34
|
-
*/
|
|
35
|
-
private getAccountKey(): Buffer {
|
|
36
|
-
// Implementation details would depend on plugin requirements
|
|
37
|
-
// This is a simplified version
|
|
38
|
-
if (!this.options.certificateStore) {
|
|
39
|
-
throw new Error('Certificate store is required for ACME challenges');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// This is just a placeholder - actual implementation would check for
|
|
43
|
-
// existing account key and create one if needed
|
|
44
|
-
return Buffer.from('account-key-placeholder');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Validates a domain using HTTP-01 challenge
|
|
49
|
-
* @param domain Domain to validate
|
|
50
|
-
* @param challengeToken ACME challenge token
|
|
51
|
-
* @param keyAuthorization Key authorization for the challenge
|
|
52
|
-
*/
|
|
53
|
-
public async handleHttpChallenge(
|
|
54
|
-
domain: string,
|
|
55
|
-
challengeToken: string,
|
|
56
|
-
keyAuthorization: string
|
|
57
|
-
): Promise<void> {
|
|
58
|
-
// Store challenge for response
|
|
59
|
-
this.pendingChallenges.set(challengeToken, keyAuthorization);
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
// Wait for challenge validation - this would normally be handled by the ACME client
|
|
63
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
64
|
-
this.emit(CertificateEvents.CERTIFICATE_ISSUED, {
|
|
65
|
-
domain,
|
|
66
|
-
success: true
|
|
67
|
-
});
|
|
68
|
-
} catch (error) {
|
|
69
|
-
this.emit(CertificateEvents.CERTIFICATE_FAILED, {
|
|
70
|
-
domain,
|
|
71
|
-
error: error instanceof Error ? error.message : String(error),
|
|
72
|
-
isRenewal: false
|
|
73
|
-
});
|
|
74
|
-
throw error;
|
|
75
|
-
} finally {
|
|
76
|
-
// Clean up the challenge
|
|
77
|
-
this.pendingChallenges.delete(challengeToken);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Responds to an HTTP-01 challenge request
|
|
83
|
-
* @param token Challenge token from the request path
|
|
84
|
-
* @returns The key authorization if found
|
|
85
|
-
*/
|
|
86
|
-
public getChallengeResponse(token: string): string | null {
|
|
87
|
-
return this.pendingChallenges.get(token) || null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Checks if a request path is an ACME challenge
|
|
92
|
-
* @param path Request path
|
|
93
|
-
* @returns True if this is an ACME challenge request
|
|
94
|
-
*/
|
|
95
|
-
public isAcmeChallenge(path: string): boolean {
|
|
96
|
-
return path.startsWith('/.well-known/acme-challenge/');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Extracts the challenge token from an ACME challenge path
|
|
101
|
-
* @param path Request path
|
|
102
|
-
* @returns The challenge token if valid
|
|
103
|
-
*/
|
|
104
|
-
public extractChallengeToken(path: string): string | null {
|
|
105
|
-
if (!this.isAcmeChallenge(path)) return null;
|
|
106
|
-
|
|
107
|
-
const parts = path.split('/');
|
|
108
|
-
return parts[parts.length - 1] || null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Certificate-related events emitted by certificate management components
|
|
3
|
-
*/
|
|
4
|
-
export enum CertificateEvents {
|
|
5
|
-
CERTIFICATE_ISSUED = 'certificate-issued',
|
|
6
|
-
CERTIFICATE_RENEWED = 'certificate-renewed',
|
|
7
|
-
CERTIFICATE_FAILED = 'certificate-failed',
|
|
8
|
-
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
|
9
|
-
CERTIFICATE_APPLIED = 'certificate-applied',
|
|
10
|
-
// Events moved from Port80Handler for compatibility
|
|
11
|
-
MANAGER_STARTED = 'manager-started',
|
|
12
|
-
MANAGER_STOPPED = 'manager-stopped',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Port80Handler-specific events including certificate-related ones
|
|
17
|
-
* @deprecated Use CertificateEvents and HttpEvents instead
|
|
18
|
-
*/
|
|
19
|
-
export enum Port80HandlerEvents {
|
|
20
|
-
CERTIFICATE_ISSUED = 'certificate-issued',
|
|
21
|
-
CERTIFICATE_RENEWED = 'certificate-renewed',
|
|
22
|
-
CERTIFICATE_FAILED = 'certificate-failed',
|
|
23
|
-
CERTIFICATE_EXPIRING = 'certificate-expiring',
|
|
24
|
-
MANAGER_STARTED = 'manager-started',
|
|
25
|
-
MANAGER_STOPPED = 'manager-stopped',
|
|
26
|
-
REQUEST_FORWARDED = 'request-forwarded',
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Certificate provider events
|
|
31
|
-
*/
|
|
32
|
-
export enum CertProvisionerEvents {
|
|
33
|
-
CERTIFICATE_ISSUED = 'certificate',
|
|
34
|
-
CERTIFICATE_RENEWED = 'certificate',
|
|
35
|
-
CERTIFICATE_FAILED = 'certificate-failed'
|
|
36
|
-
}
|