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