@push.rocks/smartproxy 19.6.17 → 20.0.1

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 (27) hide show
  1. package/changelog.md +142 -0
  2. package/dist_ts/core/utils/shared-security-manager.js +30 -5
  3. package/dist_ts/proxies/http-proxy/request-handler.d.ts +4 -0
  4. package/dist_ts/proxies/http-proxy/request-handler.js +104 -21
  5. package/dist_ts/proxies/http-proxy/websocket-handler.d.ts +4 -0
  6. package/dist_ts/proxies/http-proxy/websocket-handler.js +78 -8
  7. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +19 -2
  8. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  9. package/dist_ts/proxies/smart-proxy/nftables-manager.js +14 -11
  10. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
  11. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +112 -28
  12. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +23 -23
  13. package/dist_ts/proxies/smart-proxy/utils/route-patterns.js +13 -13
  14. package/dist_ts/proxies/smart-proxy/utils/route-utils.js +4 -7
  15. package/dist_ts/proxies/smart-proxy/utils/route-validators.js +41 -25
  16. package/package.json +3 -2
  17. package/readme.plan.md +139 -266
  18. package/ts/core/utils/shared-security-manager.ts +33 -4
  19. package/ts/proxies/http-proxy/request-handler.ts +124 -21
  20. package/ts/proxies/http-proxy/websocket-handler.ts +96 -8
  21. package/ts/proxies/smart-proxy/models/route-types.ts +34 -8
  22. package/ts/proxies/smart-proxy/nftables-manager.ts +14 -10
  23. package/ts/proxies/smart-proxy/route-connection-handler.ts +132 -28
  24. package/ts/proxies/smart-proxy/utils/route-helpers.ts +14 -14
  25. package/ts/proxies/smart-proxy/utils/route-patterns.ts +6 -6
  26. package/ts/proxies/smart-proxy/utils/route-utils.ts +3 -6
  27. package/ts/proxies/smart-proxy/utils/route-validators.ts +38 -21
