@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.
Files changed (53) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/certificate/certificate-manager.d.ts +150 -0
  3. package/dist_ts/certificate/certificate-manager.js +505 -0
  4. package/dist_ts/certificate/events/simplified-events.d.ts +56 -0
  5. package/dist_ts/certificate/events/simplified-events.js +13 -0
  6. package/dist_ts/certificate/models/certificate-errors.d.ts +69 -0
  7. package/dist_ts/certificate/models/certificate-errors.js +141 -0
  8. package/dist_ts/certificate/models/certificate-strategy.d.ts +60 -0
  9. package/dist_ts/certificate/models/certificate-strategy.js +73 -0
  10. package/dist_ts/certificate/simplified-certificate-manager.d.ts +150 -0
  11. package/dist_ts/certificate/simplified-certificate-manager.js +501 -0
  12. package/dist_ts/http/index.d.ts +1 -9
  13. package/dist_ts/http/index.js +5 -11
  14. package/dist_ts/plugins.d.ts +3 -1
  15. package/dist_ts/plugins.js +4 -2
  16. package/dist_ts/proxies/network-proxy/network-proxy.js +3 -1
  17. package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.d.ts +48 -0
  18. package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.js +76 -0
  19. package/dist_ts/proxies/network-proxy/websocket-handler.js +41 -4
  20. package/dist_ts/proxies/smart-proxy/cert-store.d.ts +10 -0
  21. package/dist_ts/proxies/smart-proxy/cert-store.js +70 -0
  22. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +116 -0
  23. package/dist_ts/proxies/smart-proxy/certificate-manager.js +401 -0
  24. package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.d.ts +168 -0
  25. package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.js +642 -0
  26. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +26 -0
  27. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  28. package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.d.ts +65 -0
  29. package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.js +31 -0
  30. package/dist_ts/proxies/smart-proxy/models/smartproxy-options.d.ts +102 -0
  31. package/dist_ts/proxies/smart-proxy/models/smartproxy-options.js +73 -0
  32. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +10 -44
  33. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +66 -202
  34. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
  35. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +62 -2
  36. package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.d.ts +41 -0
  37. package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.js +132 -0
  38. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +18 -13
  39. package/dist_ts/proxies/smart-proxy/smart-proxy.js +79 -196
  40. package/package.json +7 -5
  41. package/readme.md +224 -10
  42. package/readme.plan.md +1405 -617
  43. package/ts/00_commitinfo_data.ts +1 -1
  44. package/ts/http/index.ts +5 -12
  45. package/ts/plugins.ts +4 -1
  46. package/ts/proxies/network-proxy/network-proxy.ts +3 -0
  47. package/ts/proxies/network-proxy/websocket-handler.ts +38 -3
  48. package/ts/proxies/smart-proxy/cert-store.ts +86 -0
  49. package/ts/proxies/smart-proxy/certificate-manager.ts +506 -0
  50. package/ts/proxies/smart-proxy/models/route-types.ts +33 -3
  51. package/ts/proxies/smart-proxy/network-proxy-bridge.ts +86 -239
  52. package/ts/proxies/smart-proxy/route-connection-handler.ts +74 -1
  53. package/ts/proxies/smart-proxy/smart-proxy.ts +105 -222
package/readme.plan.md CHANGED
@@ -1,654 +1,1442 @@
1
- # NFTables-SmartProxy Integration Plan
1
+ # ACME/Certificate Simplification Plan for SmartProxy
2
+
3
+ ## Command to reread CLAUDE.md
4
+ `reread /home/philkunz/.claude/CLAUDE.md`
2
5
 
3
6
  ## Overview
7
+ Simplify the ACME/Certificate system by consolidating components, removing unnecessary abstraction layers, and integrating directly into SmartProxy's route-based architecture.
4
8
 
5
- This document outlines a comprehensive plan to integrate the existing NFTables functionality with the SmartProxy core to provide advanced network-level routing capabilities. The NFTables proxy already exists in the codebase but is not fully integrated with the SmartProxy routing system. This integration will allow SmartProxy to leverage the power of Linux's NFTables firewall system for high-performance port forwarding, load balancing, and security filtering.
6
-
7
- ## Current State
8
-
9
- 1. **NFTablesProxy**: A standalone implementation exists in `ts/proxies/nftables-proxy/` with its own configuration and API.
10
- 2. **SmartProxy**: The main routing system with route-based configuration.
11
- 3. **No Integration**: Currently, these systems operate independently with no shared configuration or coordination.
12
-
13
- ## Goals
14
-
15
- 1. Create a unified configuration system where SmartProxy routes can specify NFTables-based forwarding.
16
- 2. Allow SmartProxy to dynamically provision and manage NFTables rules based on route configuration.
17
- 3. Support advanced filtering and security rules through NFTables for better performance.
18
- 4. Ensure backward compatibility with existing setups.
19
- 5. Provide metrics integration between the systems.
20
-
21
- ## Implementation Plan
22
-
23
- ### Phase 1: Route Configuration Schema Extension
24
-
25
- 1. **Extend Route Configuration Schema**:
26
- - Add new `forwardingEngine` option to IRouteAction to specify the forwarding implementation.
27
- - Support values: 'node' (current NodeJS implementation) and 'nftables' (Linux NFTables).
28
- - Add NFTables-specific configuration options to IRouteAction.
29
-
30
- 2. **Update Type Definitions**:
31
- ```typescript
32
- // In route-types.ts
33
- export interface IRouteAction {
34
- type: 'forward' | 'redirect' | 'block';
35
- target?: IRouteTarget;
36
- security?: IRouteSecurity;
37
- options?: IRouteOptions;
38
- tls?: IRouteTlsOptions;
39
- forwardingEngine?: 'node' | 'nftables'; // New field
40
- nftables?: INfTablesOptions; // New field
41
- }
42
-
43
- export interface INfTablesOptions {
44
- preserveSourceIP?: boolean;
45
- protocol?: 'tcp' | 'udp' | 'all';
46
- maxRate?: string; // QoS rate limiting
47
- priority?: number; // QoS priority
48
- tableName?: string; // Optional custom table name
49
- useIPSets?: boolean; // Use IP sets for performance
50
- useAdvancedNAT?: boolean; // Use connection tracking
51
- }
52
- ```
53
-
54
- ### Phase 2: NFTablesManager Implementation
55
-
56
- 1. **Create NFTablesManager Class**:
57
- - Create a new class to manage NFTables rules based on SmartProxy routes.
58
- - Add methods to create, update, and remove NFTables rules.
59
- - Design a rule naming scheme to track which rules correspond to which routes.
60
-
61
- 2. **Implementation**:
62
- ```typescript
63
- // In ts/proxies/smart-proxy/nftables-manager.ts
64
- export class NFTablesManager {
65
- private rulesMap: Map<string, NfTablesProxy> = new Map();
66
-
67
- constructor(private options: ISmartProxyOptions) {}
68
-
69
- /**
70
- * Provision NFTables rules for a route
71
- */
72
- public async provisionRoute(route: IRouteConfig): Promise<boolean> {
73
- // Generate a unique ID for this route
74
- const routeId = this.generateRouteId(route);
75
-
76
- // Skip if route doesn't use NFTables
77
- if (route.action.forwardingEngine !== 'nftables') {
78
- return true;
79
- }
80
-
81
- // Create NFTables options from route configuration
82
- const nftOptions = this.createNfTablesOptions(route);
83
-
84
- // Create and start an NFTablesProxy instance
85
- const proxy = new NfTablesProxy(nftOptions);
86
-
87
- try {
88
- await proxy.start();
89
- this.rulesMap.set(routeId, proxy);
90
- return true;
91
- } catch (err) {
92
- console.error(`Failed to provision NFTables rules for route ${route.name}: ${err.message}`);
93
- return false;
94
- }
95
- }
96
-
97
- /**
98
- * Remove NFTables rules for a route
99
- */
100
- public async deprovisionRoute(route: IRouteConfig): Promise<boolean> {
101
- const routeId = this.generateRouteId(route);
102
-
103
- const proxy = this.rulesMap.get(routeId);
104
- if (!proxy) {
105
- return true; // Nothing to remove
106
- }
107
-
108
- try {
109
- await proxy.stop();
110
- this.rulesMap.delete(routeId);
111
- return true;
112
- } catch (err) {
113
- console.error(`Failed to deprovision NFTables rules for route ${route.name}: ${err.message}`);
114
- return false;
115
- }
116
- }
117
-
118
- /**
119
- * Update NFTables rules when route changes
120
- */
121
- public async updateRoute(oldRoute: IRouteConfig, newRoute: IRouteConfig): Promise<boolean> {
122
- // Remove old rules and add new ones
123
- await this.deprovisionRoute(oldRoute);
124
- return this.provisionRoute(newRoute);
125
- }
126
-
127
- /**
128
- * Generate a unique ID for a route
129
- */
130
- private generateRouteId(route: IRouteConfig): string {
131
- // Generate a unique ID based on route properties
132
- return `${route.name || 'unnamed'}-${JSON.stringify(route.match)}-${Date.now()}`;
133
- }
134
-
135
- /**
136
- * Create NFTablesProxy options from a route configuration
137
- */
138
- private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
139
- const { action } = route;
140
-
141
- // Ensure we have a target
142
- if (!action.target) {
143
- throw new Error('Route must have a target to use NFTables forwarding');
144
- }
145
-
146
- // Convert port specifications
147
- const fromPorts = this.expandPortRange(route.match.ports);
148
-
149
- // Determine target port
150
- let toPorts;
151
- if (action.target.port === 'preserve') {
152
- // 'preserve' means use the same ports as the source
153
- toPorts = fromPorts;
154
- } else if (typeof action.target.port === 'function') {
155
- // For function-based ports, we can't determine at setup time
156
- // Use the "preserve" approach and let NFTables handle it
157
- toPorts = fromPorts;
158
- } else {
159
- toPorts = action.target.port;
160
- }
161
-
162
- // Create options
163
- const options: NfTableProxyOptions = {
164
- fromPort: fromPorts,
165
- toPort: toPorts,
166
- toHost: typeof action.target.host === 'function'
167
- ? 'localhost' // Can't determine at setup time, use localhost
168
- : (Array.isArray(action.target.host)
169
- ? action.target.host[0] // Use first host for now
170
- : action.target.host),
171
- protocol: action.nftables?.protocol || 'tcp',
172
- preserveSourceIP: action.nftables?.preserveSourceIP,
173
- useIPSets: action.nftables?.useIPSets !== false,
174
- useAdvancedNAT: action.nftables?.useAdvancedNAT,
175
- enableLogging: this.options.enableDetailedLogging,
176
- deleteOnExit: true,
177
- tableName: action.nftables?.tableName || 'smartproxy'
178
- };
179
-
180
- // Add security-related options
181
- if (action.security?.ipAllowList?.length) {
182
- options.allowedSourceIPs = action.security.ipAllowList;
183
- }
184
-
185
- if (action.security?.ipBlockList?.length) {
186
- options.bannedSourceIPs = action.security.ipBlockList;
187
- }
188
-
189
- // Add QoS options
190
- if (action.nftables?.maxRate || action.nftables?.priority) {
191
- options.qos = {
192
- enabled: true,
193
- maxRate: action.nftables.maxRate,
194
- priority: action.nftables.priority
195
- };
196
- }
197
-
198
- return options;
199
- }
200
-
201
- /**
202
- * Expand port range specifications
203
- */
204
- private expandPortRange(ports: TPortRange): number | PortRange | Array<number | PortRange> {
205
- // Use RouteManager's expandPortRange to convert to actual port numbers
206
- const routeManager = new RouteManager(this.options);
207
-
208
- // Process different port specifications
209
- if (typeof ports === 'number') {
210
- return ports;
211
- } else if (Array.isArray(ports)) {
212
- const result: Array<number | PortRange> = [];
213
-
214
- for (const item of ports) {
215
- if (typeof item === 'number') {
216
- result.push(item);
217
- } else if ('from' in item && 'to' in item) {
218
- result.push({ from: item.from, to: item.to });
219
- }
220
- }
221
-
222
- return result;
223
- } else if ('from' in ports && 'to' in ports) {
224
- return { from: ports.from, to: ports.to };
225
- }
226
-
227
- // Fallback
228
- return 80;
229
- }
230
-
231
- /**
232
- * Get status of all managed rules
233
- */
234
- public async getStatus(): Promise<Record<string, NfTablesStatus>> {
235
- const result: Record<string, NfTablesStatus> = {};
236
-
237
- for (const [routeId, proxy] of this.rulesMap.entries()) {
238
- result[routeId] = await proxy.getStatus();
239
- }
240
-
241
- return result;
242
- }
243
-
244
- /**
245
- * Stop all NFTables rules
246
- */
247
- public async stop(): Promise<void> {
248
- // Stop all NFTables proxies
249
- const stopPromises = Array.from(this.rulesMap.values()).map(proxy => proxy.stop());
250
- await Promise.all(stopPromises);
251
-
252
- this.rulesMap.clear();
253
- }
254
- }
255
- ```
9
+ ## Core Principles
10
+ 1. **No backward compatibility** - Clean break from legacy implementations
11
+ 2. **No migration helpers** - Users must update to new configuration format
12
+ 3. **Remove all legacy code** - Delete deprecated methods and interfaces
13
+ 4. **Forward-only approach** - Focus on simplicity over compatibility
14
+ 5. **No complexity for edge cases** - Only support the clean, new way
256
15
 
