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