@push.rocks/smartproxy 19.6.15 → 19.6.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/core/utils/log-deduplicator.js +10 -2
- package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +17 -1
- package/dist_ts/proxies/smart-proxy/certificate-manager.js +84 -10
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -0
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +9 -1
- package/package.json +1 -1
- package/readme.hints.md +52 -2
- package/readme.md +105 -2
- package/readme.plan.md +270 -34
- package/ts/core/utils/log-deduplicator.ts +10 -1
- package/ts/proxies/smart-proxy/certificate-manager.ts +98 -13
- package/ts/proxies/smart-proxy/models/interfaces.ts +6 -0
- package/ts/proxies/smart-proxy/smart-proxy.ts +10 -0
package/readme.plan.md
CHANGED
|
@@ -1,45 +1,281 @@
|
|
|
1
|
-
# SmartProxy
|
|
1
|
+
# SmartProxy Implementation Plan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Feature: Custom Certificate Provision Function
|
|
4
4
|
|
|
5
|
-
|
|
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'.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
11
14
|
|
|
12
|
-
|
|
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.
|
|
13
17
|
|
|
14
|
-
###
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
18
23
|
|
|
19
|
-
###
|
|
20
|
-
- [x] Add connection count tracking per route in ConnectionManager
|
|
21
|
-
- [x] Update SharedSecurityManager.isAllowed() to check route-specific maxConnections
|
|
22
|
-
- [x] Add route connection limit validation in route-connection-handler.ts
|
|
24
|
+
### Implementation Steps
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
- [x] Ensure new connections added during processing aren't missed
|
|
27
|
-
- [x] Add proper synchronization for cleanup operations
|
|
26
|
+
#### 1. Update Certificate Manager to Support Custom Provision Function
|
|
27
|
+
**File**: `ts/proxies/smart-proxy/certificate-manager.ts`
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [x] Add memory-efficient data structures for IP tracking
|
|
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
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
- [x] Test route-level connection limits
|
|
37
|
-
- [x] Test cleanup queue edge cases
|
|
38
|
-
- [x] Test memory usage with many unique IPs
|
|
33
|
+
#### 2. Implement Custom Certificate Provisioning Logic
|
|
34
|
+
**Location**: Modify `provisionAcmeCertificate()` method
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
```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
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
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
|
+
}
|
|
97
|
+
```
|
|
41
98
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
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
|
+
|
|
106
|
+
```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);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
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
|
+
|
|
138
|
+
```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
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
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
|
|
@@ -269,6 +269,7 @@ export class LogDeduplicator {
|
|
|
269
269
|
|
|
270
270
|
private flushIPRejections(aggregated: IAggregatedEvent): void {
|
|
271
271
|
const byIP = new Map<string, { count: number; reasons: Set<string> }>();
|
|
272
|
+
const allReasons = new Map<string, number>();
|
|
272
273
|
|
|
273
274
|
for (const [ip, event] of aggregated.events) {
|
|
274
275
|
if (!byIP.has(ip)) {
|
|
@@ -278,9 +279,17 @@ export class LogDeduplicator {
|
|
|
278
279
|
ipData.count += event.count;
|
|
279
280
|
if (event.data?.reason) {
|
|
280
281
|
ipData.reasons.add(event.data.reason);
|
|
282
|
+
// Track overall reason counts
|
|
283
|
+
allReasons.set(event.data.reason, (allReasons.get(event.data.reason) || 0) + event.count);
|
|
281
284
|
}
|
|
282
285
|
}
|
|
283
286
|
|
|
287
|
+
// Create reason summary
|
|
288
|
+
const reasonSummary = Array.from(allReasons.entries())
|
|
289
|
+
.sort((a, b) => b[1] - a[1])
|
|
290
|
+
.map(([reason, count]) => `${reason}: ${count}`)
|
|
291
|
+
.join(', ');
|
|
292
|
+
|
|
284
293
|
// Log top offenders
|
|
285
294
|
const topOffenders = Array.from(byIP.entries())
|
|
286
295
|
.sort((a, b) => b[1].count - a[1].count)
|
|
@@ -291,7 +300,7 @@ export class LogDeduplicator {
|
|
|
291
300
|
const totalRejections = Array.from(byIP.values()).reduce((sum, data) => sum + data.count, 0);
|
|
292
301
|
|
|
293
302
|
const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
|
|
294
|
-
logger.log('warn', `[SUMMARY] Rejected ${totalRejections} connections from ${byIP.size} IPs in ${Math.round(duration/1000)}s`, {
|
|
303
|
+
logger.log('warn', `[SUMMARY] Rejected ${totalRejections} connections from ${byIP.size} IPs in ${Math.round(duration/1000)}s (${reasonSummary})`, {
|
|
295
304
|
topOffenders,
|
|
296
305
|
component: 'ip-dedup'
|
|
297
306
|
});
|
|
@@ -12,7 +12,7 @@ export interface ICertStatus {
|
|
|
12
12
|
status: 'valid' | 'pending' | 'expired' | 'error';
|
|
13
13
|
expiryDate?: Date;
|
|
14
14
|
issueDate?: Date;
|
|
15
|
-
source: 'static' | 'acme';
|
|
15
|
+
source: 'static' | 'acme' | 'custom';
|
|
16
16
|
error?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -22,6 +22,7 @@ export interface ICertificateData {
|
|
|
22
22
|
ca?: string;
|
|
23
23
|
expiryDate: Date;
|
|
24
24
|
issueDate: Date;
|
|
25
|
+
source?: 'static' | 'acme' | 'custom';
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export class SmartCertManager {
|
|
@@ -50,6 +51,12 @@ export class SmartCertManager {
|
|
|
50
51
|
// ACME state manager reference
|
|
51
52
|
private acmeStateManager: AcmeStateManager | null = null;
|
|
52
53
|
|
|
54
|
+
// Custom certificate provision function
|
|
55
|
+
private certProvisionFunction?: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>;
|
|
56
|
+
|
|
57
|
+
// Whether to fallback to ACME if custom provision fails
|
|
58
|
+
private certProvisionFallbackToAcme: boolean = true;
|
|
59
|
+
|
|
53
60
|
constructor(
|
|
54
61
|
private routes: IRouteConfig[],
|
|
55
62
|
private certDir: string = './certs',
|
|
@@ -89,6 +96,20 @@ export class SmartCertManager {
|
|
|
89
96
|
this.globalAcmeDefaults = defaults;
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Set custom certificate provision function
|
|
101
|
+
*/
|
|
102
|
+
public setCertProvisionFunction(fn: (domain: string) => Promise<plugins.tsclass.network.ICert | 'http01'>): void {
|
|
103
|
+
this.certProvisionFunction = fn;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set whether to fallback to ACME if custom provision fails
|
|
108
|
+
*/
|
|
109
|
+
public setCertProvisionFallbackToAcme(fallback: boolean): void {
|
|
110
|
+
this.certProvisionFallbackToAcme = fallback;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
/**
|
|
93
114
|
* Set callback for updating routes (used for challenge routes)
|
|
94
115
|
*/
|
|
@@ -212,15 +233,6 @@ export class SmartCertManager {
|
|
|
212
233
|
route: IRouteConfig,
|
|
213
234
|
domains: string[]
|
|
214
235
|
): Promise<void> {
|
|
215
|
-
if (!this.smartAcme) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
'SmartAcme not initialized. This usually means no ACME email was provided. ' +
|
|
218
|
-
'Please ensure you have configured ACME with an email address either:\n' +
|
|
219
|
-
'1. In the top-level "acme" configuration\n' +
|
|
220
|
-
'2. In the route\'s "tls.acme" configuration'
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
236
|
const primaryDomain = domains[0];
|
|
225
237
|
const routeName = route.name || primaryDomain;
|
|
226
238
|
|
|
@@ -229,10 +241,68 @@ export class SmartCertManager {
|
|
|
229
241
|
if (existingCert && this.isCertificateValid(existingCert)) {
|
|
230
242
|
logger.log('info', `Using existing valid certificate for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
|
231
243
|
await this.applyCertificate(primaryDomain, existingCert);
|
|
232
|
-
this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
|
|
244
|
+
this.updateCertStatus(routeName, 'valid', existingCert.source || 'acme', existingCert);
|
|
233
245
|
return;
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
// Check for custom provision function first
|
|
249
|
+
if (this.certProvisionFunction) {
|
|
250
|
+
try {
|
|
251
|
+
logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
|
252
|
+
const result = await this.certProvisionFunction(primaryDomain);
|
|
253
|
+
|
|
254
|
+
if (result === 'http01') {
|
|
255
|
+
logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
|
256
|
+
// Continue with existing ACME logic below
|
|
257
|
+
} else {
|
|
258
|
+
// Use custom certificate
|
|
259
|
+
const customCert = result as plugins.tsclass.network.ICert;
|
|
260
|
+
|
|
261
|
+
// Convert to internal certificate format
|
|
262
|
+
const certData: ICertificateData = {
|
|
263
|
+
cert: customCert.publicKey,
|
|
264
|
+
key: customCert.privateKey,
|
|
265
|
+
ca: '',
|
|
266
|
+
issueDate: new Date(),
|
|
267
|
+
expiryDate: this.extractExpiryDate(customCert.publicKey),
|
|
268
|
+
source: 'custom'
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Store and apply certificate
|
|
272
|
+
await this.certStore.saveCertificate(routeName, certData);
|
|
273
|
+
await this.applyCertificate(primaryDomain, certData);
|
|
274
|
+
this.updateCertStatus(routeName, 'valid', 'custom', certData);
|
|
275
|
+
|
|
276
|
+
logger.log('info', `Custom certificate applied for ${primaryDomain}`, {
|
|
277
|
+
domain: primaryDomain,
|
|
278
|
+
expiryDate: certData.expiryDate,
|
|
279
|
+
component: 'certificate-manager'
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, {
|
|
285
|
+
domain: primaryDomain,
|
|
286
|
+
error: error.message,
|
|
287
|
+
component: 'certificate-manager'
|
|
288
|
+
});
|
|
289
|
+
// Check if we should fallback to ACME
|
|
290
|
+
if (!this.certProvisionFallbackToAcme) {
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`, { domain: primaryDomain, component: 'certificate-manager' });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!this.smartAcme) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
'SmartAcme not initialized. This usually means no ACME email was provided. ' +
|
|
300
|
+
'Please ensure you have configured ACME with an email address either:\n' +
|
|
301
|
+
'1. In the top-level "acme" configuration\n' +
|
|
302
|
+
'2. In the route\'s "tls.acme" configuration'
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
236
306
|
// Apply renewal threshold from global defaults or route config
|
|
237
307
|
const renewThreshold = route.action.tls?.acme?.renewBeforeDays ||
|
|
238
308
|
this.globalAcmeDefaults?.renewThresholdDays ||
|
|
@@ -280,7 +350,8 @@ export class SmartCertManager {
|
|
|
280
350
|
key: cert.privateKey,
|
|
281
351
|
ca: cert.publicKey, // Use same as cert for now
|
|
282
352
|
expiryDate: new Date(cert.validUntil),
|
|
283
|
-
issueDate: new Date(cert.created)
|
|
353
|
+
issueDate: new Date(cert.created),
|
|
354
|
+
source: 'acme'
|
|
284
355
|
};
|
|
285
356
|
|
|
286
357
|
await this.certStore.saveCertificate(routeName, certData);
|
|
@@ -328,7 +399,8 @@ export class SmartCertManager {
|
|
|
328
399
|
cert,
|
|
329
400
|
key,
|
|
330
401
|
expiryDate: certInfo.validTo,
|
|
331
|
-
issueDate: certInfo.validFrom
|
|
402
|
+
issueDate: certInfo.validFrom,
|
|
403
|
+
source: 'static'
|
|
332
404
|
};
|
|
333
405
|
|
|
334
406
|
// Save to store for consistency
|
|
@@ -399,6 +471,19 @@ export class SmartCertManager {
|
|
|
399
471
|
return cert.expiryDate > expiryThreshold;
|
|
400
472
|
}
|
|
401
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Extract expiry date from a PEM certificate
|
|
476
|
+
*/
|
|
477
|
+
private extractExpiryDate(_certPem: string): Date {
|
|
478
|
+
// For now, we'll default to 90 days for custom certificates
|
|
479
|
+
// In production, you might want to use a proper X.509 parser
|
|
480
|
+
// or require the custom cert provider to include expiry info
|
|
481
|
+
logger.log('info', 'Using default 90-day expiry for custom certificate', {
|
|
482
|
+
component: 'certificate-manager'
|
|
483
|
+
});
|
|
484
|
+
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
|
485
|
+
}
|
|
486
|
+
|
|
402
487
|
|
|
403
488
|
/**
|
|
404
489
|
* Add challenge route to SmartProxy
|
|
@@ -135,6 +135,12 @@ export interface ISmartProxyOptions {
|
|
|
135
135
|
* or a static certificate object for immediate provisioning.
|
|
136
136
|
*/
|
|
137
137
|
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Whether to fallback to ACME if custom certificate provision fails.
|
|
141
|
+
* Default: true
|
|
142
|
+
*/
|
|
143
|
+
certProvisionFallbackToAcme?: boolean;
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
/**
|
|
@@ -243,6 +243,16 @@ export class SmartProxy extends plugins.EventEmitter {
|
|
|
243
243
|
certManager.setGlobalAcmeDefaults(this.settings.acme);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// Pass down the custom certificate provision function if available
|
|
247
|
+
if (this.settings.certProvisionFunction) {
|
|
248
|
+
certManager.setCertProvisionFunction(this.settings.certProvisionFunction);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Pass down the fallback to ACME setting
|
|
252
|
+
if (this.settings.certProvisionFallbackToAcme !== undefined) {
|
|
253
|
+
certManager.setCertProvisionFallbackToAcme(this.settings.certProvisionFallbackToAcme);
|
|
254
|
+
}
|
|
255
|
+
|
|
246
256
|
await certManager.initialize();
|
|
247
257
|
return certManager;
|
|
248
258
|
}
|