257
- ### Phase 3: SmartProxy Integration
16
+ ## Key Discoveries from Implementation Analysis
17
+
18
+ 1. **SmartProxy already supports static routes** - The 'static' type exists in TRouteActionType
19
+ 2. **Path-based routing works perfectly** - The route matching system handles paths with glob patterns
20
+ 3. **Dynamic route updates are safe** - SmartProxy's updateRoutes() method handles changes gracefully
21
+ 4. **Priority-based routing exists** - Routes are sorted by priority, ensuring ACME routes match first
22
+ 5. **No separate HTTP server needed** - ACME challenges can be regular SmartProxy routes
23
+
24
+ ## Current State Analysis
258
25
 
259
- 1. **Extend SmartProxy Class**:
260
- - Add NFTablesManager as a property of SmartProxy.
261
- - Hook into route configuration to provision NFTables rules.
262
- - Add methods to manage NFTables functionality.
263
-
264
- 2. **Implementation**:
265
- ```typescript
266
- // In ts/proxies/smart-proxy/smart-proxy.ts
267
- import { NFTablesManager } from './nftables-manager.js';
268
-
269
- export class SmartProxy {
270
- // Existing properties
271
- private nftablesManager: NFTablesManager;
272
-
273
- constructor(options: ISmartProxyOptions) {
274
- // Existing initialization
275
-
276
- // Initialize NFTablesManager
277
- this.nftablesManager = new NFTablesManager(options);
278
- }
279
-
280
- /**
281
- * Start the SmartProxy server
282
- */
283
- public async start(): Promise<void> {
284
- // Existing initialization
285
-
286
- // If we have routes, provision NFTables rules for them
287
- for (const route of this.settings.routes) {
288
- if (route.action.forwardingEngine === 'nftables') {
289
- await this.nftablesManager.provisionRoute(route);
290
- }
291
- }
292
-
293
- // Rest of existing start method
294
- }
295
-
296
- /**
297
- * Stop the SmartProxy server
298
- */
299
- public async stop(): Promise<void> {
300
- // Stop NFTablesManager first
301
- await this.nftablesManager.stop();
302
-
303
- // Rest of existing stop method
304
- }
305
-
306
- /**
307
- * Update routes
308
- */
309
- public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
310
- // Get existing routes that use NFTables
311
- const oldNfTablesRoutes = this.settings.routes.filter(
312
- r => r.action.forwardingEngine === 'nftables'
313
- );
314
-
315
- // Get new routes that use NFTables
316
- const newNfTablesRoutes = routes.filter(
317
- r => r.action.forwardingEngine === 'nftables'
318
- );
319
-
320
- // Find routes to remove, update, or add
321
- for (const oldRoute of oldNfTablesRoutes) {
322
- const newRoute = newNfTablesRoutes.find(r => r.name === oldRoute.name);
323
-
324
- if (!newRoute) {
325
- // Route was removed
326
- await this.nftablesManager.deprovisionRoute(oldRoute);
327
- } else {
328
- // Route was updated
329
- await this.nftablesManager.updateRoute(oldRoute, newRoute);
330
- }
331
- }
332
-
333
- // Find new routes to add
334
- for (const newRoute of newNfTablesRoutes) {
335
- const oldRoute = oldNfTablesRoutes.find(r => r.name === newRoute.name);
336
-
337
- if (!oldRoute) {
338
- // New route
339
- await this.nftablesManager.provisionRoute(newRoute);
340
- }
341
- }
342
-
343
- // Update settings with the new routes
344
- this.settings.routes = routes;
345
-
346
- // Update route manager with new routes
347
- this.routeManager.updateRoutes(routes);
348
- }
349
-
350
- /**
351
- * Get NFTables status
352
- */
353
- public async getNfTablesStatus(): Promise<Record<string, NfTablesStatus>> {
354
- return this.nftablesManager.getStatus();
355
- }
356
- }
357
- ```
358
-
359
- ### Phase 4: Routing System Integration
360
-
361
- 1. **Extend the Route-Connection-Handler**:
362
- - Modify to check if a route uses NFTables.
363
- - Skip Node.js-based connection handling for NFTables routes.
364
-
365
- 2. **Implementation**:
366
- ```typescript
367
- // In ts/proxies/smart-proxy/route-connection-handler.ts
368
- export class RouteConnectionHandler {
369
- // Existing methods
370
-
371
- /**
372
- * Route the connection based on match criteria
373
- */
374
- private routeConnection(
375
- socket: plugins.net.Socket,
376
- record: IConnectionRecord,
377
- serverName: string,
378
- initialChunk?: Buffer
379
- ): void {
380
- // Find matching route
381
- const routeMatch = this.routeManager.findMatchingRoute({
382
- port: record.localPort,
383
- domain: serverName,
384
- clientIp: record.remoteIP,
385
- path: undefined,
386
- tlsVersion: undefined
387
- });
388
-
389
- if (!routeMatch) {
390
- // Existing code for no matching route
391
- return;
392
- }
393
-
394
- const route = routeMatch.route;
395
-
396
- // Check if this route uses NFTables for forwarding
397
- if (route.action.forwardingEngine === 'nftables') {
398
- // For NFTables routes, we don't need to do anything at the application level
399
- // The packet is forwarded at the kernel level
400
-
401
- // Log the connection
402
- console.log(
403
- `[${record.id}] Connection forwarded by NFTables: ${record.remoteIP} -> port ${record.localPort}`
404
- );
405
-
406
- // Just close the socket in our application since it's handled at kernel level
407
- socket.end();
408
- this.connectionManager.initiateCleanupOnce(record, 'nftables_handled');
409
- return;
410
- }
411
-
412
- // Existing code for handling the route
413
- }
414
- }
415
- ```
416
-
417
- ### Phase 5: CLI and Configuration Helpers
418
-
419
- 1. **Add Helper Functions**:
420
- - Create helper functions for easy route creation with NFTables.
421
- - Update the route-helpers.ts utility file.
422
-
423
- 2. **Implementation**:
424
- ```typescript
425
- // In ts/proxies/smart-proxy/utils/route-helpers.ts
426
-
427
- /**
428
- * Create an NFTables-based route
429
- */
430
- export function createNfTablesRoute(
431
- nameOrDomains: string | string[],
432
- target: { host: string; port: number | 'preserve' },
433
- options: {
434
- ports?: TPortRange;
435
- protocol?: 'tcp' | 'udp' | 'all';
436
- preserveSourceIP?: boolean;
437
- allowedIps?: string[];
438
- maxRate?: string;
439
- priority?: number;
440
- useTls?: boolean;
441
- } = {}
442
- ): IRouteConfig {
443
- // Determine if this is a name or domain
444
- let name: string;
445
- let domains: string | string[];
446
-
447
- if (Array.isArray(nameOrDomains) || nameOrDomains.includes('.')) {
448
- domains = nameOrDomains;
449
- name = Array.isArray(nameOrDomains) ? nameOrDomains[0] : nameOrDomains;
450
- } else {
451
- name = nameOrDomains;
452
- domains = []; // No domains
453
- }
454
-
455
- const route: IRouteConfig = {
456
- name,
457
- match: {
458
- domains,
459
- ports: options.ports || 80
460
- },
461
- action: {
462
- type: 'forward',
463
- target: {
464
- host: target.host,
465
- port: target.port
466
- },
467
- forwardingEngine: 'nftables',
468
- nftables: {
469
- protocol: options.protocol || 'tcp',
470
- preserveSourceIP: options.preserveSourceIP,
471
- maxRate: options.maxRate,
472
- priority: options.priority
473
- }
474
- }
475
- };
476
-
477
- // Add security if allowed IPs are specified
478
- if (options.allowedIps?.length) {
479
- route.action.security = {
480
- ipAllowList: options.allowedIps
481
- };
482
- }
483
-
484
- // Add TLS options if needed
485
- if (options.useTls) {
486
- route.action.tls = {
487
- mode: 'passthrough'
488
- };
489
- }
490
-
491
- return route;
492
- }
493
-
494
- /**
495
- * Create an NFTables-based TLS termination route
496
- */
497
- export function createNfTablesTerminateRoute(
498
- nameOrDomains: string | string[],
499
- target: { host: string; port: number | 'preserve' },
500
- options: {
501
- ports?: TPortRange;
502
- protocol?: 'tcp' | 'udp' | 'all';
503
- preserveSourceIP?: boolean;
504
- allowedIps?: string[];
505
- maxRate?: string;
506
- priority?: number;
507
- certificate?: string | { cert: string; key: string };
508
- } = {}
509
- ): IRouteConfig {
510
- const route = createNfTablesRoute(
511
- nameOrDomains,
512
- target,
513
- {
514
- ...options,
515
- ports: options.ports || 443,
516
- useTls: false
517
- }
518
- );
519
-
520
- // Set TLS termination
521
- route.action.tls = {
522
- mode: 'terminate',
523
- certificate: options.certificate || 'auto'
524
- };
525
-
526
- return route;
527
- }
528
- ```
529
-
530
- ### Phase 6: Documentation and Testing
531
-
532
- 1. **Update Documentation**:
533
- - Add NFTables integration documentation to README and API docs.
534
- - Document the implementation and use cases.
535
-
536
- 2. **Test Cases**:
537
- - Create test cases for NFTables-based routing.
538
- - Test performance comparison with Node.js-based forwarding.
539
- - Test security features with IP allowlists/blocklists.
26
+ ### Files to be Removed/Replaced
27
+ ```
28
+ ts/certificate/ (ENTIRE DIRECTORY TO BE REMOVED)
29
+ ├── acme/
30
+ │ ├── acme-factory.ts (28 lines)
31
+ │ ├── challenge-handler.ts (227 lines)
32
+ └── index.ts (2 lines)
33
+ ├── events/
34
+ └── certificate-events.ts (75 lines)
35
+ ├── models/
36
+ └── certificate-types.ts (168 lines)
37
+ ├── providers/
38
+ │ ├── cert-provisioner.ts (547 lines)
39
+ │ └── index.ts (2 lines)
40
+ ├── storage/
41
+ │ ├── file-storage.ts (134 lines)
42
+ │ └── index.ts (2 lines)
43
+ ├── utils/
44
+ │ └── certificate-helpers.ts (166 lines)
45
+ └── index.ts (75 lines)
46
+
47
+ ts/http/port80/ (ENTIRE SUBDIRECTORY TO BE REMOVED)
48
+ ├── acme-interfaces.ts
49
+ ├── challenge-responder.ts
50
+ ├── port80-handler.ts
51
+ └── index.ts
540
52
 
53
+ ts/http/ (KEEP OTHER SUBDIRECTORIES)
54
+ ├── index.ts (UPDATE to remove port80 exports)
55
+ ├── models/ (KEEP)
56
+ ├── redirects/ (KEEP)
57
+ ├── router/ (KEEP)
58
+ └── utils/ (KEEP)
59
+
60
+ ts/proxies/smart-proxy/
61
+ └── network-proxy-bridge.ts (267 lines - to be simplified)
62
+ ```
63
+
64
+ ### Current Dependencies
65
+ - @push.rocks/smartacme (ACME client)
66
+ - @push.rocks/smartfile (file operations)
67
+ - @push.rocks/smartcrypto (certificate operations)
68
+ - @push.rocks/smartexpress (HTTP server for challenges)
69
+
70
+ ## Detailed Implementation Plan
71
+
72
+ ### Phase 1: Create SmartCertManager
73
+
74
+ #### 1.1 Create certificate-manager.ts
541
75
  ```typescript
542
- // In test/test.nftables-integration.ts
543
- import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
544
- import { createNfTablesRoute } from '../ts/proxies/smart-proxy/utils/route-helpers.js';
545
- import { expect, tap } from '@push.rocks/tapbundle';
546
- import * as net from 'net';
76
+ // ts/proxies/smart-proxy/certificate-manager.ts
77
+ import * as plugins from '../../plugins.js';
78
+ import { NetworkProxy } from '../network-proxy/index.js';
79
+ import type { IRouteConfig, IRouteTls } from './models/route-types.js';
80
+ import { CertStore } from './cert-store.js';
81
+ import { AcmeClient } from './acme-client.js';
547
82
 
548
- // Test server and client utilities
549
- let testServer: net.Server;
550
- let smartProxy: SmartProxy;
83
+ export interface ICertStatus {
84
+ domain: string;
85
+ status: 'valid' | 'pending' | 'expired' | 'error';
86
+ expiryDate?: Date;
87
+ issueDate?: Date;
88
+ source: 'static' | 'acme';
89
+ error?: string;
90
+ }
551
91
 
552
- const TEST_PORT = 4000;
553
- const PROXY_PORT = 5000;
554
- const TEST_DATA = 'Hello through NFTables!';
92
+ export interface ICertificateData {
93
+ cert: string;
94
+ key: string;
95
+ ca?: string;
96
+ expiryDate: Date;
97
+ issueDate: Date;
98
+ }
555
99
 
556
- tap.test('setup NFTables integration test environment', async () => {
557
- // Create a test TCP server
558
- testServer = net.createServer((socket) => {
559
- socket.on('data', (data) => {
560
- socket.write(`Server says: ${data.toString()}`);
561
- });
562
- });
100
+ export class SmartCertManager {
101
+ private certStore: CertStore;
102
+ private smartAcme: plugins.smartacme.SmartAcme | null = null;
103
+ private networkProxy: NetworkProxy | null = null;
104
+ private renewalTimer: NodeJS.Timer | null = null;
105
+ private pendingChallenges: Map<string, string> = new Map();
106
+
107
+ // Track certificate status by route name
108
+ private certStatus: Map<string, ICertStatus> = new Map();
109
+
110
+ // Callback to update SmartProxy routes for challenges
111
+ private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
112
+
113
+ constructor(
114
+ private routes: IRouteConfig[],
115
+ private certDir: string = './certs',
116
+ private acmeOptions?: {
117
+ email?: string;
118
+ useProduction?: boolean;
119
+ port?: number;
120
+ }
121
+ ) {
122
+ this.certStore = new CertStore(certDir);
123
+ }
124
+
125
+ public setNetworkProxy(networkProxy: NetworkProxy): void {
126
+ this.networkProxy = networkProxy;
127
+ }
128
+
129
+ /**
130
+ * Set callback for updating routes (used for challenge routes)
131
+ */
132
+ public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
133
+ this.updateRoutesCallback = callback;
134
+ }
135
+
136
+ /**
137
+ * Initialize certificate manager and provision certificates for all routes
138
+ */
139
+ public async initialize(): Promise<void> {
140
+ // Create certificate directory if it doesn't exist
141
+ await this.certStore.initialize();
142
+
143
+ // Initialize SmartAcme if we have any ACME routes
144
+ const hasAcmeRoutes = this.routes.some(r =>
145
+ r.action.tls?.certificate === 'auto'
146
+ );
147
+
148
+ if (hasAcmeRoutes && this.acmeOptions?.email) {
149
+ // Create SmartAcme instance with our challenge handler
150
+ this.smartAcme = new plugins.smartacme.SmartAcme({
151
+ accountEmail: this.acmeOptions.email,
152
+ environment: this.acmeOptions.useProduction ? 'production' : 'staging',
153
+ certManager: new InMemoryCertManager(), // Simple in-memory cert manager
154
+ challengeHandlers: [{
155
+ type: 'http-01',
156
+ setChallenge: async (domain: string, token: string, keyAuth: string) => {
157
+ await this.handleChallenge(token, keyAuth);
158
+ },
159
+ removeChallenge: async (domain: string, token: string) => {
160
+ await this.cleanupChallenge(token);
161
+ }
162
+ }]
163
+ });
164
+
165
+ await this.smartAcme.start();
166
+ }
167
+
168
+ // Provision certificates for all routes
169
+ await this.provisionAllCertificates();
170
+
171
+ // Start renewal timer
172
+ this.startRenewalTimer();
173
+ }
174
+
175
+ /**
176
+ * Provision certificates for all routes that need them
177
+ */
178
+ private async provisionAllCertificates(): Promise<void> {
179
+ const certRoutes = this.routes.filter(r =>
180
+ r.action.tls?.mode === 'terminate' ||
181
+ r.action.tls?.mode === 'terminate-and-reencrypt'
182
+ );
183
+
184
+ for (const route of certRoutes) {
185
+ try {
186
+ await this.provisionCertificate(route);
187
+ } catch (error) {
188
+ console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Provision certificate for a single route
195
+ */
196
+ public async provisionCertificate(route: IRouteConfig): Promise<void> {
197
+ const tls = route.action.tls;
198
+ if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
199
+ return;
200
+ }
201
+
202
+ const domains = this.extractDomainsFromRoute(route);
203
+ if (domains.length === 0) {
204
+ console.warn(`Route ${route.name} has TLS termination but no domains`);
205
+ return;
206
+ }
207
+
208
+ const primaryDomain = domains[0];
209
+
210
+ if (tls.certificate === 'auto') {
211
+ // ACME certificate
212
+ await this.provisionAcmeCertificate(route, domains);
213
+ } else if (typeof tls.certificate === 'object') {
214
+ // Static certificate
215
+ await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Provision ACME certificate
221
+ */
222
+ private async provisionAcmeCertificate(
223
+ route: IRouteConfig,
224
+ domains: string[]
225
+ ): Promise<void> {
226
+ if (!this.smartAcme) {
227
+ throw new Error('SmartAcme not initialized');
228
+ }
229
+
230
+ const primaryDomain = domains[0];
231
+ const routeName = route.name || primaryDomain;
232
+
233
+ // Check if we already have a valid certificate
234
+ const existingCert = await this.certStore.getCertificate(routeName);
235
+ if (existingCert && this.isCertificateValid(existingCert)) {
236
+ console.log(`Using existing valid certificate for ${primaryDomain}`);
237
+ await this.applyCertificate(primaryDomain, existingCert);
238
+ this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
239
+ return;
240
+ }
241
+
242
+ console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
243
+ this.updateCertStatus(routeName, 'pending', 'acme');
244
+
245
+ try {
246
+ // Use smartacme to get certificate
247
+ const cert = await this.smartAcme.getCertificateForDomain(primaryDomain, {
248
+ altNames: domains.slice(1)
249
+ });
250
+
251
+ // smartacme returns a Cert object with these properties
252
+ const certData: ICertificateData = {
253
+ cert: cert.cert,
254
+ key: cert.privateKey,
255
+ ca: cert.fullChain || cert.cert, // Use fullChain if available
256
+ expiryDate: new Date(cert.validTo),
257
+ issueDate: new Date(cert.validFrom)
258
+ };
259
+
260
+ await this.certStore.saveCertificate(routeName, certData);
261
+ await this.applyCertificate(primaryDomain, certData);
262
+ this.updateCertStatus(routeName, 'valid', 'acme', certData);
263
+
264
+ console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
265
+ } catch (error) {
266
+ console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
267
+ this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
268
+ throw error;
269
+ }
270
+ }
563
271
 
564
- await new Promise<void>((resolve) => {
565
- testServer.listen(TEST_PORT, () => {
566
- console.log(`Test server listening on port ${TEST_PORT}`);
567
- resolve();
272
+ /**
273
+ * Provision static certificate
274
+ */
275
+ private async provisionStaticCertificate(
276
+ route: IRouteConfig,
277
+ domain: string,
278
+ certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
279
+ ): Promise<void> {
280
+ const routeName = route.name || domain;
281
+
282
+ try {
283
+ let key: string = certConfig.key;
284
+ let cert: string = certConfig.cert;
285
+
286
+ // Load from files if paths are provided
287
+ if (certConfig.keyFile) {
288
+ key = await plugins.smartfile.fs.readFileAsString(certConfig.keyFile);
289
+ }
290
+ if (certConfig.certFile) {
291
+ cert = await plugins.smartfile.fs.readFileAsString(certConfig.certFile);
292
+ }
293
+
294
+ // Parse certificate to get dates
295
+ const certInfo = await plugins.smartcrypto.cert.parseCert(cert);
296
+
297
+ const certData: ICertificateData = {
298
+ cert,
299
+ key,
300
+ expiryDate: certInfo.validTo,
301
+ issueDate: certInfo.validFrom
302
+ };
303
+
304
+ // Save to store for consistency
305
+ await this.certStore.saveCertificate(routeName, certData);
306
+ await this.applyCertificate(domain, certData);
307
+ this.updateCertStatus(routeName, 'valid', 'static', certData);
308
+
309
+ console.log(`Successfully loaded static certificate for ${domain}`);
310
+ } catch (error) {
311
+ console.error(`Failed to provision static certificate for ${domain}: ${error}`);
312
+ this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
313
+ throw error;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Apply certificate to NetworkProxy
319
+ */
320
+ private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
321
+ if (!this.networkProxy) {
322
+ console.warn('NetworkProxy not set, cannot apply certificate');
323
+ return;
324
+ }
325
+
326
+ // Apply certificate to NetworkProxy
327
+ this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
328
+
329
+ // Also apply for wildcard if it's a subdomain
330
+ if (domain.includes('.') && !domain.startsWith('*.')) {
331
+ const parts = domain.split('.');
332
+ if (parts.length >= 2) {
333
+ const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
334
+ this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
335
+ }
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Extract domains from route configuration
341
+ */
342
+ private extractDomainsFromRoute(route: IRouteConfig): string[] {
343
+ if (!route.match.domains) {
344
+ return [];
345
+ }
346
+
347
+ const domains = Array.isArray(route.match.domains)
348
+ ? route.match.domains
349
+ : [route.match.domains];
350
+
351
+ // Filter out wildcards and patterns
352
+ return domains.filter(d =>
353
+ !d.includes('*') &&
354
+ !d.includes('{') &&
355
+ d.includes('.')
356
+ );
357
+ }
358
+
359
+ /**
360
+ * Check if certificate is valid
361
+ */
362
+ private isCertificateValid(cert: ICertificateData): boolean {
363
+ const now = new Date();
364
+ const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
365
+
366
+ return cert.expiryDate > expiryThreshold;
367
+ }
368
+
369
+ /**
370
+ * Create ACME challenge route
371
+ * NOTE: SmartProxy already handles path-based routing and priority
372
+ */
373
+ private createChallengeRoute(): IRouteConfig {
374
+ return {
375
+ name: 'acme-challenge',
376
+ priority: 1000, // High priority to ensure it's checked first
377
+ match: {
378
+ ports: 80,
379
+ path: '/.well-known/acme-challenge/*'
380
+ },
381
+ action: {
382
+ type: 'static',
383
+ handler: async (context) => {
384
+ const token = context.path?.split('/').pop();
385
+ const keyAuth = token ? this.pendingChallenges.get(token) : undefined;
386
+
387
+ if (keyAuth) {
388
+ return {
389
+ status: 200,
390
+ headers: { 'Content-Type': 'text/plain' },
391
+ body: keyAuth
392
+ };
393
+ } else {
394
+ return {
395
+ status: 404,
396
+ body: 'Not found'
397
+ };
398
+ }
399
+ }
400
+ }
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Add challenge route to SmartProxy
406
+ */
407
+ private async addChallengeRoute(): Promise<void> {
408
+ if (!this.updateRoutesCallback) {
409
+ throw new Error('No route update callback set');
410
+ }
411
+
412
+ const challengeRoute = this.createChallengeRoute();
413
+ const updatedRoutes = [...this.routes, challengeRoute];
414
+
415
+ await this.updateRoutesCallback(updatedRoutes);
416
+ }
417
+
418
+ /**
419
+ * Remove challenge route from SmartProxy
420
+ */
421
+ private async removeChallengeRoute(): Promise<void> {
422
+ if (!this.updateRoutesCallback) {
423
+ return;
424
+ }
425
+
426
+ const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
427
+ await this.updateRoutesCallback(filteredRoutes);
428
+ }
429
+
430
+ /**
431
+ * Start renewal timer
432
+ */
433
+ private startRenewalTimer(): void {
434
+ // Check for renewals every 12 hours
435
+ this.renewalTimer = setInterval(() => {
436
+ this.checkAndRenewCertificates();
437
+ }, 12 * 60 * 60 * 1000);
438
+
439
+ // Also do an immediate check
440
+ this.checkAndRenewCertificates();
441
+ }
442
+
443
+ /**
444
+ * Check and renew certificates that are expiring
445
+ */
446
+ private async checkAndRenewCertificates(): Promise<void> {
447
+ for (const route of this.routes) {
448
+ if (route.action.tls?.certificate === 'auto') {
449
+ const routeName = route.name || this.extractDomainsFromRoute(route)[0];
450
+ const cert = await this.certStore.getCertificate(routeName);
451
+
452
+ if (cert && !this.isCertificateValid(cert)) {
453
+ console.log(`Certificate for ${routeName} needs renewal`);
454
+ try {
455
+ await this.provisionCertificate(route);
456
+ } catch (error) {
457
+ console.error(`Failed to renew certificate for ${routeName}: ${error}`);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Update certificate status
466
+ */
467
+ private updateCertStatus(
468
+ routeName: string,
469
+ status: ICertStatus['status'],
470
+ source: ICertStatus['source'],
471
+ certData?: ICertificateData,
472
+ error?: string
473
+ ): void {
474
+ this.certStatus.set(routeName, {
475
+ domain: routeName,
476
+ status,
477
+ source,
478
+ expiryDate: certData?.expiryDate,
479
+ issueDate: certData?.issueDate,
480
+ error
568
481
  });
569
- });
482
+ }
570
483
 
571
- // Create SmartProxy with NFTables route
572
- smartProxy = new SmartProxy({
573
- routes: [
574
- createNfTablesRoute('test-nftables', {
575
- host: 'localhost',
576
- port: TEST_PORT
577
- }, {
578
- ports: PROXY_PORT,
579
- protocol: 'tcp'
580
- })
581
- ]
582
- });
484
+ /**
485
+ * Get certificate status for a route
486
+ */
487
+ public getCertificateStatus(routeName: string): ICertStatus | undefined {
488
+ return this.certStatus.get(routeName);
489
+ }
583
490
 
584
- // Start the proxy
585
- await smartProxy.start();
586
- });
491
+ /**
492
+ * Force renewal of a certificate
493
+ */
494
+ public async renewCertificate(routeName: string): Promise<void> {
495
+ const route = this.routes.find(r => r.name === routeName);
496
+ if (!route) {
497
+ throw new Error(`Route ${routeName} not found`);
498
+ }
499
+
500
+ // Remove existing certificate to force renewal
501
+ await this.certStore.deleteCertificate(routeName);
502
+ await this.provisionCertificate(route);
503
+ }
504
+
505
+ /**
506
+ * Handle ACME challenge
507
+ */
508
+ private async handleChallenge(token: string, keyAuth: string): Promise<void> {
509
+ this.pendingChallenges.set(token, keyAuth);
510
+
511
+ // Add challenge route if it's the first challenge
512
+ if (this.pendingChallenges.size === 1) {
513
+ await this.addChallengeRoute();
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Cleanup ACME challenge
519
+ */
520
+ private async cleanupChallenge(token: string): Promise<void> {
521
+ this.pendingChallenges.delete(token);
522
+
523
+ // Remove challenge route if no more challenges
524
+ if (this.pendingChallenges.size === 0) {
525
+ await this.removeChallengeRoute();
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Stop certificate manager
531
+ */
532
+ public async stop(): Promise<void> {
533
+ if (this.renewalTimer) {
534
+ clearInterval(this.renewalTimer);
535
+ this.renewalTimer = null;
536
+ }
537
+
538
+ if (this.smartAcme) {
539
+ await this.smartAcme.stop();
540
+ }
541
+
542
+ // Remove any active challenge routes
543
+ if (this.pendingChallenges.size > 0) {
544
+ this.pendingChallenges.clear();
545
+ await this.removeChallengeRoute();
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Get ACME options (for recreating after route updates)
551
+ */
552
+ public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
553
+ return this.acmeOptions;
554
+ }
555
+ }
587
556
 
588
- tap.test('should forward TCP connections through NFTables', async () => {
589
- // Connect to the proxy port
590
- const client = new net.Socket();
557
+ /**
558
+ * Simple in-memory certificate manager for SmartAcme
559
+ * We only use this to satisfy SmartAcme's interface - actual storage is handled by CertStore
560
+ */
561
+ class InMemoryCertManager implements plugins.smartacme.CertManager {
562
+ private store = new Map<string, any>();
563
+
564
+ public async getCert(domain: string): Promise<any> {
565
+ // SmartAcme uses this to check for existing certs
566
+ // We return null to force it to always request new certs
567
+ return null;
568
+ }
569
+
570
+ public async setCert(domain: string, certificate: any): Promise<void> {
571
+ // SmartAcme calls this after getting a cert
572
+ // We ignore it since we handle storage ourselves
573
+ }
591
574
 
592
- const response = await new Promise<string>((resolve, reject) => {
593
- let responseData = '';
575
+ public async removeCert(domain: string): Promise<void> {
576
+ // Not needed for our use case
577
+ }
578
+ }
579
+ ```
580
+
581
+ #### 1.2 Create cert-store.ts
582
+ ```typescript
583
+ // ts/proxies/smart-proxy/cert-store.ts
584
+ import * as plugins from '../../plugins.js';
585
+ import type { ICertificateData } from './certificate-manager.js';
586
+
587
+ export class CertStore {
588
+ constructor(private certDir: string) {}
589
+
590
+ public async initialize(): Promise<void> {
591
+ await plugins.smartfile.fs.ensureDirectory(this.certDir);
592
+ }
593
+
594
+ public async getCertificate(routeName: string): Promise<ICertificateData | null> {
595
+ const certPath = this.getCertPath(routeName);
596
+ const metaPath = `${certPath}/meta.json`;
594
597
 
595
- client.connect(PROXY_PORT, 'localhost', () => {
596
- client.write(TEST_DATA);
597
- });
598
+ if (!await plugins.smartfile.fs.fileExists(metaPath)) {
599
+ return null;
600
+ }
598
601
 
599
- client.on('data', (data) => {
600
- responseData += data.toString();
601
- client.end();
602
- });
602
+ try {
603
+ const meta = await plugins.smartfile.fs.readJson(metaPath);
604
+ const cert = await plugins.smartfile.fs.readFileAsString(`${certPath}/cert.pem`);
605
+ const key = await plugins.smartfile.fs.readFileAsString(`${certPath}/key.pem`);
606
+
607
+ let ca: string | undefined;
608
+ const caPath = `${certPath}/ca.pem`;
609
+ if (await plugins.smartfile.fs.fileExists(caPath)) {
610
+ ca = await plugins.smartfile.fs.readFileAsString(caPath);
611
+ }
612
+
613
+ return {
614
+ cert,
615
+ key,
616
+ ca,
617
+ expiryDate: new Date(meta.expiryDate),
618
+ issueDate: new Date(meta.issueDate)
619
+ };
620
+ } catch (error) {
621
+ console.error(`Failed to load certificate for ${routeName}: ${error}`);
622
+ return null;
623
+ }
624
+ }
625
+
626
+ public async saveCertificate(
627
+ routeName: string,
628
+ certData: ICertificateData
629
+ ): Promise<void> {
630
+ const certPath = this.getCertPath(routeName);
631
+ await plugins.smartfile.fs.ensureDirectory(certPath);
632
+
633
+ // Save certificate files
634
+ await plugins.smartfile.fs.writeFileAsString(
635
+ `${certPath}/cert.pem`,
636
+ certData.cert
637
+ );
638
+ await plugins.smartfile.fs.writeFileAsString(
639
+ `${certPath}/key.pem`,
640
+ certData.key
641
+ );
642
+
643
+ if (certData.ca) {
644
+ await plugins.smartfile.fs.writeFileAsString(
645
+ `${certPath}/ca.pem`,
646
+ certData.ca
647
+ );
648
+ }
649
+
650
+ // Save metadata
651
+ const meta = {
652
+ expiryDate: certData.expiryDate.toISOString(),
653
+ issueDate: certData.issueDate.toISOString(),
654
+ savedAt: new Date().toISOString()
655
+ };
656
+
657
+ await plugins.smartfile.fs.writeJson(`${certPath}/meta.json`, meta);
658
+ }
659
+
660
+ public async deleteCertificate(routeName: string): Promise<void> {
661
+ const certPath = this.getCertPath(routeName);
662
+ if (await plugins.smartfile.fs.fileExists(certPath)) {
663
+ await plugins.smartfile.fs.removeDirectory(certPath);
664
+ }
665
+ }
666
+
667
+ private getCertPath(routeName: string): string {
668
+ // Sanitize route name for filesystem
669
+ const safeName = routeName.replace(/[^a-zA-Z0-9-_]/g, '_');
670
+ return `${this.certDir}/${safeName}`;
671
+ }
672
+ }
673
+ ```
674
+
675
+
676
+ ### Phase 2: Update Route Types and Handler
677
+
678
+ #### 2.1 Update route-types.ts
679
+ ```typescript
680
+ // Add to ts/proxies/smart-proxy/models/route-types.ts
681
+
682
+ /**
683
+ * ACME configuration for automatic certificate provisioning
684
+ */
685
+ export interface IRouteAcme {
686
+ email: string; // Contact email for ACME account
687
+ useProduction?: boolean; // Use production ACME servers (default: false)
688
+ challengePort?: number; // Port for HTTP-01 challenges (default: 80)
689
+ renewBeforeDays?: number; // Days before expiry to renew (default: 30)
690
+ }
691
+
692
+ /**
693
+ * Static route handler response
694
+ */
695
+ export interface IStaticResponse {
696
+ status: number;
697
+ headers?: Record<string, string>;
698
+ body: string | Buffer;
699
+ }
700
+
701
+ /**
702
+ * Update IRouteAction to support static handlers
703
+ * NOTE: The 'static' type already exists in TRouteActionType
704
+ */
705
+ export interface IRouteAction {
706
+ type: TRouteActionType;
707
+ target?: IRouteTarget;
708
+ security?: IRouteSecurity;
709
+ options?: IRouteOptions;
710
+ tls?: IRouteTls;
711
+ redirect?: IRouteRedirect;
712
+ handler?: (context: IRouteContext) => Promise<IStaticResponse>; // For static routes
713
+ }
714
+
715
+ /**
716
+ * Extend IRouteConfig to ensure challenge routes have higher priority
717
+ */
718
+ export interface IRouteConfig {
719
+ name?: string;
720
+ match: IRouteMatch;
721
+ action: IRouteAction;
722
+ priority?: number; // Already exists - ACME routes should use high priority
723
+ }
724
+
725
+ /**
726
+ * Extended TLS configuration for route actions
727
+ */
728
+ export interface IRouteTls {
729
+ mode: TTlsMode;
730
+ certificate?: 'auto' | { // Auto = use ACME
731
+ key: string; // PEM-encoded private key
732
+ cert: string; // PEM-encoded certificate
733
+ ca?: string; // PEM-encoded CA chain
734
+ keyFile?: string; // Path to key file (overrides key)
735
+ certFile?: string; // Path to cert file (overrides cert)
736
+ };
737
+ acme?: IRouteAcme; // ACME options when certificate is 'auto'
738
+ versions?: string[]; // Allowed TLS versions (e.g., ['TLSv1.2', 'TLSv1.3'])
739
+ ciphers?: string; // OpenSSL cipher string
740
+ honorCipherOrder?: boolean; // Use server's cipher preferences
741
+ sessionTimeout?: number; // TLS session timeout in seconds
742
+ }
743
+ ```
744
+
745
+ #### 2.2 Add Static Route Handler
746
+ ```typescript
747
+ // Add to ts/proxies/smart-proxy/route-connection-handler.ts
748
+
749
+ /**
750
+ * Handle the route based on its action type
751
+ */
752
+ switch (route.action.type) {
753
+ case 'forward':
754
+ return this.handleForwardAction(socket, record, route, initialChunk);
755
+
756
+ case 'redirect':
757
+ return this.handleRedirectAction(socket, record, route);
758
+
759
+ case 'block':
760
+ return this.handleBlockAction(socket, record, route);
761
+
762
+ case 'static':
763
+ return this.handleStaticAction(socket, record, route);
764
+
765
+ default:
766
+ console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
767
+ socket.end();
768
+ this.connectionManager.cleanupConnection(record, 'unknown_action');
769
+ }
770
+
771
+ /**
772
+ * Handle a static action for a route
773
+ */
774
+ private async handleStaticAction(
775
+ socket: plugins.net.Socket,
776
+ record: IConnectionRecord,
777
+ route: IRouteConfig
778
+ ): Promise<void> {
779
+ const connectionId = record.id;
780
+
781
+ if (!route.action.handler) {
782
+ console.error(`[${connectionId}] Static route '${route.name}' has no handler`);
783
+ socket.end();
784
+ this.connectionManager.cleanupConnection(record, 'no_handler');
785
+ return;
786
+ }
787
+
788
+ try {
789
+ // Build route context
790
+ const context: IRouteContext = {
791
+ port: record.localPort,
792
+ domain: record.lockedDomain,
793
+ clientIp: record.remoteIP,
794
+ serverIp: socket.localAddress!,
795
+ path: record.path, // Will need to be extracted from HTTP request
796
+ isTls: record.isTLS,
797
+ tlsVersion: record.tlsVersion,
798
+ routeName: route.name,
799
+ routeId: route.name,
800
+ timestamp: Date.now(),
801
+ connectionId
802
+ };
803
+
804
+ // Call the handler
805
+ const response = await route.action.handler(context);
806
+
807
+ // Send HTTP response
808
+ const headers = response.headers || {};
809
+ headers['Content-Length'] = Buffer.byteLength(response.body).toString();
810
+
811
+ let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
812
+ for (const [key, value] of Object.entries(headers)) {
813
+ httpResponse += `${key}: ${value}\r\n`;
814
+ }
815
+ httpResponse += '\r\n';
816
+
817
+ socket.write(httpResponse);
818
+ socket.write(response.body);
819
+ socket.end();
820
+
821
+ this.connectionManager.cleanupConnection(record, 'completed');
822
+ } catch (error) {
823
+ console.error(`[${connectionId}] Error in static handler: ${error}`);
824
+ socket.end();
825
+ this.connectionManager.cleanupConnection(record, 'handler_error');
826
+ }
827
+ }
828
+
829
+ // Helper function for status text
830
+ function getStatusText(status: number): string {
831
+ const statusTexts: Record<number, string> = {
832
+ 200: 'OK',
833
+ 404: 'Not Found',
834
+ 500: 'Internal Server Error'
835
+ };
836
+ return statusTexts[status] || 'Unknown';
837
+ }
838
+ ```
839
+
840
+ ### Phase 3: SmartProxy Integration
841
+
842
+ #### 3.1 Update SmartProxy class
843
+ ```typescript
844
+ // Changes to ts/proxies/smart-proxy/smart-proxy.ts
845
+
846
+ import { SmartCertManager } from './certificate-manager.js';
847
+ // Remove ALL certificate/ACME related imports:
848
+ // - CertProvisioner
849
+ // - Port80Handler
850
+ // - buildPort80Handler
851
+ // - createPort80HandlerOptions
852
+
853
+ export class SmartProxy extends plugins.EventEmitter {
854
+ // Replace certProvisioner and port80Handler with just:
855
+ private certManager: SmartCertManager | null = null;
856
+
857
+ constructor(settingsArg: ISmartProxyOptions) {
858
+ super();
859
+
860
+ // ... existing initialization ...
861
+
862
+ // No need for ACME settings in ISmartProxyOptions anymore
863
+ // Certificate configuration is now in route definitions
864
+ }
865
+
866
+ /**
867
+ * Initialize certificate manager
868
+ */
869
+ private async initializeCertificateManager(): Promise<void> {
870
+ // Extract global ACME options if any routes use auto certificates
871
+ const autoRoutes = this.settings.routes.filter(r =>
872
+ r.action.tls?.certificate === 'auto'
873
+ );
874
+
875
+ if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
876
+ console.log('No routes require certificate management');
877
+ return;
878
+ }
603
879
 
604
- client.on('end', () => {
605
- resolve(responseData);
880
+ // Use the first auto route's ACME config as defaults
881
+ const defaultAcme = autoRoutes[0]?.action.tls?.acme;
882
+
883
+ this.certManager = new SmartCertManager(
884
+ this.settings.routes,
885
+ './certs', // Certificate directory
886
+ defaultAcme ? {
887
+ email: defaultAcme.email,
888
+ useProduction: defaultAcme.useProduction,
889
+ port: defaultAcme.challengePort || 80
890
+ } : undefined
891
+ );
892
+
893
+ // Connect with NetworkProxy
894
+ if (this.networkProxyBridge.getNetworkProxy()) {
895
+ this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
896
+ }
897
+
898
+ // Set route update callback for ACME challenges
899
+ this.certManager.setUpdateRoutesCallback(async (routes) => {
900
+ await this.updateRoutes(routes);
606
901
  });
607
902
 
608
- client.on('error', (err) => {
609
- reject(err);
903
+ await this.certManager.initialize();
904
+ }
905
+
906
+ /**
907
+ * Check if we have routes with static certificates
908
+ */
909
+ private hasStaticCertRoutes(): boolean {
910
+ return this.settings.routes.some(r =>
911
+ r.action.tls?.certificate &&
912
+ r.action.tls.certificate !== 'auto'
913
+ );
914
+ }
915
+
916
+ public async start() {
917
+ if (this.isShuttingDown) {
918
+ console.log("Cannot start SmartProxy while it's shutting down");
919
+ return;
920
+ }
921
+
922
+ // Initialize certificate manager before starting servers
923
+ await this.initializeCertificateManager();
924
+
925
+ // Initialize and start NetworkProxy if needed
926
+ if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
927
+ await this.networkProxyBridge.initialize();
928
+
929
+ // Connect NetworkProxy with certificate manager
930
+ if (this.certManager) {
931
+ this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
932
+ }
933
+
934
+ await this.networkProxyBridge.start();
935
+ }
936
+
937
+ // ... rest of start method ...
938
+ }
939
+
940
+ public async stop() {
941
+ console.log('SmartProxy shutting down...');
942
+ this.isShuttingDown = true;
943
+ this.portManager.setShuttingDown(true);
944
+
945
+ // Stop certificate manager
946
+ if (this.certManager) {
947
+ await this.certManager.stop();
948
+ console.log('Certificate manager stopped');
949
+ }
950
+
951
+ // ... rest of stop method ...
952
+ }
953
+
954
+ /**
955
+ * Update routes with new configuration
956
+ */
957
+ public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
958
+ console.log(`Updating routes (${newRoutes.length} routes)`);
959
+
960
+ // Update certificate manager with new routes
961
+ if (this.certManager) {
962
+ await this.certManager.stop();
963
+
964
+ this.certManager = new SmartCertManager(
965
+ newRoutes,
966
+ './certs',
967
+ this.certManager.getAcmeOptions()
968
+ );
969
+
970
+ if (this.networkProxyBridge.getNetworkProxy()) {
971
+ this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
972
+ }
973
+
974
+ await this.certManager.initialize();
975
+ }
976
+
977
+ // ... rest of updateRoutes method ...
978
+ }
979
+
980
+ /**
981
+ * Manually provision a certificate for a route
982
+ */
983
+ public async provisionCertificate(routeName: string): Promise<void> {
984
+ if (!this.certManager) {
985
+ throw new Error('Certificate manager not initialized');
986
+ }
987
+
988
+ const route = this.settings.routes.find(r => r.name === routeName);
989
+ if (!route) {
990
+ throw new Error(`Route ${routeName} not found`);
991
+ }
992
+
993
+ await this.certManager.provisionCertificate(route);
994
+ }
995
+
996
+ /**
997
+ * Force renewal of a certificate
998
+ */
999
+ public async renewCertificate(routeName: string): Promise<void> {
1000
+ if (!this.certManager) {
1001
+ throw new Error('Certificate manager not initialized');
1002
+ }
1003
+
1004
+ await this.certManager.renewCertificate(routeName);
1005
+ }
1006
+
1007
+ /**
1008
+ * Get certificate status for a route
1009
+ */
1010
+ public getCertificateStatus(routeName: string): ICertStatus | undefined {
1011
+ if (!this.certManager) {
1012
+ return undefined;
1013
+ }
1014
+
1015
+ return this.certManager.getCertificateStatus(routeName);
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ #### 3.2 Simplify NetworkProxyBridge
1021
+ ```typescript
1022
+ // Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts
1023
+
1024
+ import * as plugins from '../../plugins.js';
1025
+ import { NetworkProxy } from '../network-proxy/index.js';
1026
+ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
1027
+ import type { IRouteConfig } from './models/route-types.js';
1028
+
1029
+ export class NetworkProxyBridge {
1030
+ private networkProxy: NetworkProxy | null = null;
1031
+
1032
+ constructor(private settings: ISmartProxyOptions) {}
1033
+
1034
+ /**
1035
+ * Get the NetworkProxy instance
1036
+ */
1037
+ public getNetworkProxy(): NetworkProxy | null {
1038
+ return this.networkProxy;
1039
+ }
1040
+
1041
+ /**
1042
+ * Initialize NetworkProxy instance
1043
+ */
1044
+ public async initialize(): Promise<void> {
1045
+ if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
1046
+ const networkProxyOptions: any = {
1047
+ port: this.settings.networkProxyPort!,
1048
+ portProxyIntegration: true,
1049
+ logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
1050
+ };
1051
+
1052
+ this.networkProxy = new NetworkProxy(networkProxyOptions);
1053
+ console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
1054
+
1055
+ // Apply route configurations to NetworkProxy
1056
+ await this.syncRoutesToNetworkProxy(this.settings.routes || []);
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Sync routes to NetworkProxy
1062
+ */
1063
+ private async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
1064
+ if (!this.networkProxy) return;
1065
+
1066
+ // Convert routes to NetworkProxy format
1067
+ const networkProxyConfigs = routes
1068
+ .filter(route =>
1069
+ this.settings.useNetworkProxy?.includes(route.match.domains?.[0]) ||
1070
+ this.settings.useNetworkProxy?.includes('*')
1071
+ )
1072
+ .map(route => this.routeToNetworkProxyConfig(route));
1073
+
1074
+ // Apply configurations to NetworkProxy
1075
+ await this.networkProxy.updateProxyConfigs(networkProxyConfigs);
1076
+ }
1077
+
1078
+ /**
1079
+ * Convert route to NetworkProxy configuration
1080
+ */
1081
+ private routeToNetworkProxyConfig(route: IRouteConfig): any {
1082
+ // Convert route to NetworkProxy domain config format
1083
+ return {
1084
+ domain: route.match.domains?.[0] || '*',
1085
+ target: route.action.target,
1086
+ tls: route.action.tls,
1087
+ security: route.action.security
1088
+ };
1089
+ }
1090
+
1091
+ /**
1092
+ * Check if connection should use NetworkProxy
1093
+ */
1094
+ public shouldUseNetworkProxy(connection: IConnectionRecord, routeMatch: any): boolean {
1095
+ // Only use NetworkProxy for TLS termination
1096
+ return (
1097
+ routeMatch.route.action.tls?.mode === 'terminate' ||
1098
+ routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt'
1099
+ ) && this.networkProxy !== null;
1100
+ }
1101
+
1102
+ /**
1103
+ * Pipe connection to NetworkProxy
1104
+ */
1105
+ public async pipeToNetworkProxy(socket: plugins.net.Socket): Promise<void> {
1106
+ if (!this.networkProxy) {
1107
+ throw new Error('NetworkProxy not initialized');
1108
+ }
1109
+
1110
+ const proxySocket = new plugins.net.Socket();
1111
+
1112
+ await new Promise<void>((resolve, reject) => {
1113
+ proxySocket.connect(this.settings.networkProxyPort!, 'localhost', () => {
1114
+ console.log(`Connected to NetworkProxy for termination`);
1115
+ resolve();
1116
+ });
1117
+
1118
+ proxySocket.on('error', reject);
610
1119
  });
611
- });
1120
+
1121
+ // Pipe the sockets together
1122
+ socket.pipe(proxySocket);
1123
+ proxySocket.pipe(socket);
1124
+
1125
+ // Handle cleanup
1126
+ const cleanup = () => {
1127
+ socket.unpipe(proxySocket);
1128
+ proxySocket.unpipe(socket);
1129
+ proxySocket.destroy();
1130
+ };
1131
+
1132
+ socket.on('end', cleanup);
1133
+ socket.on('error', cleanup);
1134
+ proxySocket.on('end', cleanup);
1135
+ proxySocket.on('error', cleanup);
1136
+ }
612
1137
 