package/readme.plan.md CHANGED
@@ -1,281 +1,154 @@
1
- # SmartProxy Implementation Plan
2
-
3
- ## Feature: Custom Certificate Provision Function
4
-
5
- ### Summary
6
- This plan implements the `certProvisionFunction` feature that allows users to provide their own certificate generation logic. The function can either return a custom certificate or delegate back to Let's Encrypt by returning 'http01'.
7
-
8
- ### Key Changes
9
- 1. Add `certProvisionFunction` support to CertificateManager
10
- 2. Modify `provisionAcmeCertificate()` to check custom function first
11
- 3. Add certificate expiry parsing for custom certificates
12
- 4. Support both initial provisioning and renewal
13
- 5. Add fallback configuration option
14
-
15
- ### Overview
16
- Implement the `certProvisionFunction` callback that's defined in the interface but currently not implemented. This will allow users to provide custom certificate generation logic while maintaining backward compatibility with the existing Let's Encrypt integration.
17
-
18
- ### Requirements
19
- 1. The function should be called for any new certificate provisioning or renewal
20
- 2. Must support returning custom certificates or falling back to Let's Encrypt
21
- 3. Should integrate seamlessly with the existing certificate lifecycle
22
- 4. Must maintain backward compatibility
23
-
24
- ### Implementation Steps
25
-
26
- #### 1. Update Certificate Manager to Support Custom Provision Function
27
- **File**: `ts/proxies/smart-proxy/certificate-manager.ts`
28
-
29
- - [ ] Add `certProvisionFunction` property to CertificateManager class
30
- - [ ] Pass the function from SmartProxy options during initialization
31
- - [ ] Modify `provisionCertificate()` method to check for custom function first
32
-
33
- #### 2. Implement Custom Certificate Provisioning Logic
34
- **Location**: Modify `provisionAcmeCertificate()` method
35
-
1
+ # SmartProxy Enhanced Routing Plan
2
+
3
+ ## Goal
4
+ Implement enhanced routing structure with multiple targets per route, sub-matching capabilities, and target-specific overrides to enable more elegant and DRY configurations.
5
+
6
+ ## Key Changes
7
+
8
+ ### 1. Update Route Target Interface
9
+ - Add `match` property to `IRouteTarget` for sub-matching within routes
10
+ - Add target-specific override properties (tls, websocket, loadBalancing, etc.)
11
+ - Add priority field for controlling match order
12
+
13
+ ### 2. Update Route Action Interface
14
+ - Remove singular `target` property
15
+ - Use only `targets` array (single target = array with one element)
16
+ - Maintain backwards compatibility during migration
17
+
18
+ ### 3. Implementation Steps
19
+
20
+ #### Phase 1: Type Updates
21
+ - [x] Update `IRouteTarget` interface in `route-types.ts`
22
+ - Add `match?: ITargetMatch` property
23
+ - Add override properties (tls, websocket, etc.)
24
+ - Add `priority?: number` field
25
+ - [x] Create `ITargetMatch` interface for sub-matching criteria
26
+ - [x] Update `IRouteAction` to use only `targets: IRouteTarget[]`
27
+
28
+ #### Phase 2: Route Resolution Logic
29
+ - [x] Update route matching logic to handle multiple targets
30
+ - [x] Implement target sub-matching algorithm:
31
+ 1. Sort targets by priority (highest first)
32
+ 2. For each target with a match property, check if request matches
33
+ 3. Use first matching target, or fallback to target without match
34
+ - [x] Ensure target-specific settings override route-level settings
35
+
36
+ #### Phase 3: Code Migration
37
+ - [x] Find all occurrences of `action.target` and update to use `action.targets`
38
+ - [x] Update route helpers and utilities
39
+ - [x] Update certificate manager to handle multiple targets
40
+ - [x] Update connection handlers
41
+
42
+ #### Phase 4: Testing
43
+ - [x] Update existing tests to use new format
44
+ - [ ] Add tests for multi-target scenarios
45
+ - [ ] Add tests for sub-matching logic
46
+ - [ ] Add tests for setting overrides
47
+
48
+ #### Phase 5: Documentation
49
+ - [ ] Update type documentation
50
+ - [ ] Add examples of new routing patterns
51
+ - [ ] Document migration path for existing configs
52
+
53
+ ## Example Configurations
54
+
55
+ ### Before (Current)
36
56
  ```typescript
37
- private async provisionAcmeCertificate(
38
- route: IRouteConfig,
39
- domains: string[]
40
- ): Promise<void> {
41
- const primaryDomain = domains[0];
42
- const routeName = route.name || primaryDomain;
43
-
44
- // Check for custom provision function first
45
- if (this.certProvisionFunction) {
46
- try {
47
- logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain });
48
- const result = await this.certProvisionFunction(primaryDomain);
49
-
50
- if (result === 'http01') {
51
- logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`);
52
- // Continue with existing ACME logic below
53
- } else {
54
- // Use custom certificate
55
- const customCert = result as plugins.tsclass.network.ICert;
56
-
57
- // Convert to internal certificate format
58
- const certData: ICertificateData = {
59
- cert: customCert.cert,
60
- key: customCert.key,
61
- ca: customCert.ca || '',
62
- issueDate: new Date(),
63
- expiryDate: this.extractExpiryDate(customCert.cert)
64
- };
65
-
66
- // Store and apply certificate
67
- await this.certStore.saveCertificate(routeName, certData);
68
- await this.applyCertificate(primaryDomain, certData);
69
- this.updateCertStatus(routeName, 'valid', 'custom', certData);
70
-
71
- logger.log('info', `Custom certificate applied for ${primaryDomain}`, {
72
- domain: primaryDomain,
73
- expiryDate: certData.expiryDate
74
- });
75
- return;
76
- }
77
- } catch (error) {
78
- logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, {
79
- domain: primaryDomain,
80
- error: error.message
81
- });
82
- // Configuration option to control fallback behavior
83
- if (this.smartProxy.settings.certProvisionFallbackToAcme !== false) {
84
- logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`);
85
- } else {
86
- throw error;
87
- }
57
+ // Need separate routes for different ports/paths
58
+ [
59
+ {
60
+ match: { domains: ['api.example.com'], ports: [80] },
61
+ action: {
62
+ type: 'forward',
63
+ target: { host: 'backend', port: 8080 },
64
+ tls: { mode: 'terminate' }
65
+ }
66
+ },
67
+ {
68
+ match: { domains: ['api.example.com'], ports: [443] },
69
+ action: {
70
+ type: 'forward',
71
+ target: { host: 'backend', port: 8081 },
72
+ tls: { mode: 'passthrough' }
88
73
  }
89
74
  }
90
-
91
- // Existing Let's Encrypt logic continues here...
92
- if (!this.smartAcme) {
93
- throw new Error('SmartAcme not initialized...');
94
- }
95
- // ... rest of existing code
96
- }
75
+ ]
97
76
  ```
98
77
 
99
- #### 3. Add Helper Method for Certificate Expiry Extraction
100
- **New method**: `extractExpiryDate()`
101
-
102
- - [ ] Parse PEM certificate to extract expiry date
103
- - [ ] Use existing certificate parsing utilities
104
- - [ ] Handle parse errors gracefully
105
-
78
+ ### After (Enhanced)
106
79
  ```typescript
107
- private extractExpiryDate(certPem: string): Date {
108
- try {
109
- // Use forge or similar library to parse certificate
110
- const cert = forge.pki.certificateFromPem(certPem);
111
- return cert.validity.notAfter;
112
- } catch (error) {
113
- // Default to 90 days if parsing fails
114
- return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
80
+ // Single route with multiple targets
81
+ {
82
+ match: { domains: ['api.example.com'], ports: [80, 443] },
83
+ action: {
84
+ type: 'forward',
85
+ targets: [
86
+ {
87
+ match: { ports: [80] },
88
+ host: 'backend',
89
+ port: 8080,
90
+ tls: { mode: 'terminate' }
91
+ },
92
+ {
93
+ match: { ports: [443] },
94
+ host: 'backend',
95
+ port: 8081,
96
+ tls: { mode: 'passthrough' }
97
+ }
98
+ ]
115
99
  }
116
100
  }
117
101
  ```
118
102
 
119
- #### 4. Update SmartProxy Initialization
120
- **File**: `ts/proxies/smart-proxy/index.ts`
121
-
122
- - [ ] Pass `certProvisionFunction` from options to CertificateManager
123
- - [ ] Validate function if provided
124
-
125
- #### 5. Add Type Safety and Validation
126
- **Tasks**:
127
- - [ ] Validate returned certificate has required fields (cert, key, ca)
128
- - [ ] Check certificate validity dates
129
- - [ ] Ensure certificate matches requested domain
130
-
131
- #### 6. Update Certificate Renewal Logic
132
- **Location**: `checkAndRenewCertificates()`
133
-
134
- - [ ] Ensure renewal checks work for both ACME and custom certificates
135
- - [ ] Custom certificates should go through the same `provisionAcmeCertificate()` path
136
- - [ ] The existing renewal logic already calls `provisionCertificate()` which will use our modified flow
137
-
103
+ ### Advanced Example
138
104
  ```typescript
139
- // No changes needed here - the existing renewal logic will automatically
140
- // use the custom provision function when calling provisionCertificate()
141
- private async checkAndRenewCertificates(): Promise<void> {
142
- // Existing code already handles this correctly
143
- for (const route of routes) {
144
- if (this.shouldRenewCertificate(cert, renewThreshold)) {
145
- // This will call provisionCertificate -> provisionAcmeCertificate
146
- // which now includes our custom function check
147
- await this.provisionCertificate(route);
148
- }
105
+ {
106
+ match: { domains: ['app.example.com'], ports: [443] },
107
+ action: {
108
+ type: 'forward',
109
+ tls: { mode: 'terminate', certificate: 'auto' }, // Route-level default
110
+ websocket: { enabled: true }, // Route-level default
111
+ targets: [
112
+ {
113
+ match: { path: '/api/v2/*' },
114
+ host: 'api-v2',
115
+ port: 8082,
116
+ priority: 10
117
+ },
118
+ {
119
+ match: { path: '/api/*', headers: { 'X-Version': 'v1' } },
120
+ host: 'api-v1',
121
+ port: 8081,
122
+ priority: 5
123
+ },
124
+ {
125
+ match: { path: '/ws/*' },
126
+ host: 'websocket-server',
127
+ port: 8090,
128
+ websocket: {
129
+ enabled: true,
130
+ rewritePath: '/' // Strip /ws prefix
131
+ }
132
+ },
133
+ {
134
+ // Default target (no match property)
135
+ host: 'web-backend',
136
+ port: 8080
137
+ }
138
+ ]
149
139
  }
150
140
  }
151
141
  ```
152
142
 
153
- #### 7. Add Integration Tests
154
- **File**: `test/test.certificate-provision.ts`
155
-
156
- - [ ] Test custom certificate provision
157
- - [ ] Test fallback to Let's Encrypt ('http01' return)
158
- - [ ] Test error handling
159
- - [ ] Test renewal with custom function
160
-
161
- #### 8. Update Documentation
162
- **Files**:
163
- - [ ] Update interface documentation
164
- - [ ] Add examples to README
165
- - [ ] Document ICert structure requirements
166
-
167
- ### API Design
168
-
169
- ```typescript
170
- // Example usage
171
- const proxy = new SmartProxy({
172
- certProvisionFunction: async (domain: string) => {
173
- // Option 1: Return custom certificate
174
- const customCert = await myCustomCA.generateCert(domain);
175
- return {
176
- cert: customCert.certificate,
177
- key: customCert.privateKey,
178
- ca: customCert.chain
179
- };
180
-
181
- // Option 2: Use Let's Encrypt for certain domains
182
- if (domain.endsWith('.internal')) {
183
- return customCert;
184
- }
185
- return 'http01'; // Fallback to Let's Encrypt
186
- },
187
- certProvisionFallbackToAcme: true, // Default: true
188
- routes: [...]
189
- });
190
- ```
191
-
192
- ### Configuration Options to Add
193
-
194
- ```typescript
195
- interface ISmartProxyOptions {
196
- // Existing options...
197
-
198
- // Custom certificate provision function
199
- certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
200
-
201
- // Whether to fallback to ACME if custom provision fails
202
- certProvisionFallbackToAcme?: boolean; // Default: true
203
- }
204
- ```
205
-
206
- ### Error Handling Strategy
207
-
208
- 1. **Custom Function Errors**:
209
- - Log detailed error with domain context
210
- - Option A: Fallback to Let's Encrypt (safer)
211
- - Option B: Fail certificate provisioning (stricter)
212
- - Make this configurable via option?
213
-
214
- 2. **Invalid Certificate Returns**:
215
- - Validate certificate structure
216
- - Check expiry dates
217
- - Verify domain match
218
-
219
- ### Testing Plan
220
-
221
- 1. **Unit Tests**:
222
- - Mock certProvisionFunction returns
223
- - Test validation logic
224
- - Test error scenarios
225
-
226
- 2. **Integration Tests**:
227
- - Real certificate generation
228
- - Renewal cycle testing
229
- - Mixed custom/Let's Encrypt scenarios
230
-
231
- ### Backward Compatibility
232
-
233
- - If no `certProvisionFunction` provided, behavior unchanged
234
- - Existing routes with 'auto' certificates continue using Let's Encrypt
235
- - No breaking changes to existing API
236
-
237
- ### Future Enhancements
238
-
239
- 1. **Per-Route Custom Functions**:
240
- - Allow different provision functions per route
241
- - Override global function at route level
242
-
243
- 2. **Certificate Events**:
244
- - Emit events for custom cert provisioning
245
- - Allow monitoring/logging hooks
246
-
247
- 3. **Async Certificate Updates**:
248
- - Support updating certificates outside renewal cycle
249
- - Hot-reload certificates without restart
250
-
251
- ### Implementation Notes
252
-
253
- 1. **Certificate Status Tracking**:
254
- - The `updateCertStatus()` method needs to support a new type: 'custom'
255
- - Current types are 'acme' and 'static'
256
- - This helps distinguish custom certificates in monitoring/logs
257
-
258
- 2. **Certificate Store Integration**:
259
- - Custom certificates are stored the same way as ACME certificates
260
- - They participate in the same renewal cycle
261
- - The store handles persistence across restarts
262
-
263
- 3. **Existing Methods to Reuse**:
264
- - `applyCertificate()` - Already handles applying certs to routes
265
- - `isCertificateValid()` - Can validate custom certificates
266
- - `certStore.saveCertificate()` - Handles storage
267
-
268
- ### Implementation Priority
269
-
270
- 1. Core functionality (steps 1-3)
271
- 2. Type safety and validation (step 5)
272
- 3. Renewal support (step 6)
273
- 4. Tests (step 7)
274
- 5. Documentation (step 8)
275
-
276
- ### Estimated Effort
277
-
278
- - Core implementation: 4-6 hours
279
- - Testing: 2-3 hours
280
- - Documentation: 1 hour
281
- - Total: ~8-10 hours
143
+ ## Benefits
144
+ 1. **DRY Configuration**: No need to duplicate common settings across routes
145
+ 2. **Flexibility**: Different backends for different ports/paths within same domain
146
+ 3. **Clarity**: All routing for a domain in one place
147
+ 4. **Performance**: Single route lookup instead of multiple
148
+ 5. **Backwards Compatible**: Can migrate gradually
149
+
150
+ ## Migration Strategy
151
+ 1. Keep support for `target` temporarily with deprecation warning
152
+ 2. Auto-convert `target` to `targets: [target]` internally
153
+ 3. Update documentation with migration examples
154
+ 4. Remove `target` support in next major version
@@ -13,7 +13,8 @@ import {
13
13
  trackConnection,
14
14
  removeConnection,
15
15
  cleanupExpiredRateLimits,
16
- parseBasicAuthHeader
16
+ parseBasicAuthHeader,
17
+ normalizeIP
17
18
  } from './security-utils.js';
18
19
 
19
20
  /**
@@ -78,7 +79,15 @@ export class SharedSecurityManager {
78
79
  * @returns Number of connections from this IP
79
80
  */
80
81
  public getConnectionCountByIP(ip: string): number {
81
- return this.connectionsByIP.get(ip)?.connections.size || 0;
82
+ // Check all normalized variants of the IP
83
+ const variants = normalizeIP(ip);
84
+ for (const variant of variants) {
85
+ const info = this.connectionsByIP.get(variant);
86
+ if (info) {
87
+ return info.connections.size;
88
+ }
89
+ }
90
+ return 0;
82
91
  }
83
92
 
84
93
  /**
@@ -88,7 +97,19 @@ export class SharedSecurityManager {
88
97
  * @param connectionId - The connection ID to associate
89
98
  */
90
99
  public trackConnectionByIP(ip: string, connectionId: string): void {
91
- trackConnection(ip, connectionId, this.connectionsByIP);
100
+ // Check if any variant already exists
101
+ const variants = normalizeIP(ip);
102
+ let existingKey: string | null = null;
103
+
104
+ for (const variant of variants) {
105
+ if (this.connectionsByIP.has(variant)) {
106
+ existingKey = variant;
107
+ break;
108
+ }
109
+ }
110
+
111
+ // Use existing key or the original IP
112
+ trackConnection(existingKey || ip, connectionId, this.connectionsByIP);
92
113
  }
93
114
 
94
115
  /**
@@ -98,7 +119,15 @@ export class SharedSecurityManager {
98
119
  * @param connectionId - The connection ID to remove
99
120
  */
100
121
  public removeConnectionByIP(ip: string, connectionId: string): void {
101
- removeConnection(ip, connectionId, this.connectionsByIP);
122
+ // Check all variants to find where the connection is tracked
123
+ const variants = normalizeIP(ip);
124
+
125
+ for (const variant of variants) {
126
+ if (this.connectionsByIP.has(variant)) {
127
+ removeConnection(variant, connectionId, this.connectionsByIP);
128
+ break;
129
+ }
130
+ }
102
131
  }
103
132
 
104
133
  /**