@push.rocks/smartproxy 19.0.0 → 19.2.3
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/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/interfaces.d.ts +17 -0
- 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 +9 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +132 -38
- package/package.json +1 -2
- package/readme.hints.md +31 -1
- package/readme.md +69 -1
- package/readme.plan.md +101 -1459
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/proxies/smart-proxy/certificate-manager.ts +31 -4
- package/ts/proxies/smart-proxy/models/interfaces.ts +24 -8
- package/ts/proxies/smart-proxy/route-manager.ts +7 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +173 -45
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '19.
|
|
6
|
+
version: '19.2.3',
|
|
7
7
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
|
8
8
|
}
|
|
@@ -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: {
|
|
@@ -2,15 +2,16 @@ import * as plugins from '../../../plugins.js';
|
|
|
2
2
|
// Certificate types removed - define IAcmeOptions locally
|
|
3
3
|
export interface IAcmeOptions {
|
|
4
4
|
enabled?: boolean;
|
|
5
|
-
email?: string;
|
|
5
|
+
email?: string; // Required when any route uses certificate: 'auto'
|
|
6
6
|
environment?: 'production' | 'staging';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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')
|
|
12
13
|
skipConfiguredCerts?: boolean;
|
|
13
|
-
renewCheckIntervalHours?: number;
|
|
14
|
+
renewCheckIntervalHours?: number; // How often to check for renewals (default: 24)
|
|
14
15
|
routeForwards?: any[];
|
|
15
16
|
}
|
|
16
17
|
import type { IRouteConfig } from './route-types.js';
|
|
@@ -97,7 +98,22 @@ export interface ISmartProxyOptions {
|
|
|
97
98
|
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
98
99
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
99
100
|
|
|
100
|
-
|
|
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
|
+
*/
|
|
101
117
|
acme?: IAcmeOptions;
|
|
102
118
|
|
|
103
119
|
/**
|
|
@@ -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,20 +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
|
-
email:
|
|
125
|
-
useProduction: false,
|
|
126
|
-
renewThresholdDays: 30,
|
|
127
|
-
autoRenew:
|
|
128
|
-
certificateStore: './certs',
|
|
129
|
-
skipConfiguredCerts: false,
|
|
130
|
-
renewCheckIntervalHours: 24,
|
|
131
|
-
routeForwards: []
|
|
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
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -172,6 +178,36 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
172
178
|
*/
|
|
173
179
|
public settings: ISmartProxyOptions;
|
|
174
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Helper method to create and configure certificate manager
|
|
183
|
+
* This ensures consistent setup including the required ACME callback
|
|
184
|
+
*/
|
|
185
|
+
private async createCertificateManager(
|
|
186
|
+
routes: IRouteConfig[],
|
|
187
|
+
certStore: string = './certs',
|
|
188
|
+
acmeOptions?: any
|
|
189
|
+
): Promise<SmartCertManager> {
|
|
190
|
+
const certManager = new SmartCertManager(routes, certStore, acmeOptions);
|
|
191
|
+
|
|
192
|
+
// Always set up the route update callback for ACME challenges
|
|
193
|
+
certManager.setUpdateRoutesCallback(async (routes) => {
|
|
194
|
+
await this.updateRoutes(routes);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Connect with NetworkProxy if available
|
|
198
|
+
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
199
|
+
certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Pass down the global ACME config if available
|
|
203
|
+
if (this.settings.acme) {
|
|
204
|
+
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await certManager.initialize();
|
|
208
|
+
return certManager;
|
|
209
|
+
}
|
|
210
|
+
|
|
175
211
|
/**
|
|
176
212
|
* Initialize certificate manager
|
|
177
213
|
*/
|
|
@@ -186,30 +222,50 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
186
222
|
return;
|
|
187
223
|
}
|
|
188
224
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
225
|
+
// Prepare ACME options with priority:
|
|
226
|
+
// 1. Use top-level ACME config if available
|
|
227
|
+
// 2. Fall back to first auto route's ACME config
|
|
228
|
+
// 3. Otherwise use undefined
|
|
229
|
+
let acmeOptions: { email?: string; useProduction?: boolean; port?: number } | undefined;
|
|
230
|
+
|
|
231
|
+
if (this.settings.acme?.email) {
|
|
232
|
+
// Use top-level ACME config
|
|
233
|
+
acmeOptions = {
|
|
234
|
+
email: this.settings.acme.email,
|
|
235
|
+
useProduction: this.settings.acme.useProduction || false,
|
|
236
|
+
port: this.settings.acme.port || 80
|
|
237
|
+
};
|
|
238
|
+
console.log(`Using top-level ACME configuration with email: ${acmeOptions.email}`);
|
|
239
|
+
} else if (autoRoutes.length > 0) {
|
|
240
|
+
// Check for route-level ACME config
|
|
241
|
+
const routeWithAcme = autoRoutes.find(r => r.action.tls?.acme?.email);
|
|
242
|
+
if (routeWithAcme?.action.tls?.acme) {
|
|
243
|
+
const routeAcme = routeWithAcme.action.tls.acme;
|
|
244
|
+
acmeOptions = {
|
|
245
|
+
email: routeAcme.email,
|
|
246
|
+
useProduction: routeAcme.useProduction || false,
|
|
247
|
+
port: routeAcme.challengePort || 80
|
|
248
|
+
};
|
|
249
|
+
console.log(`Using route-level ACME configuration from route '${routeWithAcme.name}' with email: ${acmeOptions.email}`);
|
|
250
|
+
}
|
|
205
251
|
}
|
|
206
252
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
253
|
+
// Validate we have required configuration
|
|
254
|
+
if (autoRoutes.length > 0 && !acmeOptions?.email) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
'ACME email is required for automatic certificate provisioning. ' +
|
|
257
|
+
'Please provide email in either:\n' +
|
|
258
|
+
'1. Top-level "acme" configuration\n' +
|
|
259
|
+
'2. Individual route\'s "tls.acme" configuration'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
211
262
|
|
|
212
|
-
|
|
263
|
+
// Use the helper method to create and configure the certificate manager
|
|
264
|
+
this.certManager = await this.createCertificateManager(
|
|
265
|
+
this.settings.routes,
|
|
266
|
+
this.settings.acme?.certificateStore || './certs',
|
|
267
|
+
acmeOptions
|
|
268
|
+
);
|
|
213
269
|
}
|
|
214
270
|
|
|
215
271
|
/**
|
|
@@ -249,9 +305,14 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
249
305
|
|
|
250
306
|
// Validate the route configuration
|
|
251
307
|
const configWarnings = this.routeManager.validateConfiguration();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
308
|
+
|
|
309
|
+
// Also validate ACME configuration
|
|
310
|
+
const acmeWarnings = this.validateAcmeConfiguration();
|
|
311
|
+
const allWarnings = [...configWarnings, ...acmeWarnings];
|
|
312
|
+
|
|
313
|
+
if (allWarnings.length > 0) {
|
|
314
|
+
console.log("Configuration warnings:");
|
|
315
|
+
for (const warning of allWarnings) {
|
|
255
316
|
console.log(` - ${warning}`);
|
|
256
317
|
}
|
|
257
318
|
}
|
|
@@ -473,19 +534,15 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
473
534
|
|
|
474
535
|
// Update certificate manager with new routes
|
|
475
536
|
if (this.certManager) {
|
|
537
|
+
const existingAcmeOptions = this.certManager.getAcmeOptions();
|
|
476
538
|
await this.certManager.stop();
|
|
477
539
|
|
|
478
|
-
|
|
540
|
+
// Use the helper method to create and configure the certificate manager
|
|
541
|
+
this.certManager = await this.createCertificateManager(
|
|
479
542
|
newRoutes,
|
|
480
543
|
'./certs',
|
|
481
|
-
|
|
544
|
+
existingAcmeOptions
|
|
482
545
|
);
|
|
483
|
-
|
|
484
|
-
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
485
|
-
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
await this.certManager.initialize();
|
|
489
546
|
}
|
|
490
547
|
}
|
|
491
548
|
|
|
@@ -663,5 +720,76 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
663
720
|
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
|
664
721
|
return this.nftablesManager.getStatus();
|
|
665
722
|
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Validate ACME configuration
|
|
726
|
+
*/
|
|
727
|
+
private validateAcmeConfiguration(): string[] {
|
|
728
|
+
const warnings: string[] = [];
|
|
729
|
+
|
|
730
|
+
// Check for routes with certificate: 'auto'
|
|
731
|
+
const autoRoutes = this.settings.routes.filter(r =>
|
|
732
|
+
r.action.tls?.certificate === 'auto'
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
if (autoRoutes.length === 0) {
|
|
736
|
+
return warnings;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Check if we have ACME email configuration
|
|
740
|
+
const hasTopLevelEmail = this.settings.acme?.email;
|
|
741
|
+
const routesWithEmail = autoRoutes.filter(r => r.action.tls?.acme?.email);
|
|
742
|
+
|
|
743
|
+
if (!hasTopLevelEmail && routesWithEmail.length === 0) {
|
|
744
|
+
warnings.push(
|
|
745
|
+
'Routes with certificate: "auto" require ACME email configuration. ' +
|
|
746
|
+
'Add email to either top-level "acme" config or individual route\'s "tls.acme" config.'
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Check for port 80 availability for challenges
|
|
751
|
+
if (autoRoutes.length > 0) {
|
|
752
|
+
const challengePort = this.settings.acme?.port || 80;
|
|
753
|
+
const portsInUse = this.routeManager.getListeningPorts();
|
|
754
|
+
|
|
755
|
+
if (!portsInUse.includes(challengePort)) {
|
|
756
|
+
warnings.push(
|
|
757
|
+
`Port ${challengePort} is not configured for any routes but is needed for ACME challenges. ` +
|
|
758
|
+
`Add a route listening on port ${challengePort} or ensure it's accessible for HTTP-01 challenges.`
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Check for mismatched environments
|
|
764
|
+
if (this.settings.acme?.useProduction) {
|
|
765
|
+
const stagingRoutes = autoRoutes.filter(r =>
|
|
766
|
+
r.action.tls?.acme?.useProduction === false
|
|
767
|
+
);
|
|
768
|
+
if (stagingRoutes.length > 0) {
|
|
769
|
+
warnings.push(
|
|
770
|
+
'Top-level ACME uses production but some routes use staging. ' +
|
|
771
|
+
'Consider aligning environments to avoid certificate issues.'
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Check for wildcard domains with auto certificates
|
|
777
|
+
for (const route of autoRoutes) {
|
|
778
|
+
const domains = Array.isArray(route.match.domains)
|
|
779
|
+
? route.match.domains
|
|
780
|
+
: [route.match.domains];
|
|
781
|
+
|
|
782
|
+
const wildcardDomains = domains.filter(d => d?.includes('*'));
|
|
783
|
+
if (wildcardDomains.length > 0) {
|
|
784
|
+
warnings.push(
|
|
785
|
+
`Route "${route.name}" has wildcard domain(s) ${wildcardDomains.join(', ')} ` +
|
|
786
|
+
'with certificate: "auto". Wildcard certificates require DNS-01 challenges, ' +
|
|
787
|
+
'which are not currently supported. Use static certificates instead.'
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return warnings;
|
|
793
|
+
}
|
|
666
794
|
|
|
667
795
|
}
|