613
- expect(response).toEqual(`Server says: ${TEST_DATA}`);
1138
+ /**
1139
+ * Start NetworkProxy
1140
+ */
1141
+ public async start(): Promise<void> {
1142
+ if (this.networkProxy) {
1143
+ await this.networkProxy.start();
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * Stop NetworkProxy
1149
+ */
1150
+ public async stop(): Promise<void> {
1151
+ if (this.networkProxy) {
1152
+ await this.networkProxy.stop();
1153
+ this.networkProxy = null;
1154
+ }
1155
+ }
1156
+ }
1157
+ ```
1158
+
1159
+ ### Phase 4: Configuration Examples (No Migration)
1160
+
1161
+ #### 4.1 New Configuration Format ONLY
1162
+ ```typescript
1163
+ // Update test files to use new structure
1164
+ // test/test.certificate-provisioning.ts
1165
+
1166
+ import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
1167
+ import { expect, tap } from '@push.rocks/tapbundle';
1168
+
1169
+ const testProxy = new SmartProxy({
1170
+ routes: [{
1171
+ name: 'test-route',
1172
+ match: { ports: 443, domains: 'test.example.com' },
1173
+ action: {
1174
+ type: 'forward',
1175
+ target: { host: 'localhost', port: 8080 },
1176
+ tls: {
1177
+ mode: 'terminate',
1178
+ certificate: 'auto',
1179
+ acme: {
1180
+ email: 'test@example.com',
1181
+ useProduction: false
1182
+ }
1183
+ }
1184
+ }
1185
+ }]
614
1186
  });
615
1187
 
616
- tap.test('cleanup NFTables integration test environment', async () => {
617
- // Stop the proxy and test server
618
- await smartProxy.stop();
1188
+ tap.test('should provision certificate automatically', async () => {
1189
+ await testProxy.start();
619
1190
 
620
- await new Promise<void>((resolve) => {
621
- testServer.close(() => {
622
- resolve();
623
- });
1191
+ // Wait for certificate provisioning
1192
+ await new Promise(resolve => setTimeout(resolve, 5000));
1193
+
1194
+ const status = testProxy.getCertificateStatus('test-route');
1195
+ expect(status).toBeDefined();
1196
+ expect(status.status).toEqual('valid');
1197
+ expect(status.source).toEqual('acme');
1198
+
1199
+ await testProxy.stop();
1200
+ });
1201
+
1202
+ tap.test('should handle static certificates', async () => {
1203
+ const proxy = new SmartProxy({
1204
+ routes: [{
1205
+ name: 'static-route',
1206
+ match: { ports: 443, domains: 'static.example.com' },
1207
+ action: {
1208
+ type: 'forward',
1209
+ target: { host: 'localhost', port: 8080 },
1210
+ tls: {
1211
+ mode: 'terminate',
1212
+ certificate: {
1213
+ certFile: './test/fixtures/cert.pem',
1214
+ keyFile: './test/fixtures/key.pem'
1215
+ }
1216
+ }
1217
+ }
1218
+ }]
624
1219
  });
1220
+
1221
+ await proxy.start();
1222
+
1223
+ const status = proxy.getCertificateStatus('static-route');
1224
+ expect(status).toBeDefined();
1225
+ expect(status.status).toEqual('valid');
1226
+ expect(status.source).toEqual('static');
1227
+
1228
+ await proxy.stop();
625
1229
  });
1230
+ ```
1231
+
1232
+ ### Phase 5: Documentation Update
1233
+
1234
+ #### 5.1 Update README.md sections
1235
+ ```markdown
1236
+ ## Certificate Management
1237
+
1238
+ SmartProxy includes built-in certificate management with automatic ACME (Let's Encrypt) support.
1239
+
1240
+ ### Automatic Certificates (ACME)
1241
+
1242
+ ```typescript
1243
+ const proxy = new SmartProxy({
1244
+ routes: [{
1245
+ name: 'secure-site',
1246
+ match: {
1247
+ ports: 443,
1248
+ domains: ['example.com', 'www.example.com']
1249
+ },
1250
+ action: {
1251
+ type: 'forward',
1252
+ target: { host: 'backend', port: 8080 },
1253
+ tls: {
1254
+ mode: 'terminate',
1255
+ certificate: 'auto',
1256
+ acme: {
1257
+ email: 'admin@example.com',
1258
+ useProduction: true,
1259
+ renewBeforeDays: 30
1260
+ }
1261
+ }
1262
+ }
1263
+ }]
1264
+ });
1265
+ ```
1266
+
1267
+ ### Static Certificates
1268
+
1269
+ ```typescript
1270
+ const proxy = new SmartProxy({
1271
+ routes: [{
1272
+ name: 'static-cert',
1273
+ match: { ports: 443, domains: 'secure.example.com' },
1274
+ action: {
1275
+ type: 'forward',
1276
+ target: { host: 'backend', port: 8080 },
1277
+ tls: {
1278
+ mode: 'terminate',
1279
+ certificate: {
1280
+ certFile: './certs/secure.pem',
1281
+ keyFile: './certs/secure.key'
1282
+ }
1283
+ }
1284
+ }
1285
+ }]
1286
+ });
1287
+ ```
1288
+
1289
+ ### Certificate Management API
1290
+
1291
+ ```typescript
1292
+ // Get certificate status
1293
+ const status = proxy.getCertificateStatus('route-name');
1294
+ console.log(status);
1295
+ // {
1296
+ // domain: 'example.com',
1297
+ // status: 'valid',
1298
+ // source: 'acme',
1299
+ // expiryDate: Date,
1300
+ // issueDate: Date
1301
+ // }
1302
+
1303
+ // Manually provision certificate
1304
+ await proxy.provisionCertificate('route-name');
1305
+
1306
+ // Force certificate renewal
1307
+ await proxy.renewCertificate('route-name');
1308
+ ```
1309
+
1310
+ ### Certificate Storage
626
1311
 
627
- export default tap.start();
1312
+ Certificates are stored in the `./certs` directory by default:
1313
+
1314
+ ```
1315
+ ./certs/
1316
+ ├── route-name/
1317
+ │ ├── cert.pem
1318
+ │ ├── key.pem
1319
+ │ ├── ca.pem (if available)
1320
+ │ └── meta.json
628
1321
  ```
1322
+ ```
1323
+
1324
+ ### Phase 5: Update HTTP Module
1325
+
1326
+ #### 5.1 Update http/index.ts
1327
+ ```typescript
1328
+ // ts/http/index.ts
1329
+ /**
1330
+ * HTTP functionality module
1331
+ */
1332
+
1333
+ // Export types and models
1334
+ export * from './models/http-types.js';
1335
+
1336
+ // Export submodules (remove port80 export)
1337
+ export * from './router/index.js';
1338
+ export * from './redirects/index.js';
1339
+ // REMOVED: export * from './port80/index.js';
1340
+
1341
+ // Convenience namespace exports (no more Port80)
1342
+ export const Http = {
1343
+ // Only router and redirect functionality remain
1344
+ };
1345
+ ```
1346
+
1347
+ ### Phase 6: Cleanup Tasks
1348
+
1349
+ #### 6.1 File Deletion Script
1350
+ ```bash
1351
+ #!/bin/bash
1352
+ # cleanup-certificates.sh
1353
+
1354
+ # Remove old certificate module
1355
+ rm -rf ts/certificate/
1356
+
1357
+ # Remove entire port80 subdirectory
1358
+ rm -rf ts/http/port80/
1359
+
1360
+ # Remove old imports from index files
1361
+ sed -i '/certificate\//d' ts/index.ts
1362
+ sed -i '/port80\//d' ts/http/index.ts
1363
+
1364
+ # Update plugins.ts to remove unused dependencies (if not used elsewhere)
1365
+ # sed -i '/smartexpress/d' ts/plugins.ts
1366
+ ```
1367
+
1368
+ #### 6.2 Key Simplifications Achieved
1369
+
1370
+ 1. **No custom ACME wrapper** - Direct use of @push.rocks/smartacme
1371
+ 2. **No separate HTTP server** - ACME challenges are regular routes
1372
+ 3. **Built-in path routing** - SmartProxy already handles path-based matching
1373
+ 4. **Built-in priorities** - Routes are already sorted by priority
1374
+ 5. **Safe updates** - Route updates are already thread-safe
1375
+ 6. **Minimal new code** - Mostly configuration and integration
1376
+
1377
+ The simplification leverages SmartProxy's existing capabilities rather than reinventing them.
1378
+
1379
+ #### 6.2 Update Package.json
1380
+ ```json
1381
+ {
1382
+ "dependencies": {
1383
+ // Remove if no longer needed elsewhere:
1384
+ // "@push.rocks/smartexpress": "x.x.x"
1385
+ }
1386
+ }
1387
+ ```
1388
+
1389
+ ## Implementation Sequence
1390
+
1391
+ 1. **Day 1: Core Implementation**
1392
+ - Create SmartCertManager class
1393
+ - Create CertStore and AcmeClient
1394
+ - Update route types
1395
+
1396
+ 2. **Day 2: Integration**
1397
+ - Update SmartProxy to use SmartCertManager
1398
+ - Simplify NetworkProxyBridge
1399
+ - Remove old certificate system
1400
+
1401
+ 3. **Day 3: Testing**
1402
+ - Create new tests using new format only
1403
+ - No migration testing needed
1404
+ - Test all new functionality
1405
+
1406
+ 4. **Day 4: Documentation & Cleanup**
1407
+ - Update all documentation
1408
+ - Clean up old files
1409
+ - Final testing and validation
629
1410
 
630
- ## Expected Benefits
1411
+ ## Risk Mitigation
631
1412
 
632
- 1. **Performance**: NFTables operates at the kernel level, offering much higher performance than Node.js-based routing.
633
- 2. **Scalability**: Handle more connections with less CPU and memory usage.
634
- 3. **Security**: Leverage kernel-level security features for better protection.
635
- 4. **Integration**: Unified configuration model between application and network layers.
636
- 5. **Advanced Features**: Support for QoS, rate limiting, and other advanced networking features.
1413
+ 1. **Static Route Handler**
1414
+ - Already exists in the type system
1415
+ - Just needs implementation in route-connection-handler.ts
1416
+ - Low risk as it follows existing patterns
637
1417
 
638
- ## Implementation Notes
1418
+ 2. **Route Updates During Operation**
1419
+ - SmartProxy's updateRoutes() is already thread-safe
1420
+ - Sequential processing prevents race conditions
1421
+ - Challenge routes are added/removed atomically
639
1422
 
640
- - This integration requires root/sudo access to configure NFTables rules.
641
- - Consider adding a capability check to gracefully fall back to Node.js routing if NFTables is not available.
642
- - The NFTables integration should be optional and SmartProxy should continue to work without it.
643
- - The integration provides a path for future extensions to other kernel-level networking features.
1423
+ 3. **Port 80 Conflicts**
1424
+ - Priority-based routing ensures ACME routes match first
1425
+ - Path-based matching (`/.well-known/acme-challenge/*`) is specific
1426
+ - Other routes on port 80 won't interfere
644
1427
 
645
- ## Timeline
1428
+ 4. **Error Recovery**
1429
+ - SmartAcme initialization failures are handled gracefully
1430
+ - Null checks prevent crashes if ACME isn't available
1431
+ - Routes continue to work without certificates
646
1432
 
647
- - Phase 1 (Route Configuration Schema): 1-2 days
648
- - Phase 2 (NFTablesManager): 2-3 days
649
- - Phase 3 (SmartProxy Integration): 1-2 days
650
- - Phase 4 (Routing System Integration): 1 day
651
- - Phase 5 (CLI and Helpers): 1 day
652
- - Phase 6 (Documentation and Testing): 2 days
1433
+ 5. **Testing Strategy**
1434
+ - Test concurrent ACME challenges
1435
+ - Test route priority conflicts
1436
+ - Test certificate renewal during high traffic
1437
+ - Test the new configuration format only
653
1438
 
654
- **Total Estimated Time: 8-11 days**
1439
+ 6. **No Migration Path**
1440
+ - Breaking change is intentional
1441
+ - Old configurations must be manually updated
1442
+ - No compatibility shims or helpers provided