@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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '19.0.0',
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('SmartAcme not initialized');
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
- console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
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
- const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
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: 80,
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
- port?: number;
8
- useProduction?: boolean;
9
- renewThresholdDays?: number;
10
- autoRenew?: boolean;
11
- certificateStore?: string;
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
- // ACME configuration options for SmartProxy
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
- // Set default ACME options if not provided
119
- this.settings.acme = this.settings.acme || {};
120
- if (Object.keys(this.settings.acme).length === 0) {
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: 'admin@example.com',
125
- useProduction: false,
126
- renewThresholdDays: 30,
127
- autoRenew: true,
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
- // Use the first auto route's ACME config as defaults
190
- const defaultAcme = autoRoutes[0]?.action.tls?.acme;
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', // Certificate directory
195
- defaultAcme ? {
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
- if (configWarnings.length > 0) {
253
- console.log("Route configuration warnings:");
254
- for (const warning of configWarnings) {
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
  }