@push.rocks/smartproxy 19.0.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/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 +4 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +112 -25
- package/package.json +1 -2
- package/readme.hints.md +31 -1
- package/readme.md +69 -1
- package/readme.plan.md +109 -1434
- 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 +142 -24
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.2',
|
|
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
|
|
|
@@ -186,19 +192,55 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
186
192
|
return;
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
//
|
|
190
|
-
|
|
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
|
+
}
|
|
191
232
|
|
|
192
233
|
this.certManager = new SmartCertManager(
|
|
193
234
|
this.settings.routes,
|
|
194
|
-
'./certs',
|
|
195
|
-
|
|
196
|
-
email: defaultAcme.email,
|
|
197
|
-
useProduction: defaultAcme.useProduction,
|
|
198
|
-
port: defaultAcme.challengePort || 80
|
|
199
|
-
} : undefined
|
|
235
|
+
this.settings.acme?.certificateStore || './certs',
|
|
236
|
+
acmeOptions
|
|
200
237
|
);
|
|
201
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
|
+
|
|
202
244
|
// Connect with NetworkProxy
|
|
203
245
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
|
204
246
|
this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
|
|
@@ -249,9 +291,14 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
249
291
|
|
|
250
292
|
// Validate the route configuration
|
|
251
293
|
const configWarnings = this.routeManager.validateConfiguration();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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) {
|
|
255
302
|
console.log(` - ${warning}`);
|
|
256
303
|
}
|
|
257
304
|
}
|
|
@@ -663,5 +710,76 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
663
710
|
public async getNfTablesStatus(): Promise<Record<string, any>> {
|
|
664
711
|
return this.nftablesManager.getStatus();
|
|
665
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
|
+
}
|
|
666
784
|
|
|
667
785
|
}
|