update
This commit is contained in:
393
readme.plan.md
393
readme.plan.md
@@ -1,281 +1,154 @@
|
||||
# SmartProxy Implementation Plan
|
||||
# SmartProxy Enhanced Routing Plan
|
||||
|
||||
## Feature: Custom Certificate Provision Function
|
||||
## Goal
|
||||
Implement enhanced routing structure with multiple targets per route, sub-matching capabilities, and target-specific overrides to enable more elegant and DRY configurations.
|
||||
|
||||
### Summary
|
||||
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'.
|
||||
## Key Changes
|
||||
|
||||
### Key Changes
|
||||
1. Add `certProvisionFunction` support to CertificateManager
|
||||
2. Modify `provisionAcmeCertificate()` to check custom function first
|
||||
3. Add certificate expiry parsing for custom certificates
|
||||
4. Support both initial provisioning and renewal
|
||||
5. Add fallback configuration option
|
||||
### 1. Update Route Target Interface
|
||||
- Add `match` property to `IRouteTarget` for sub-matching within routes
|
||||
- Add target-specific override properties (tls, websocket, loadBalancing, etc.)
|
||||
- Add priority field for controlling match order
|
||||
|
||||
### Overview
|
||||
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.
|
||||
### 2. Update Route Action Interface
|
||||
- Remove singular `target` property
|
||||
- Use only `targets` array (single target = array with one element)
|
||||
- Maintain backwards compatibility during migration
|
||||
|
||||
### Requirements
|
||||
1. The function should be called for any new certificate provisioning or renewal
|
||||
2. Must support returning custom certificates or falling back to Let's Encrypt
|
||||
3. Should integrate seamlessly with the existing certificate lifecycle
|
||||
4. Must maintain backward compatibility
|
||||
### 3. Implementation Steps
|
||||
|
||||
### Implementation Steps
|
||||
#### Phase 1: Type Updates
|
||||
- [ ] Update `IRouteTarget` interface in `route-types.ts`
|
||||
- Add `match?: ITargetMatch` property
|
||||
- Add override properties (tls, websocket, etc.)
|
||||
- Add `priority?: number` field
|
||||
- [ ] Create `ITargetMatch` interface for sub-matching criteria
|
||||
- [ ] Update `IRouteAction` to use only `targets: IRouteTarget[]`
|
||||
|
||||
#### 1. Update Certificate Manager to Support Custom Provision Function
|
||||
**File**: `ts/proxies/smart-proxy/certificate-manager.ts`
|
||||
#### Phase 2: Route Resolution Logic
|
||||
- [ ] Update route matching logic to handle multiple targets
|
||||
- [ ] Implement target sub-matching algorithm:
|
||||
1. Sort targets by priority (highest first)
|
||||
2. For each target with a match property, check if request matches
|
||||
3. Use first matching target, or fallback to target without match
|
||||
- [ ] Ensure target-specific settings override route-level settings
|
||||
|
||||
- [ ] Add `certProvisionFunction` property to CertificateManager class
|
||||
- [ ] Pass the function from SmartProxy options during initialization
|
||||
- [ ] Modify `provisionCertificate()` method to check for custom function first
|
||||
#### Phase 3: Code Migration
|
||||
- [ ] Find all occurrences of `action.target` and update to `action.targets[0]`
|
||||
- [ ] Update route helpers and utilities
|
||||
- [ ] Update certificate manager to handle multiple targets
|
||||
- [ ] Update connection handlers
|
||||
|
||||
#### 2. Implement Custom Certificate Provisioning Logic
|
||||
**Location**: Modify `provisionAcmeCertificate()` method
|
||||
#### Phase 4: Testing
|
||||
- [ ] Update existing tests to use new format
|
||||
- [ ] Add tests for multi-target scenarios
|
||||
- [ ] Add tests for sub-matching logic
|
||||
- [ ] Add tests for setting overrides
|
||||
|
||||
#### Phase 5: Documentation
|
||||
- [ ] Update type documentation
|
||||
- [ ] Add examples of new routing patterns
|
||||
- [ ] Document migration path for existing configs
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Before (Current)
|
||||
```typescript
|
||||
private async provisionAcmeCertificate(
|
||||
route: IRouteConfig,
|
||||
domains: string[]
|
||||
): Promise<void> {
|
||||
const primaryDomain = domains[0];
|
||||
const routeName = route.name || primaryDomain;
|
||||
|
||||
// Check for custom provision function first
|
||||
if (this.certProvisionFunction) {
|
||||
try {
|
||||
logger.log('info', `Attempting custom certificate provision for ${primaryDomain}`, { domain: primaryDomain });
|
||||
const result = await this.certProvisionFunction(primaryDomain);
|
||||
|
||||
if (result === 'http01') {
|
||||
logger.log('info', `Custom function returned 'http01', falling back to Let's Encrypt for ${primaryDomain}`);
|
||||
// Continue with existing ACME logic below
|
||||
} else {
|
||||
// Use custom certificate
|
||||
const customCert = result as plugins.tsclass.network.ICert;
|
||||
|
||||
// Convert to internal certificate format
|
||||
const certData: ICertificateData = {
|
||||
cert: customCert.cert,
|
||||
key: customCert.key,
|
||||
ca: customCert.ca || '',
|
||||
issueDate: new Date(),
|
||||
expiryDate: this.extractExpiryDate(customCert.cert)
|
||||
};
|
||||
|
||||
// Store and apply certificate
|
||||
await this.certStore.saveCertificate(routeName, certData);
|
||||
await this.applyCertificate(primaryDomain, certData);
|
||||
this.updateCertStatus(routeName, 'valid', 'custom', certData);
|
||||
|
||||
logger.log('info', `Custom certificate applied for ${primaryDomain}`, {
|
||||
domain: primaryDomain,
|
||||
expiryDate: certData.expiryDate
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log('error', `Custom cert provision failed for ${primaryDomain}: ${error.message}`, {
|
||||
domain: primaryDomain,
|
||||
error: error.message
|
||||
});
|
||||
// Configuration option to control fallback behavior
|
||||
if (this.smartProxy.settings.certProvisionFallbackToAcme !== false) {
|
||||
logger.log('info', `Falling back to Let's Encrypt for ${primaryDomain}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
// Need separate routes for different ports/paths
|
||||
[
|
||||
{
|
||||
match: { domains: ['api.example.com'], ports: [80] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: 'backend', port: 8080 },
|
||||
tls: { mode: 'terminate' }
|
||||
}
|
||||
}
|
||||
|
||||
// Existing Let's Encrypt logic continues here...
|
||||
if (!this.smartAcme) {
|
||||
throw new Error('SmartAcme not initialized...');
|
||||
}
|
||||
// ... rest of existing code
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Add Helper Method for Certificate Expiry Extraction
|
||||
**New method**: `extractExpiryDate()`
|
||||
|
||||
- [ ] Parse PEM certificate to extract expiry date
|
||||
- [ ] Use existing certificate parsing utilities
|
||||
- [ ] Handle parse errors gracefully
|
||||
|
||||
```typescript
|
||||
private extractExpiryDate(certPem: string): Date {
|
||||
try {
|
||||
// Use forge or similar library to parse certificate
|
||||
const cert = forge.pki.certificateFromPem(certPem);
|
||||
return cert.validity.notAfter;
|
||||
} catch (error) {
|
||||
// Default to 90 days if parsing fails
|
||||
return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Update SmartProxy Initialization
|
||||
**File**: `ts/proxies/smart-proxy/index.ts`
|
||||
|
||||
- [ ] Pass `certProvisionFunction` from options to CertificateManager
|
||||
- [ ] Validate function if provided
|
||||
|
||||
#### 5. Add Type Safety and Validation
|
||||
**Tasks**:
|
||||
- [ ] Validate returned certificate has required fields (cert, key, ca)
|
||||
- [ ] Check certificate validity dates
|
||||
- [ ] Ensure certificate matches requested domain
|
||||
|
||||
#### 6. Update Certificate Renewal Logic
|
||||
**Location**: `checkAndRenewCertificates()`
|
||||
|
||||
- [ ] Ensure renewal checks work for both ACME and custom certificates
|
||||
- [ ] Custom certificates should go through the same `provisionAcmeCertificate()` path
|
||||
- [ ] The existing renewal logic already calls `provisionCertificate()` which will use our modified flow
|
||||
|
||||
```typescript
|
||||
// No changes needed here - the existing renewal logic will automatically
|
||||
// use the custom provision function when calling provisionCertificate()
|
||||
private async checkAndRenewCertificates(): Promise<void> {
|
||||
// Existing code already handles this correctly
|
||||
for (const route of routes) {
|
||||
if (this.shouldRenewCertificate(cert, renewThreshold)) {
|
||||
// This will call provisionCertificate -> provisionAcmeCertificate
|
||||
// which now includes our custom function check
|
||||
await this.provisionCertificate(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 7. Add Integration Tests
|
||||
**File**: `test/test.certificate-provision.ts`
|
||||
|
||||
- [ ] Test custom certificate provision
|
||||
- [ ] Test fallback to Let's Encrypt ('http01' return)
|
||||
- [ ] Test error handling
|
||||
- [ ] Test renewal with custom function
|
||||
|
||||
#### 8. Update Documentation
|
||||
**Files**:
|
||||
- [ ] Update interface documentation
|
||||
- [ ] Add examples to README
|
||||
- [ ] Document ICert structure requirements
|
||||
|
||||
### API Design
|
||||
|
||||
```typescript
|
||||
// Example usage
|
||||
const proxy = new SmartProxy({
|
||||
certProvisionFunction: async (domain: string) => {
|
||||
// Option 1: Return custom certificate
|
||||
const customCert = await myCustomCA.generateCert(domain);
|
||||
return {
|
||||
cert: customCert.certificate,
|
||||
key: customCert.privateKey,
|
||||
ca: customCert.chain
|
||||
};
|
||||
|
||||
// Option 2: Use Let's Encrypt for certain domains
|
||||
if (domain.endsWith('.internal')) {
|
||||
return customCert;
|
||||
}
|
||||
return 'http01'; // Fallback to Let's Encrypt
|
||||
},
|
||||
certProvisionFallbackToAcme: true, // Default: true
|
||||
routes: [...]
|
||||
});
|
||||
{
|
||||
match: { domains: ['api.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: { host: 'backend', port: 8081 },
|
||||
tls: { mode: 'passthrough' }
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Configuration Options to Add
|
||||
|
||||
### After (Enhanced)
|
||||
```typescript
|
||||
interface ISmartProxyOptions {
|
||||
// Existing options...
|
||||
|
||||
// Custom certificate provision function
|
||||
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
||||
|
||||
// Whether to fallback to ACME if custom provision fails
|
||||
certProvisionFallbackToAcme?: boolean; // Default: true
|
||||
// Single route with multiple targets
|
||||
{
|
||||
match: { domains: ['api.example.com'], ports: [80, 443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [
|
||||
{
|
||||
match: { ports: [80] },
|
||||
host: 'backend',
|
||||
port: 8080,
|
||||
tls: { mode: 'terminate' }
|
||||
},
|
||||
{
|
||||
match: { ports: [443] },
|
||||
host: 'backend',
|
||||
port: 8081,
|
||||
tls: { mode: 'passthrough' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling Strategy
|
||||
### Advanced Example
|
||||
```typescript
|
||||
{
|
||||
match: { domains: ['app.example.com'], ports: [443] },
|
||||
action: {
|
||||
type: 'forward',
|
||||
tls: { mode: 'terminate', certificate: 'auto' }, // Route-level default
|
||||
websocket: { enabled: true }, // Route-level default
|
||||
targets: [
|
||||
{
|
||||
match: { path: '/api/v2/*' },
|
||||
host: 'api-v2',
|
||||
port: 8082,
|
||||
priority: 10
|
||||
},
|
||||
{
|
||||
match: { path: '/api/*', headers: { 'X-Version': 'v1' } },
|
||||
host: 'api-v1',
|
||||
port: 8081,
|
||||
priority: 5
|
||||
},
|
||||
{
|
||||
match: { path: '/ws/*' },
|
||||
host: 'websocket-server',
|
||||
port: 8090,
|
||||
websocket: {
|
||||
enabled: true,
|
||||
rewritePath: '/' // Strip /ws prefix
|
||||
}
|
||||
},
|
||||
{
|
||||
// Default target (no match property)
|
||||
host: 'web-backend',
|
||||
port: 8080
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. **Custom Function Errors**:
|
||||
- Log detailed error with domain context
|
||||
- Option A: Fallback to Let's Encrypt (safer)
|
||||
- Option B: Fail certificate provisioning (stricter)
|
||||
- Make this configurable via option?
|
||||
## Benefits
|
||||
1. **DRY Configuration**: No need to duplicate common settings across routes
|
||||
2. **Flexibility**: Different backends for different ports/paths within same domain
|
||||
3. **Clarity**: All routing for a domain in one place
|
||||
4. **Performance**: Single route lookup instead of multiple
|
||||
5. **Backwards Compatible**: Can migrate gradually
|
||||
|
||||
2. **Invalid Certificate Returns**:
|
||||
- Validate certificate structure
|
||||
- Check expiry dates
|
||||
- Verify domain match
|
||||
|
||||
### Testing Plan
|
||||
|
||||
1. **Unit Tests**:
|
||||
- Mock certProvisionFunction returns
|
||||
- Test validation logic
|
||||
- Test error scenarios
|
||||
|
||||
2. **Integration Tests**:
|
||||
- Real certificate generation
|
||||
- Renewal cycle testing
|
||||
- Mixed custom/Let's Encrypt scenarios
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- If no `certProvisionFunction` provided, behavior unchanged
|
||||
- Existing routes with 'auto' certificates continue using Let's Encrypt
|
||||
- No breaking changes to existing API
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
1. **Per-Route Custom Functions**:
|
||||
- Allow different provision functions per route
|
||||
- Override global function at route level
|
||||
|
||||
2. **Certificate Events**:
|
||||
- Emit events for custom cert provisioning
|
||||
- Allow monitoring/logging hooks
|
||||
|
||||
3. **Async Certificate Updates**:
|
||||
- Support updating certificates outside renewal cycle
|
||||
- Hot-reload certificates without restart
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
1. **Certificate Status Tracking**:
|
||||
- The `updateCertStatus()` method needs to support a new type: 'custom'
|
||||
- Current types are 'acme' and 'static'
|
||||
- This helps distinguish custom certificates in monitoring/logs
|
||||
|
||||
2. **Certificate Store Integration**:
|
||||
- Custom certificates are stored the same way as ACME certificates
|
||||
- They participate in the same renewal cycle
|
||||
- The store handles persistence across restarts
|
||||
|
||||
3. **Existing Methods to Reuse**:
|
||||
- `applyCertificate()` - Already handles applying certs to routes
|
||||
- `isCertificateValid()` - Can validate custom certificates
|
||||
- `certStore.saveCertificate()` - Handles storage
|
||||
|
||||
### Implementation Priority
|
||||
|
||||
1. Core functionality (steps 1-3)
|
||||
2. Type safety and validation (step 5)
|
||||
3. Renewal support (step 6)
|
||||
4. Tests (step 7)
|
||||
5. Documentation (step 8)
|
||||
|
||||
### Estimated Effort
|
||||
|
||||
- Core implementation: 4-6 hours
|
||||
- Testing: 2-3 hours
|
||||
- Documentation: 1 hour
|
||||
- Total: ~8-10 hours
|
||||
## Migration Strategy
|
||||
1. Keep support for `target` temporarily with deprecation warning
|
||||
2. Auto-convert `target` to `targets: [target]` internally
|
||||
3. Update documentation with migration examples
|
||||
4. Remove `target` support in next major version
|
Reference in New Issue
Block a user