Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c84947068c | ||
|
26f7431111 | ||
|
aa6ddbc4a6 | ||
|
6aa5f415c1 | ||
|
b26abbfd87 | ||
|
82df9a6f52 |
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"expiryDate": "2025-10-01T02:31:27.435Z",
|
"expiryDate": "2025-10-18T13:15:48.916Z",
|
||||||
"issueDate": "2025-07-03T02:31:27.435Z",
|
"issueDate": "2025-07-20T13:15:48.916Z",
|
||||||
"savedAt": "2025-07-03T02:31:27.435Z"
|
"savedAt": "2025-07-20T13:15:48.916Z"
|
||||||
}
|
}
|
25
changelog.md
25
changelog.md
@@ -1,5 +1,30 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-07-22 - 21.0.0 - BREAKING_CHANGE(forwarding)
|
||||||
|
Remove legacy forwarding module
|
||||||
|
|
||||||
|
- Removed the `forwarding` namespace export from main index
|
||||||
|
- Removed TForwardingType and all forwarding handlers
|
||||||
|
- Consolidated route helper functions into route-helpers.ts
|
||||||
|
- All functionality is now available through the route-based system
|
||||||
|
- MIGRATION: Replace `import { forwarding } from '@push.rocks/smartproxy'` with direct imports of route helpers
|
||||||
|
|
||||||
|
## 2025-07-21 - 20.0.2 - fix(docs)
|
||||||
|
Update documentation to improve clarity
|
||||||
|
|
||||||
|
- Enhanced readme with clearer breaking change warning for v20.0.0
|
||||||
|
- Fixed example email address from ssl@bleu.de to ssl@example.com
|
||||||
|
- Added load balancing and failover features to feature list
|
||||||
|
- Improved documentation structure and examples
|
||||||
|
|
||||||
|
## 2025-07-20 - 20.0.1 - BREAKING_CHANGE(routing)
|
||||||
|
Refactor route configuration to support multiple targets
|
||||||
|
|
||||||
|
- Changed route action configuration from single `target` to `targets` array
|
||||||
|
- Enables load balancing and failover capabilities with multiple upstream targets
|
||||||
|
- Updated all test files to use new `targets` array syntax
|
||||||
|
- Automatic certificate metadata refresh
|
||||||
|
|
||||||
## 2025-06-01 - 19.5.19 - fix(smartproxy)
|
## 2025-06-01 - 19.5.19 - fix(smartproxy)
|
||||||
Fix connection handling and improve route matching edge cases
|
Fix connection handling and improve route matching edge cases
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "19.6.17",
|
"version": "21.0.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -51,7 +51,8 @@
|
|||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
"npmextra.json",
|
||||||
"readme.md"
|
"readme.md",
|
||||||
|
"changelog.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"last 1 chrome versions"
|
"last 1 chrome versions"
|
||||||
|
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
|
## Key Changes
|
||||||
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
|
### 1. Update Route Target Interface
|
||||||
1. Add `certProvisionFunction` support to CertificateManager
|
- Add `match` property to `IRouteTarget` for sub-matching within routes
|
||||||
2. Modify `provisionAcmeCertificate()` to check custom function first
|
- Add target-specific override properties (tls, websocket, loadBalancing, etc.)
|
||||||
3. Add certificate expiry parsing for custom certificates
|
- Add priority field for controlling match order
|
||||||
4. Support both initial provisioning and renewal
|
|
||||||
5. Add fallback configuration option
|
|
||||||
|
|
||||||
### Overview
|
### 2. Update Route Action Interface
|
||||||
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.
|
- Remove singular `target` property
|
||||||
|
- Use only `targets` array (single target = array with one element)
|
||||||
|
- Maintain backwards compatibility during migration
|
||||||
|
|
||||||
### Requirements
|
### 3. Implementation Steps
|
||||||
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
|
|
||||||
|
|
||||||
### Implementation Steps
|
#### Phase 1: Type Updates
|
||||||
|
- [x] Update `IRouteTarget` interface in `route-types.ts`
|
||||||
|
- Add `match?: ITargetMatch` property
|
||||||
|
- Add override properties (tls, websocket, etc.)
|
||||||
|
- Add `priority?: number` field
|
||||||
|
- [x] Create `ITargetMatch` interface for sub-matching criteria
|
||||||
|
- [x] Update `IRouteAction` to use only `targets: IRouteTarget[]`
|
||||||
|
|
||||||
#### 1. Update Certificate Manager to Support Custom Provision Function
|
#### Phase 2: Route Resolution Logic
|
||||||
**File**: `ts/proxies/smart-proxy/certificate-manager.ts`
|
- [x] Update route matching logic to handle multiple targets
|
||||||
|
- [x] 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
|
||||||
|
- [x] Ensure target-specific settings override route-level settings
|
||||||
|
|
||||||
- [ ] Add `certProvisionFunction` property to CertificateManager class
|
#### Phase 3: Code Migration
|
||||||
- [ ] Pass the function from SmartProxy options during initialization
|
- [x] Find all occurrences of `action.target` and update to use `action.targets`
|
||||||
- [ ] Modify `provisionCertificate()` method to check for custom function first
|
- [x] Update route helpers and utilities
|
||||||
|
- [x] Update certificate manager to handle multiple targets
|
||||||
|
- [x] Update connection handlers
|
||||||
|
|
||||||
#### 2. Implement Custom Certificate Provisioning Logic
|
#### Phase 4: Testing
|
||||||
**Location**: Modify `provisionAcmeCertificate()` method
|
- [x] 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
|
```typescript
|
||||||
private async provisionAcmeCertificate(
|
// Need separate routes for different ports/paths
|
||||||
route: IRouteConfig,
|
[
|
||||||
domains: string[]
|
{
|
||||||
): Promise<void> {
|
match: { domains: ['api.example.com'], ports: [80] },
|
||||||
const primaryDomain = domains[0];
|
action: {
|
||||||
const routeName = route.name || primaryDomain;
|
type: 'forward',
|
||||||
|
target: { host: 'backend', port: 8080 },
|
||||||
// Check for custom provision function first
|
tls: { mode: 'terminate' }
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
```typescript
|
||||||
interface ISmartProxyOptions {
|
// Single route with multiple targets
|
||||||
// Existing options...
|
{
|
||||||
|
match: { domains: ['api.example.com'], ports: [80, 443] },
|
||||||
// Custom certificate provision function
|
action: {
|
||||||
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
type: 'forward',
|
||||||
|
targets: [
|
||||||
// Whether to fallback to ACME if custom provision fails
|
{
|
||||||
certProvisionFallbackToAcme?: boolean; // Default: true
|
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**:
|
## Benefits
|
||||||
- Log detailed error with domain context
|
1. **DRY Configuration**: No need to duplicate common settings across routes
|
||||||
- Option A: Fallback to Let's Encrypt (safer)
|
2. **Flexibility**: Different backends for different ports/paths within same domain
|
||||||
- Option B: Fail certificate provisioning (stricter)
|
3. **Clarity**: All routing for a domain in one place
|
||||||
- Make this configurable via option?
|
4. **Performance**: Single route lookup instead of multiple
|
||||||
|
5. **Backwards Compatible**: Can migrate gradually
|
||||||
|
|
||||||
2. **Invalid Certificate Returns**:
|
## Migration Strategy
|
||||||
- Validate certificate structure
|
1. Keep support for `target` temporarily with deprecation warning
|
||||||
- Check expiry dates
|
2. Auto-convert `target` to `targets: [target]` internally
|
||||||
- Verify domain match
|
3. Update documentation with migration examples
|
||||||
|
4. Remove `target` support in next major version
|
||||||
### 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
|
|
@@ -32,14 +32,14 @@ tap.test('PathMatcher - wildcard matching', async () => {
|
|||||||
const result = PathMatcher.match('/api/*', '/api/users/123/profile');
|
const result = PathMatcher.match('/api/*', '/api/users/123/profile');
|
||||||
expect(result.matches).toEqual(true);
|
expect(result.matches).toEqual(true);
|
||||||
expect(result.pathMatch).toEqual('/api'); // Normalized without trailing slash
|
expect(result.pathMatch).toEqual('/api'); // Normalized without trailing slash
|
||||||
expect(result.pathRemainder).toEqual('users/123/profile');
|
expect(result.pathRemainder).toEqual('/users/123/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('PathMatcher - mixed parameters and wildcards', async () => {
|
tap.test('PathMatcher - mixed parameters and wildcards', async () => {
|
||||||
const result = PathMatcher.match('/api/:version/*', '/api/v1/users/123');
|
const result = PathMatcher.match('/api/:version/*', '/api/v1/users/123');
|
||||||
expect(result.matches).toEqual(true);
|
expect(result.matches).toEqual(true);
|
||||||
expect(result.params).toEqual({ version: 'v1' });
|
expect(result.params).toEqual({ version: 'v1' });
|
||||||
expect(result.pathRemainder).toEqual('users/123');
|
expect(result.pathRemainder).toEqual('/users/123');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('PathMatcher - trailing slash normalization', async () => {
|
tap.test('PathMatcher - trailing slash normalization', async () => {
|
||||||
|
@@ -58,7 +58,7 @@ tap.test('Shared Security Manager', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'target.com', port: 443 }
|
targets: [{ host: 'target.com', port: 443 }]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
ipAllowList: ['10.0.0.*', '192.168.1.*'],
|
ipAllowList: ['10.0.0.*', '192.168.1.*'],
|
||||||
@@ -113,7 +113,7 @@ tap.test('Shared Security Manager', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'target.com', port: 443 }
|
targets: [{ host: 'target.com', port: 443 }]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
|
@@ -59,7 +59,7 @@ tap.test('should create ACME challenge route', async (tools) => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: 8080 }
|
targets: [{ host: 'localhost', port: 8080 }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
challengeRoute
|
challengeRoute
|
||||||
|
@@ -18,7 +18,7 @@ tap.test('should defer certificate provisioning until ports are ready', async (t
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 },
|
targets: [{ host: 'localhost', port: 8181 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -30,7 +30,7 @@ tap.test('should defer certificate provisioning until after ports are listening'
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 },
|
targets: [{ host: 'localhost', port: 8181 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
@@ -126,7 +126,7 @@ tap.test('should have ACME challenge route ready before certificate provisioning
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 },
|
targets: [{ host: 'localhost', port: 8181 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
|
@@ -16,10 +16,10 @@ tap.test('SmartCertManager should call getCertificateForDomain with wildcard opt
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -59,10 +59,10 @@ tap.test('SmartProxy should support custom certificate provision function', asyn
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
@@ -109,10 +109,10 @@ tap.test('Custom certificate provision function should be called', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
@@ -172,10 +172,10 @@ tap.test('Should fallback to ACME when custom provision fails', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
@@ -231,10 +231,10 @@ tap.test('Should not fallback when certProvisionFallbackToAcme is false', async
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
@@ -310,10 +310,10 @@ tap.test('Should return http01 for unknown domains', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
|
@@ -7,7 +7,7 @@ const testProxy = new SmartProxy({
|
|||||||
match: { ports: 9443, domains: 'test.local' },
|
match: { ports: 9443, domains: 'test.local' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 },
|
targets: [{ host: 'localhost', port: 8080 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
@@ -67,7 +67,7 @@ tap.test('should handle static certificates', async () => {
|
|||||||
match: { ports: 9444, domains: 'static.example.com' },
|
match: { ports: 9444, domains: 'static.example.com' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 },
|
targets: [{ host: 'localhost', port: 8080 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: {
|
certificate: {
|
||||||
@@ -96,7 +96,7 @@ tap.test('should handle ACME challenge routes', async () => {
|
|||||||
match: { ports: 9445, domains: 'acme.local' },
|
match: { ports: 9445, domains: 'acme.local' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 },
|
targets: [{ host: 'localhost', port: 8080 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
@@ -112,7 +112,7 @@ tap.test('should handle ACME challenge routes', async () => {
|
|||||||
match: { ports: 9081, domains: 'acme.local' },
|
match: { ports: 9081, domains: 'acme.local' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 }
|
targets: [{ host: 'localhost', port: 8080 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
acme: {
|
acme: {
|
||||||
@@ -167,7 +167,7 @@ tap.test('should renew certificates', async () => {
|
|||||||
match: { ports: 9446, domains: 'renew.local' },
|
match: { ports: 9446, domains: 'renew.local' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 },
|
targets: [{ host: 'localhost', port: 8080 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -8,7 +8,7 @@ tap.test('should create SmartProxy with certificate routes', async () => {
|
|||||||
match: { ports: 8443, domains: 'test.example.com' },
|
match: { ports: 8443, domains: 'test.example.com' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 },
|
targets: [{ host: 'localhost', port: 8080 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -13,7 +13,7 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
|
|||||||
match: { ports: 8588 },
|
match: { ports: 8588 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9996 }
|
targets: [{ host: 'localhost', port: 9996 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
enableDetailedLogging: false,
|
enableDetailedLogging: false,
|
||||||
|
@@ -18,10 +18,10 @@ tap.test('should handle clients that connect and immediately disconnect without
|
|||||||
match: { ports: 8560 },
|
match: { ports: 8560 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent port
|
port: 9999 // Non-existent port
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -173,10 +173,10 @@ tap.test('should handle clients that error during connection', async () => {
|
|||||||
match: { ports: 8561 },
|
match: { ports: 8561 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999
|
port: 9999
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -20,10 +20,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
|
|||||||
match: { ports: 8570 },
|
match: { ports: 8570 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent port
|
port: 9999 // Non-existent port
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,10 +31,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
|
|||||||
match: { ports: 8571 },
|
match: { ports: 8571 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent port
|
port: 9999 // Non-existent port
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough'
|
mode: 'passthrough'
|
||||||
}
|
}
|
||||||
@@ -215,10 +215,10 @@ tap.test('comprehensive connection cleanup test - all scenarios', async () => {
|
|||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
forwardingEngine: 'nftables',
|
forwardingEngine: 'nftables',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999
|
port: 9999
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -65,10 +65,10 @@ tap.test('should forward TCP connections correctly', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 7001,
|
port: 7001,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -118,10 +118,10 @@ tap.test('should handle TLS passthrough correctly', async () => {
|
|||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough',
|
mode: 'passthrough',
|
||||||
},
|
},
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 7002,
|
port: 7002,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -179,10 +179,10 @@ tap.test('should handle SNI-based forwarding', async () => {
|
|||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough',
|
mode: 'passthrough',
|
||||||
},
|
},
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 7002,
|
port: 7002,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -197,10 +197,10 @@ tap.test('should handle SNI-based forwarding', async () => {
|
|||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough',
|
mode: 'passthrough',
|
||||||
},
|
},
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 7002,
|
port: 7002,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -90,10 +90,10 @@ tap.test('Setup test environment', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
maxConnections: 5 // Low limit for testing
|
maxConnections: 5 // Low limit for testing
|
||||||
@@ -198,10 +198,10 @@ tap.test('HttpProxy per-IP validation', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate'
|
mode: 'terminate'
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ tap.test('should verify certificate manager callback is preserved on updateRoute
|
|||||||
match: { ports: [18443], domains: ['test.local'] },
|
match: { ports: [18443], domains: ['test.local'] },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 3000 },
|
targets: [{ host: 'localhost', port: 3000 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
@@ -63,7 +63,7 @@ tap.test('should verify certificate manager callback is preserved on updateRoute
|
|||||||
match: { ports: [18444], domains: ['test2.local'] },
|
match: { ports: [18444], domains: ['test2.local'] },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 3001 },
|
targets: [{ host: 'localhost', port: 3001 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -37,7 +37,7 @@ tap.test('regular forward route should work correctly', async () => {
|
|||||||
match: { ports: 7890 },
|
match: { ports: 7890 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 6789 }
|
targets: [{ host: 'localhost', port: 6789 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -106,7 +106,7 @@ tap.skip.test('NFTables forward route should not terminate connections (requires
|
|||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
forwardingEngine: 'nftables',
|
forwardingEngine: 'nftables',
|
||||||
target: { host: 'localhost', port: 6789 }
|
targets: [{ host: 'localhost', port: 6789 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -39,10 +39,10 @@ tap.test('forward connections should not be immediately closed', async (t) => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 9090,
|
port: 9090,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
|
||||||
|
|
||||||
// First, import the components directly to avoid issues with compiled modules
|
|
||||||
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
|
||||||
// Import route-based helpers
|
// Import route-based helpers
|
||||||
import {
|
import {
|
||||||
createHttpRoute,
|
createHttpRoute,
|
||||||
@@ -39,7 +36,7 @@ tap.test('Route Helpers - Create HTTP routes', async () => {
|
|||||||
const route = helpers.httpOnly('example.com', { host: 'localhost', port: 3000 });
|
const route = helpers.httpOnly('example.com', { host: 'localhost', port: 3000 });
|
||||||
expect(route.action.type).toEqual('forward');
|
expect(route.action.type).toEqual('forward');
|
||||||
expect(route.match.domains).toEqual('example.com');
|
expect(route.match.domains).toEqual('example.com');
|
||||||
expect(route.action.target).toEqual({ host: 'localhost', port: 3000 });
|
expect(route.action.targets?.[0]).toEqual({ host: 'localhost', port: 3000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Route Helpers - Create HTTPS terminate to HTTP routes', async () => {
|
tap.test('Route Helpers - Create HTTPS terminate to HTTP routes', async () => {
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
||||||
import * as plugins from '../ts/plugins.js';
|
|
||||||
|
|
||||||
// First, import the components directly to avoid issues with compiled modules
|
|
||||||
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
|
||||||
// Import route-based helpers from the correct location
|
|
||||||
import {
|
|
||||||
createHttpRoute,
|
|
||||||
createHttpsTerminateRoute,
|
|
||||||
createHttpsPassthroughRoute,
|
|
||||||
createHttpToHttpsRedirect,
|
|
||||||
createCompleteHttpsServer,
|
|
||||||
createLoadBalancerRoute
|
|
||||||
} from '../ts/proxies/smart-proxy/utils/route-patterns.js';
|
|
||||||
|
|
||||||
// Create helper functions for building forwarding configs
|
|
||||||
const helpers = {
|
|
||||||
httpOnly: () => ({ type: 'http-only' as const }),
|
|
||||||
tlsTerminateToHttp: () => ({ type: 'https-terminate-to-http' as const }),
|
|
||||||
tlsTerminateToHttps: () => ({ type: 'https-terminate-to-https' as const }),
|
|
||||||
httpsPassthrough: () => ({ type: 'https-passthrough' as const })
|
|
||||||
};
|
|
||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
|
||||||
// HTTP-only defaults
|
|
||||||
const httpConfig = {
|
|
||||||
type: 'http-only' as const,
|
|
||||||
target: { host: 'localhost', port: 3000 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const httpWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpConfig);
|
|
||||||
|
|
||||||
expect(httpWithDefaults.port).toEqual(80);
|
|
||||||
expect(httpWithDefaults.socket).toEqual('/tmp/forwarding-http-only-80.sock');
|
|
||||||
|
|
||||||
// HTTPS passthrough defaults
|
|
||||||
const httpsPassthroughConfig = {
|
|
||||||
type: 'https-passthrough' as const,
|
|
||||||
target: { host: 'localhost', port: 443 }
|
|
||||||
};
|
|
||||||
|
|
||||||
const httpsPassthroughWithDefaults = ForwardingHandlerFactory['applyDefaults'](httpsPassthroughConfig);
|
|
||||||
|
|
||||||
expect(httpsPassthroughWithDefaults.port).toEqual(443);
|
|
||||||
expect(httpsPassthroughWithDefaults.socket).toEqual('/tmp/forwarding-https-passthrough-443.sock');
|
|
||||||
});
|
|
||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - factory function for handlers', async () => {
|
|
||||||
// @todo Implement unit tests for ForwardingHandlerFactory
|
|
||||||
// These tests would need proper mocking of the handlers
|
|
||||||
});
|
|
||||||
|
|
||||||
export default tap.start();
|
|
@@ -20,7 +20,7 @@ tap.test('should forward non-TLS connections on HttpProxy ports', async (tapTest
|
|||||||
match: { ports: testPort },
|
match: { ports: testPort },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 }
|
targets: [{ host: 'localhost', port: 8181 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -81,7 +81,7 @@ tap.test('should use direct connection for non-HttpProxy ports', async (tapTest)
|
|||||||
match: { ports: 8080 }, // Not in useHttpProxy
|
match: { ports: 8080 }, // Not in useHttpProxy
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 }
|
targets: [{ host: 'localhost', port: 8181 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -142,7 +142,7 @@ tap.test('should handle ACME HTTP-01 challenges on port 80 with HttpProxy', asyn
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8080 }
|
targets: [{ host: 'localhost', port: 8080 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
@@ -14,7 +14,7 @@ tap.test('should detect and forward non-TLS connections on useHttpProxy ports',
|
|||||||
match: { ports: 8080 },
|
match: { ports: 8080 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 }
|
targets: [{ host: 'localhost', port: 8181 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -140,7 +140,7 @@ tap.test('should handle TLS connections normally', async (tapTest) => {
|
|||||||
match: { ports: 443 },
|
match: { ports: 443 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8443 },
|
targets: [{ host: 'localhost', port: 8443 }],
|
||||||
tls: { mode: 'terminate' }
|
tls: { mode: 'terminate' }
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
@@ -17,7 +17,7 @@ tap.test('should detect and forward non-TLS connections on HttpProxy ports', asy
|
|||||||
match: { ports: 8081 },
|
match: { ports: 8081 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8181 }
|
targets: [{ host: 'localhost', port: 8181 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -120,7 +120,7 @@ tap.test('should properly detect non-TLS connections on HttpProxy ports', async
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: targetPort }
|
targets: [{ host: 'localhost', port: targetPort }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -42,7 +42,7 @@ tap.test('should forward HTTP connections on port 8080', async (tapTest) => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: targetPort }
|
targets: [{ host: 'localhost', port: targetPort }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -131,7 +131,7 @@ tap.test('should handle basic HTTP request forwarding', async (tapTest) => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: targetPort }
|
targets: [{ host: 'localhost', port: targetPort }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -67,7 +67,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: targetPort },
|
targets: [{ host: 'localhost', port: targetPort }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto' // Use ACME for certificate
|
certificate: 'auto' // Use ACME for certificate
|
||||||
@@ -83,7 +83,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: targetPort }
|
targets: [{ host: 'localhost', port: targetPort }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -191,7 +191,7 @@ tap.test('should handle ACME challenges on port 8080 with improved port binding
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: targetPort }
|
targets: [{ host: 'localhost', port: targetPort }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@@ -95,10 +95,10 @@ tap.test('should support static host/port routes', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: serverPort
|
port: serverPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -135,13 +135,13 @@ tap.test('should support function-based host', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: (context: IRouteContext) => {
|
host: (context: IRouteContext) => {
|
||||||
// Return localhost always in this test
|
// Return localhost always in this test
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
},
|
},
|
||||||
port: serverPort
|
port: serverPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -178,13 +178,13 @@ tap.test('should support function-based port', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: (context: IRouteContext) => {
|
port: (context: IRouteContext) => {
|
||||||
// Return test server port
|
// Return test server port
|
||||||
return serverPort;
|
return serverPort;
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -221,14 +221,14 @@ tap.test('should support function-based host AND port', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: (context: IRouteContext) => {
|
host: (context: IRouteContext) => {
|
||||||
return 'localhost';
|
return 'localhost';
|
||||||
},
|
},
|
||||||
port: (context: IRouteContext) => {
|
port: (context: IRouteContext) => {
|
||||||
return serverPort;
|
return serverPort;
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -265,7 +265,7 @@ tap.test('should support context-based routing with path', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: (context: IRouteContext) => {
|
host: (context: IRouteContext) => {
|
||||||
// Use path to determine host
|
// Use path to determine host
|
||||||
if (context.path?.startsWith('/api')) {
|
if (context.path?.startsWith('/api')) {
|
||||||
@@ -275,7 +275,7 @@ tap.test('should support context-based routing with path', async () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
port: serverPort
|
port: serverPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@@ -232,10 +232,10 @@ tap.test('should start the proxy server', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3100
|
port: 3100
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate'
|
mode: 'terminate'
|
||||||
},
|
},
|
||||||
|
@@ -40,7 +40,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
|
|||||||
match: { ports: 8590 },
|
match: { ports: 8590 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9998 }
|
targets: [{ host: 'localhost', port: 9998 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
@@ -117,7 +117,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
|
|||||||
match: { ports: 8591 },
|
match: { ports: 8591 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9998 }
|
targets: [{ host: 'localhost', port: 9998 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
@@ -178,7 +178,7 @@ tap.test('keepalive support - verify keepalive connections are properly handled'
|
|||||||
match: { ports: 8592 },
|
match: { ports: 8592 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9998 }
|
targets: [{ host: 'localhost', port: 9998 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@@ -39,10 +39,10 @@ tap.test('setup test environment', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9876
|
port: 9876
|
||||||
}
|
}]
|
||||||
// No TLS configuration - just plain TCP forwarding
|
// No TLS configuration - just plain TCP forwarding
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
|
@@ -29,7 +29,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => {
|
|||||||
match: { ports: 8700 },
|
match: { ports: 8700 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9995 }
|
targets: [{ host: 'localhost', port: 9995 }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -37,7 +37,7 @@ tap.test('MetricsCollector provides accurate metrics', async (tools) => {
|
|||||||
match: { ports: 8701 },
|
match: { ports: 8701 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9995 }
|
targets: [{ host: 'localhost', port: 9995 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@@ -36,10 +36,10 @@ tap.test('should create SmartProxy instance with new metrics', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: echoServerPort
|
port: echoServerPort
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough'
|
mode: 'passthrough'
|
||||||
}
|
}
|
||||||
|
@@ -34,10 +34,10 @@ tap.skip.test('NFTables forwarding should not terminate connections (requires ro
|
|||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
forwardingEngine: 'nftables',
|
forwardingEngine: 'nftables',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8001,
|
port: 8001,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Also add regular forwarding route for comparison
|
// Also add regular forwarding route for comparison
|
||||||
@@ -49,10 +49,10 @@ tap.skip.test('NFTables forwarding should not terminate connections (requires ro
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8001,
|
port: 8001,
|
||||||
},
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@@ -42,10 +42,10 @@ const sampleRoute: IRouteConfig = {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8000
|
port: 8000
|
||||||
},
|
}],
|
||||||
forwardingEngine: 'nftables',
|
forwardingEngine: 'nftables',
|
||||||
nftables: {
|
nftables: {
|
||||||
protocol: 'tcp',
|
protocol: 'tcp',
|
||||||
@@ -115,10 +115,10 @@ tap.skip.test('NFTablesManager route updating test', async () => {
|
|||||||
...sampleRoute,
|
...sampleRoute,
|
||||||
action: {
|
action: {
|
||||||
...sampleRoute.action,
|
...sampleRoute.action,
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9000 // Different port
|
port: 9000 // Different port
|
||||||
},
|
}],
|
||||||
nftables: {
|
nftables: {
|
||||||
...sampleRoute.action.nftables,
|
...sampleRoute.action.nftables,
|
||||||
protocol: 'all' // Different protocol
|
protocol: 'all' // Different protocol
|
||||||
@@ -147,10 +147,10 @@ tap.skip.test('NFTablesManager route deprovisioning test', async () => {
|
|||||||
...sampleRoute,
|
...sampleRoute,
|
||||||
action: {
|
action: {
|
||||||
...sampleRoute.action,
|
...sampleRoute.action,
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9000 // Different port from original test
|
port: 9000 // Different port from original test
|
||||||
},
|
}],
|
||||||
nftables: {
|
nftables: {
|
||||||
...sampleRoute.action.nftables,
|
...sampleRoute.action.nftables,
|
||||||
protocol: 'all' // Different protocol from original test
|
protocol: 'all' // Different protocol from original test
|
||||||
|
@@ -91,7 +91,7 @@ testFn('SmartProxy getNfTablesStatus functionality', async () => {
|
|||||||
match: { ports: 3004 },
|
match: { ports: 3004 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 3005 }
|
targets: [{ host: 'localhost', port: 3005 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -29,7 +29,7 @@ tap.test('port forwarding should not immediately close connections', async (tool
|
|||||||
match: { ports: 9999 },
|
match: { ports: 9999 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 8888 }
|
targets: [{ host: 'localhost', port: 8888 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -63,7 +63,7 @@ tap.test('TLS passthrough should work correctly', async () => {
|
|||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
tls: { mode: 'passthrough' },
|
tls: { mode: 'passthrough' },
|
||||||
target: { host: 'localhost', port: 443 }
|
targets: [{ host: 'localhost', port: 443 }]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -214,12 +214,12 @@ tap.test('should handle errors in port mapping functions', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: () => {
|
port: () => {
|
||||||
throw new Error('Test error in port mapping function');
|
throw new Error('Test error in port mapping function');
|
||||||
}
|
}
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
name: 'Error Route'
|
name: 'Error Route'
|
||||||
};
|
};
|
||||||
|
@@ -21,7 +21,7 @@ tap.test('should not double-register port 80 when user route and ACME use same p
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: 3000 }
|
targets: [{ host: 'localhost', port: 3000 }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -31,7 +31,7 @@ tap.test('should not double-register port 80 when user route and ACME use same p
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: 3001 },
|
targets: [{ host: 'localhost', port: 3001 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate' as const,
|
mode: 'terminate' as const,
|
||||||
certificate: 'auto' as const
|
certificate: 'auto' as const
|
||||||
@@ -153,7 +153,7 @@ tap.test('should handle ACME on different port than user routes', async (tools)
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: 3000 }
|
targets: [{ host: 'localhost', port: 3000 }]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -163,7 +163,7 @@ tap.test('should handle ACME on different port than user routes', async (tools)
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: { host: 'localhost', port: 3001 },
|
targets: [{ host: 'localhost', port: 3001 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate' as const,
|
mode: 'terminate' as const,
|
||||||
certificate: 'auto' as const
|
certificate: 'auto' as const
|
||||||
|
@@ -15,10 +15,10 @@ tap.test('setup two smartproxies in a chain configuration', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'httpbin.org',
|
host: 'httpbin.org',
|
||||||
port: 443
|
port: 443
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -45,10 +45,10 @@ tap.test('setup two smartproxies in a chain configuration', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8002
|
port: 8002
|
||||||
},
|
}],
|
||||||
sendProxyProtocol: true
|
sendProxyProtocol: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,10 +32,10 @@ tap.test('simple proxy chain test - identify connection accumulation', async ()
|
|||||||
match: { ports: 8591 },
|
match: { ports: 8591 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9998 // Backend that closes immediately
|
port: 9998 // Backend that closes immediately
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -50,10 +50,10 @@ tap.test('simple proxy chain test - identify connection accumulation', async ()
|
|||||||
match: { ports: 8590 },
|
match: { ports: 8590 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8591 // Forward to proxy2
|
port: 8591 // Forward to proxy2
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -19,10 +19,10 @@ tap.test('should handle proxy chaining without connection accumulation', async (
|
|||||||
match: { ports: 8581 },
|
match: { ports: 8581 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent backend
|
port: 9999 // Non-existent backend
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -37,10 +37,10 @@ tap.test('should handle proxy chaining without connection accumulation', async (
|
|||||||
match: { ports: 8580 },
|
match: { ports: 8580 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8581 // Forward to proxy2
|
port: 8581 // Forward to proxy2
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -270,10 +270,10 @@ tap.test('should handle proxy chain with HTTP traffic', async () => {
|
|||||||
match: { ports: 8583 },
|
match: { ports: 8583 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent backend
|
port: 9999 // Non-existent backend
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -289,10 +289,10 @@ tap.test('should handle proxy chain with HTTP traffic', async () => {
|
|||||||
match: { ports: 8582 },
|
match: { ports: 8582 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8583 // Forward to proxy2
|
port: 8583 // Forward to proxy2
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -19,10 +19,10 @@ tap.test('should handle rapid connection retries without leaking connections', a
|
|||||||
match: { ports: 8550 },
|
match: { ports: 8550 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9999 // Non-existent port to force connection failures
|
port: 9999 // Non-existent port to force connection failures
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -17,7 +17,7 @@ tap.test('should set update routes callback on certificate manager', async () =>
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 3000 },
|
targets: [{ host: 'localhost', port: 3000 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
@@ -95,7 +95,7 @@ tap.test('should set update routes callback on certificate manager', async () =>
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 3001 },
|
targets: [{ host: 'localhost', port: 3001 }],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -56,8 +56,8 @@ tap.test('Routes: Should create basic HTTP route', async () => {
|
|||||||
expect(httpRoute.match.ports).toEqual(80);
|
expect(httpRoute.match.ports).toEqual(80);
|
||||||
expect(httpRoute.match.domains).toEqual('example.com');
|
expect(httpRoute.match.domains).toEqual('example.com');
|
||||||
expect(httpRoute.action.type).toEqual('forward');
|
expect(httpRoute.action.type).toEqual('forward');
|
||||||
expect(httpRoute.action.target?.host).toEqual('localhost');
|
expect(httpRoute.action.targets?.[0]?.host).toEqual('localhost');
|
||||||
expect(httpRoute.action.target?.port).toEqual(3000);
|
expect(httpRoute.action.targets?.[0]?.port).toEqual(3000);
|
||||||
expect(httpRoute.name).toEqual('Basic HTTP Route');
|
expect(httpRoute.name).toEqual('Basic HTTP Route');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
|
|||||||
expect(httpsRoute.action.type).toEqual('forward');
|
expect(httpsRoute.action.type).toEqual('forward');
|
||||||
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
|
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
|
||||||
expect(httpsRoute.action.tls?.certificate).toEqual('auto');
|
expect(httpsRoute.action.tls?.certificate).toEqual('auto');
|
||||||
expect(httpsRoute.action.target?.host).toEqual('localhost');
|
expect(httpsRoute.action.targets?.[0]?.host).toEqual('localhost');
|
||||||
expect(httpsRoute.action.target?.port).toEqual(8080);
|
expect(httpsRoute.action.targets?.[0]?.port).toEqual(8080);
|
||||||
expect(httpsRoute.name).toEqual('HTTPS Route');
|
expect(httpsRoute.name).toEqual('HTTPS Route');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,10 +131,10 @@ tap.test('Routes: Should create load balancer route', async () => {
|
|||||||
// Validate the route configuration
|
// Validate the route configuration
|
||||||
expect(lbRoute.match.domains).toEqual('app.example.com');
|
expect(lbRoute.match.domains).toEqual('app.example.com');
|
||||||
expect(lbRoute.action.type).toEqual('forward');
|
expect(lbRoute.action.type).toEqual('forward');
|
||||||
expect(Array.isArray(lbRoute.action.target?.host)).toBeTrue();
|
expect(Array.isArray(lbRoute.action.targets?.[0]?.host)).toBeTrue();
|
||||||
expect((lbRoute.action.target?.host as string[]).length).toEqual(3);
|
expect((lbRoute.action.targets?.[0]?.host as string[]).length).toEqual(3);
|
||||||
expect((lbRoute.action.target?.host as string[])[0]).toEqual('10.0.0.1');
|
expect((lbRoute.action.targets?.[0]?.host as string[])[0]).toEqual('10.0.0.1');
|
||||||
expect(lbRoute.action.target?.port).toEqual(8080);
|
expect(lbRoute.action.targets?.[0]?.port).toEqual(8080);
|
||||||
expect(lbRoute.action.tls?.mode).toEqual('terminate');
|
expect(lbRoute.action.tls?.mode).toEqual('terminate');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,8 +152,8 @@ tap.test('Routes: Should create API route with CORS', async () => {
|
|||||||
expect(apiRoute.match.path).toEqual('/v1/*');
|
expect(apiRoute.match.path).toEqual('/v1/*');
|
||||||
expect(apiRoute.action.type).toEqual('forward');
|
expect(apiRoute.action.type).toEqual('forward');
|
||||||
expect(apiRoute.action.tls?.mode).toEqual('terminate');
|
expect(apiRoute.action.tls?.mode).toEqual('terminate');
|
||||||
expect(apiRoute.action.target?.host).toEqual('localhost');
|
expect(apiRoute.action.targets?.[0]?.host).toEqual('localhost');
|
||||||
expect(apiRoute.action.target?.port).toEqual(3000);
|
expect(apiRoute.action.targets?.[0]?.port).toEqual(3000);
|
||||||
|
|
||||||
// Check CORS headers
|
// Check CORS headers
|
||||||
expect(apiRoute.headers).toBeDefined();
|
expect(apiRoute.headers).toBeDefined();
|
||||||
@@ -177,8 +177,8 @@ tap.test('Routes: Should create WebSocket route', async () => {
|
|||||||
expect(wsRoute.match.path).toEqual('/socket');
|
expect(wsRoute.match.path).toEqual('/socket');
|
||||||
expect(wsRoute.action.type).toEqual('forward');
|
expect(wsRoute.action.type).toEqual('forward');
|
||||||
expect(wsRoute.action.tls?.mode).toEqual('terminate');
|
expect(wsRoute.action.tls?.mode).toEqual('terminate');
|
||||||
expect(wsRoute.action.target?.host).toEqual('localhost');
|
expect(wsRoute.action.targets?.[0]?.host).toEqual('localhost');
|
||||||
expect(wsRoute.action.target?.port).toEqual(5000);
|
expect(wsRoute.action.targets?.[0]?.port).toEqual(5000);
|
||||||
|
|
||||||
// Check WebSocket configuration
|
// Check WebSocket configuration
|
||||||
expect(wsRoute.action.websocket).toBeDefined();
|
expect(wsRoute.action.websocket).toBeDefined();
|
||||||
@@ -209,10 +209,10 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
defaults: {
|
defaults: {
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
security: {
|
security: {
|
||||||
ipAllowList: ['127.0.0.1', '192.168.0.*'],
|
ipAllowList: ['127.0.0.1', '192.168.0.*'],
|
||||||
maxConnections: 100
|
maxConnections: 100
|
||||||
@@ -294,13 +294,13 @@ tap.test('Edge Case - Wildcard Domains and Path Matching', async () => {
|
|||||||
const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
|
const bestMatch = findBestMatchingRoute(routes, { domain: 'api.example.com', path: '/api/users', port: 443 });
|
||||||
expect(bestMatch).not.toBeUndefined();
|
expect(bestMatch).not.toBeUndefined();
|
||||||
if (bestMatch) {
|
if (bestMatch) {
|
||||||
expect(bestMatch.action.target.port).toEqual(3001); // Should match the exact domain route
|
expect(bestMatch.action.targets[0].port).toEqual(3001); // Should match the exact domain route
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with a different subdomain - should only match the wildcard route
|
// Test with a different subdomain - should only match the wildcard route
|
||||||
const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 });
|
const otherMatches = findMatchingRoutes(routes, { domain: 'other.example.com', path: '/api/products', port: 443 });
|
||||||
expect(otherMatches.length).toEqual(1);
|
expect(otherMatches.length).toEqual(1);
|
||||||
expect(otherMatches[0].action.target.port).toEqual(3000); // Should match the wildcard domain route
|
expect(otherMatches[0].action.targets[0].port).toEqual(3000); // Should match the wildcard domain route
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Edge Case - Disabled Routes', async () => {
|
tap.test('Edge Case - Disabled Routes', async () => {
|
||||||
@@ -316,7 +316,7 @@ tap.test('Edge Case - Disabled Routes', async () => {
|
|||||||
|
|
||||||
// Should only find the enabled route
|
// Should only find the enabled route
|
||||||
expect(matches.length).toEqual(1);
|
expect(matches.length).toEqual(1);
|
||||||
expect(matches[0].action.target.port).toEqual(3000);
|
expect(matches[0].action.targets[0].port).toEqual(3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
||||||
@@ -333,10 +333,10 @@ tap.test('Edge Case - Complex Path and Headers Matching', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'internal-api',
|
host: 'internal-api',
|
||||||
port: 8080
|
port: 8080
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto'
|
certificate: 'auto'
|
||||||
@@ -376,10 +376,10 @@ tap.test('Edge Case - Port Range Matching', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'backend',
|
host: 'backend',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
name: 'Port Range Route'
|
name: 'Port Range Route'
|
||||||
};
|
};
|
||||||
@@ -404,10 +404,10 @@ tap.test('Edge Case - Port Range Matching', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'backend',
|
host: 'backend',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
name: 'Multi Range Route'
|
name: 'Multi Range Route'
|
||||||
};
|
};
|
||||||
@@ -452,7 +452,7 @@ tap.test('Wildcard Domain Handling', async () => {
|
|||||||
expect(bestSpecificMatch).not.toBeUndefined();
|
expect(bestSpecificMatch).not.toBeUndefined();
|
||||||
if (bestSpecificMatch) {
|
if (bestSpecificMatch) {
|
||||||
// Find which route was matched
|
// Find which route was matched
|
||||||
const matchedPort = bestSpecificMatch.action.target.port;
|
const matchedPort = bestSpecificMatch.action.targets[0].port;
|
||||||
console.log(`Matched route with port: ${matchedPort}`);
|
console.log(`Matched route with port: ${matchedPort}`);
|
||||||
|
|
||||||
// Verify it's the specific subdomain route (with highest priority)
|
// Verify it's the specific subdomain route (with highest priority)
|
||||||
@@ -465,7 +465,7 @@ tap.test('Wildcard Domain Handling', async () => {
|
|||||||
expect(bestWildcardMatch).not.toBeUndefined();
|
expect(bestWildcardMatch).not.toBeUndefined();
|
||||||
if (bestWildcardMatch) {
|
if (bestWildcardMatch) {
|
||||||
// Find which route was matched
|
// Find which route was matched
|
||||||
const matchedPort = bestWildcardMatch.action.target.port;
|
const matchedPort = bestWildcardMatch.action.targets[0].port;
|
||||||
console.log(`Matched route with port: ${matchedPort}`);
|
console.log(`Matched route with port: ${matchedPort}`);
|
||||||
|
|
||||||
// Verify it's the wildcard subdomain route (with medium priority)
|
// Verify it's the wildcard subdomain route (with medium priority)
|
||||||
@@ -513,7 +513,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
|||||||
expect(webServerMatch).not.toBeUndefined();
|
expect(webServerMatch).not.toBeUndefined();
|
||||||
if (webServerMatch) {
|
if (webServerMatch) {
|
||||||
expect(webServerMatch.action.type).toEqual('forward');
|
expect(webServerMatch.action.type).toEqual('forward');
|
||||||
expect(webServerMatch.action.target.host).toEqual('web-server');
|
expect(webServerMatch.action.targets[0].host).toEqual('web-server');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web server (HTTP redirect via socket handler)
|
// Web server (HTTP redirect via socket handler)
|
||||||
@@ -532,7 +532,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
|||||||
expect(apiMatch).not.toBeUndefined();
|
expect(apiMatch).not.toBeUndefined();
|
||||||
if (apiMatch) {
|
if (apiMatch) {
|
||||||
expect(apiMatch.action.type).toEqual('forward');
|
expect(apiMatch.action.type).toEqual('forward');
|
||||||
expect(apiMatch.action.target.host).toEqual('api-server');
|
expect(apiMatch.action.targets[0].host).toEqual('api-server');
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket server
|
// WebSocket server
|
||||||
@@ -544,7 +544,7 @@ tap.test('Route Integration - Combining Multiple Route Types', async () => {
|
|||||||
expect(wsMatch).not.toBeUndefined();
|
expect(wsMatch).not.toBeUndefined();
|
||||||
if (wsMatch) {
|
if (wsMatch) {
|
||||||
expect(wsMatch.action.type).toEqual('forward');
|
expect(wsMatch.action.type).toEqual('forward');
|
||||||
expect(wsMatch.action.target.host).toEqual('websocket-server');
|
expect(wsMatch.action.targets[0].host).toEqual('websocket-server');
|
||||||
expect(wsMatch.action.websocket?.enabled).toBeTrue();
|
expect(wsMatch.action.websocket?.enabled).toBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,10 +28,10 @@ tap.test('route security should block connections from unauthorized IPs', async
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 9990
|
port: 9990
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
// Only allow a non-existent IP
|
// Only allow a non-existent IP
|
||||||
@@ -142,10 +142,10 @@ tap.test('route security with block list should work', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 9992
|
port: 9992
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
security: { // Security at route level, not action level
|
security: { // Security at route level, not action level
|
||||||
ipBlockList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
|
ipBlockList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
|
||||||
@@ -234,10 +234,10 @@ tap.test('route without security should allow all connections', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 9994
|
port: 9994
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
// No security defined
|
// No security defined
|
||||||
}];
|
}];
|
||||||
|
@@ -10,10 +10,10 @@ tap.test('route security should be correctly configured', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8991
|
port: 8991
|
||||||
},
|
}],
|
||||||
security: {
|
security: {
|
||||||
ipAllowList: ['192.168.1.1'],
|
ipAllowList: ['192.168.1.1'],
|
||||||
ipBlockList: ['10.0.0.1']
|
ipBlockList: ['10.0.0.1']
|
||||||
|
@@ -26,10 +26,10 @@ tap.test('route-specific security should be enforced', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8877
|
port: 8877
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
ipAllowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
|
ipAllowList: ['127.0.0.1', '::1', '::ffff:127.0.0.1']
|
||||||
@@ -108,10 +108,10 @@ tap.test('route-specific IP block list should be enforced', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8879
|
port: 8879
|
||||||
}
|
}]
|
||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
ipAllowList: ['0.0.0.0/0', '::/0'], // Allow all IPs
|
ipAllowList: ['0.0.0.0/0', '::/0'], // Allow all IPs
|
||||||
@@ -215,10 +215,10 @@ tap.test('routes without security should allow all connections', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 8881
|
port: 8881
|
||||||
}
|
}]
|
||||||
// No security section - should allow all
|
// No security section - should allow all
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
@@ -13,10 +13,10 @@ const createRoute = (id: number, domain: string, port: number = 8443) => ({
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000 + id
|
port: 3000 + id
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate' as const,
|
mode: 'terminate' as const,
|
||||||
certificate: 'auto' as const,
|
certificate: 'auto' as const,
|
||||||
@@ -209,10 +209,10 @@ tap.test('should handle route updates when cert manager is not initialized', asy
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -47,7 +47,7 @@ import {
|
|||||||
addRateLimiting,
|
addRateLimiting,
|
||||||
addBasicAuth,
|
addBasicAuth,
|
||||||
addJwtAuth
|
addJwtAuth
|
||||||
} from '../ts/proxies/smart-proxy/utils/route-patterns.js';
|
} from '../ts/proxies/smart-proxy/utils/route-helpers.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IRouteConfig,
|
IRouteConfig,
|
||||||
@@ -134,10 +134,10 @@ tap.test('Route Validation - validateRouteAction', async () => {
|
|||||||
// Valid forward action
|
// Valid forward action
|
||||||
const validForwardAction: IRouteAction = {
|
const validForwardAction: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
};
|
};
|
||||||
const validForwardResult = validateRouteAction(validForwardAction);
|
const validForwardResult = validateRouteAction(validForwardAction);
|
||||||
expect(validForwardResult.valid).toBeTrue();
|
expect(validForwardResult.valid).toBeTrue();
|
||||||
@@ -154,14 +154,14 @@ tap.test('Route Validation - validateRouteAction', async () => {
|
|||||||
expect(validSocketResult.valid).toBeTrue();
|
expect(validSocketResult.valid).toBeTrue();
|
||||||
expect(validSocketResult.errors.length).toEqual(0);
|
expect(validSocketResult.errors.length).toEqual(0);
|
||||||
|
|
||||||
// Invalid action (missing target)
|
// Invalid action (missing targets)
|
||||||
const invalidAction: IRouteAction = {
|
const invalidAction: IRouteAction = {
|
||||||
type: 'forward'
|
type: 'forward'
|
||||||
};
|
};
|
||||||
const invalidResult = validateRouteAction(invalidAction);
|
const invalidResult = validateRouteAction(invalidAction);
|
||||||
expect(invalidResult.valid).toBeFalse();
|
expect(invalidResult.valid).toBeFalse();
|
||||||
expect(invalidResult.errors.length).toBeGreaterThan(0);
|
expect(invalidResult.errors.length).toBeGreaterThan(0);
|
||||||
expect(invalidResult.errors[0]).toInclude('Target is required');
|
expect(invalidResult.errors[0]).toInclude('Targets array is required');
|
||||||
|
|
||||||
// Invalid action (missing socket handler)
|
// Invalid action (missing socket handler)
|
||||||
const invalidSocketAction: IRouteAction = {
|
const invalidSocketAction: IRouteAction = {
|
||||||
@@ -180,7 +180,7 @@ tap.test('Route Validation - validateRouteConfig', async () => {
|
|||||||
expect(validResult.valid).toBeTrue();
|
expect(validResult.valid).toBeTrue();
|
||||||
expect(validResult.errors.length).toEqual(0);
|
expect(validResult.errors.length).toEqual(0);
|
||||||
|
|
||||||
// Invalid route config (missing target)
|
// Invalid route config (missing targets)
|
||||||
const invalidRoute: IRouteConfig = {
|
const invalidRoute: IRouteConfig = {
|
||||||
match: {
|
match: {
|
||||||
domains: 'example.com',
|
domains: 'example.com',
|
||||||
@@ -309,16 +309,16 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => {
|
|||||||
const actionOverride: Partial<IRouteConfig> = {
|
const actionOverride: Partial<IRouteConfig> = {
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'new-host.local',
|
host: 'new-host.local',
|
||||||
port: 5000
|
port: 5000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride);
|
const actionMergedRoute = mergeRouteConfigs(baseRoute, actionOverride);
|
||||||
expect(actionMergedRoute.action.target.host).toEqual('new-host.local');
|
expect(actionMergedRoute.action.targets?.[0]?.host).toEqual('new-host.local');
|
||||||
expect(actionMergedRoute.action.target.port).toEqual(5000);
|
expect(actionMergedRoute.action.targets?.[0]?.port).toEqual(5000);
|
||||||
|
|
||||||
// Test replacing action with socket handler
|
// Test replacing action with socket handler
|
||||||
const typeChangeOverride: Partial<IRouteConfig> = {
|
const typeChangeOverride: Partial<IRouteConfig> = {
|
||||||
@@ -336,7 +336,7 @@ tap.test('Route Utilities - mergeRouteConfigs', async () => {
|
|||||||
const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride);
|
const typeChangedRoute = mergeRouteConfigs(baseRoute, typeChangeOverride);
|
||||||
expect(typeChangedRoute.action.type).toEqual('socket-handler');
|
expect(typeChangedRoute.action.type).toEqual('socket-handler');
|
||||||
expect(typeChangedRoute.action.socketHandler).toBeDefined();
|
expect(typeChangedRoute.action.socketHandler).toBeDefined();
|
||||||
expect(typeChangedRoute.action.target).toBeUndefined();
|
expect(typeChangedRoute.action.targets).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Route Matching - routeMatchesDomain', async () => {
|
tap.test('Route Matching - routeMatchesDomain', async () => {
|
||||||
@@ -379,10 +379,10 @@ tap.test('Route Matching - routeMatchesPort', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -393,10 +393,10 @@ tap.test('Route Matching - routeMatchesPort', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -427,10 +427,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -443,10 +443,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -458,10 +458,10 @@ tap.test('Route Matching - routeMatchesPath', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -494,10 +494,10 @@ tap.test('Route Matching - routeMatchesHeaders', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -641,7 +641,7 @@ tap.test('Route Utilities - cloneRoute', async () => {
|
|||||||
expect(clonedRoute.name).toEqual(originalRoute.name);
|
expect(clonedRoute.name).toEqual(originalRoute.name);
|
||||||
expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains);
|
expect(clonedRoute.match.domains).toEqual(originalRoute.match.domains);
|
||||||
expect(clonedRoute.action.type).toEqual(originalRoute.action.type);
|
expect(clonedRoute.action.type).toEqual(originalRoute.action.type);
|
||||||
expect(clonedRoute.action.target.port).toEqual(originalRoute.action.target.port);
|
expect(clonedRoute.action.targets?.[0]?.port).toEqual(originalRoute.action.targets?.[0]?.port);
|
||||||
|
|
||||||
// Modify the clone and check that the original is unchanged
|
// Modify the clone and check that the original is unchanged
|
||||||
clonedRoute.name = 'Modified Clone';
|
clonedRoute.name = 'Modified Clone';
|
||||||
@@ -656,8 +656,8 @@ tap.test('Route Helpers - createHttpRoute', async () => {
|
|||||||
expect(route.match.domains).toEqual('example.com');
|
expect(route.match.domains).toEqual('example.com');
|
||||||
expect(route.match.ports).toEqual(80);
|
expect(route.match.ports).toEqual(80);
|
||||||
expect(route.action.type).toEqual('forward');
|
expect(route.action.type).toEqual('forward');
|
||||||
expect(route.action.target.host).toEqual('localhost');
|
expect(route.action.targets?.[0]?.host).toEqual('localhost');
|
||||||
expect(route.action.target.port).toEqual(3000);
|
expect(route.action.targets?.[0]?.port).toEqual(3000);
|
||||||
|
|
||||||
const validationResult = validateRouteConfig(route);
|
const validationResult = validateRouteConfig(route);
|
||||||
expect(validationResult.valid).toBeTrue();
|
expect(validationResult.valid).toBeTrue();
|
||||||
@@ -790,11 +790,11 @@ tap.test('Route Helpers - createLoadBalancerRoute', async () => {
|
|||||||
expect(route.match.domains).toEqual('loadbalancer.example.com');
|
expect(route.match.domains).toEqual('loadbalancer.example.com');
|
||||||
expect(route.match.ports).toEqual(443);
|
expect(route.match.ports).toEqual(443);
|
||||||
expect(route.action.type).toEqual('forward');
|
expect(route.action.type).toEqual('forward');
|
||||||
expect(Array.isArray(route.action.target.host)).toBeTrue();
|
expect(route.action.targets).toBeDefined();
|
||||||
if (Array.isArray(route.action.target.host)) {
|
if (route.action.targets && Array.isArray(route.action.targets[0]?.host)) {
|
||||||
expect(route.action.target.host.length).toEqual(3);
|
expect((route.action.targets[0].host as string[]).length).toEqual(3);
|
||||||
}
|
}
|
||||||
expect(route.action.target.port).toEqual(8080);
|
expect(route.action.targets?.[0]?.port).toEqual(8080);
|
||||||
expect(route.action.tls.mode).toEqual('terminate');
|
expect(route.action.tls.mode).toEqual('terminate');
|
||||||
|
|
||||||
const validationResult = validateRouteConfig(route);
|
const validationResult = validateRouteConfig(route);
|
||||||
@@ -819,7 +819,7 @@ tap.test('Route Patterns - createApiGatewayRoute', async () => {
|
|||||||
expect(apiGatewayRoute.match.domains).toEqual('api.example.com');
|
expect(apiGatewayRoute.match.domains).toEqual('api.example.com');
|
||||||
expect(apiGatewayRoute.match.path).toInclude('/v1');
|
expect(apiGatewayRoute.match.path).toInclude('/v1');
|
||||||
expect(apiGatewayRoute.action.type).toEqual('forward');
|
expect(apiGatewayRoute.action.type).toEqual('forward');
|
||||||
expect(apiGatewayRoute.action.target.port).toEqual(3000);
|
expect(apiGatewayRoute.action.targets?.[0]?.port).toEqual(3000);
|
||||||
|
|
||||||
// Check TLS configuration
|
// Check TLS configuration
|
||||||
if (apiGatewayRoute.action.tls) {
|
if (apiGatewayRoute.action.tls) {
|
||||||
@@ -854,7 +854,7 @@ tap.test('Route Patterns - createWebSocketPattern', async () => {
|
|||||||
expect(wsRoute.match.domains).toEqual('ws.example.com');
|
expect(wsRoute.match.domains).toEqual('ws.example.com');
|
||||||
expect(wsRoute.match.path).toEqual('/socket');
|
expect(wsRoute.match.path).toEqual('/socket');
|
||||||
expect(wsRoute.action.type).toEqual('forward');
|
expect(wsRoute.action.type).toEqual('forward');
|
||||||
expect(wsRoute.action.target.port).toEqual(3000);
|
expect(wsRoute.action.targets?.[0]?.port).toEqual(3000);
|
||||||
|
|
||||||
// Check TLS configuration
|
// Check TLS configuration
|
||||||
if (wsRoute.action.tls) {
|
if (wsRoute.action.tls) {
|
||||||
@@ -891,8 +891,8 @@ tap.test('Route Patterns - createLoadBalancerRoute pattern', async () => {
|
|||||||
expect(lbRoute.action.type).toEqual('forward');
|
expect(lbRoute.action.type).toEqual('forward');
|
||||||
|
|
||||||
// Check target hosts
|
// Check target hosts
|
||||||
if (Array.isArray(lbRoute.action.target.host)) {
|
if (lbRoute.action.targets && Array.isArray(lbRoute.action.targets[0]?.host)) {
|
||||||
expect(lbRoute.action.target.host.length).toEqual(3);
|
expect((lbRoute.action.targets[0].host as string[]).length).toEqual(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check TLS configuration
|
// Check TLS configuration
|
||||||
|
@@ -37,10 +37,10 @@ function createRouteConfig(
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: destinationIp,
|
host: destinationIp,
|
||||||
port: destinationPort
|
port: destinationPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -38,15 +38,17 @@ tap.test('Per-IP connection limits validation', async () => {
|
|||||||
|
|
||||||
// Track connections up to limit
|
// Track connections up to limit
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
securityManager.trackConnectionByIP(testIP, `conn${i}`);
|
// Validate BEFORE tracking the connection (checking if we can add a new connection)
|
||||||
const result = securityManager.validateIP(testIP);
|
const result = securityManager.validateIP(testIP);
|
||||||
expect(result.allowed).toBeTrue();
|
expect(result.allowed).toBeTrue();
|
||||||
|
// Now track the connection
|
||||||
|
securityManager.trackConnectionByIP(testIP, `conn${i}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify we're at the limit
|
// Verify we're at the limit
|
||||||
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5);
|
expect(securityManager.getConnectionCountByIP(testIP)).toEqual(5);
|
||||||
|
|
||||||
// Next connection should be rejected
|
// Next connection should be rejected (we're already at 5)
|
||||||
const result = securityManager.validateIP(testIP);
|
const result = securityManager.validateIP(testIP);
|
||||||
expect(result.allowed).toBeFalse();
|
expect(result.allowed).toBeFalse();
|
||||||
expect(result.reason).toInclude('Maximum connections per IP');
|
expect(result.reason).toInclude('Maximum connections per IP');
|
||||||
@@ -61,28 +63,23 @@ tap.test('Connection rate limiting', async () => {
|
|||||||
const testIP = '192.168.1.102';
|
const testIP = '192.168.1.102';
|
||||||
|
|
||||||
// Make connections at the rate limit
|
// Make connections at the rate limit
|
||||||
|
// Note: validateIP() already tracks timestamps internally for rate limiting
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const result = securityManager.validateIP(testIP);
|
const result = securityManager.validateIP(testIP);
|
||||||
expect(result.allowed).toBeTrue();
|
expect(result.allowed).toBeTrue();
|
||||||
securityManager.trackConnectionByIP(testIP, `conn${i}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next connection should exceed rate limit
|
// Next connection should exceed rate limit
|
||||||
const result = securityManager.validateIP(testIP);
|
const result = securityManager.validateIP(testIP);
|
||||||
expect(result.allowed).toBeFalse();
|
expect(result.allowed).toBeFalse();
|
||||||
expect(result.reason).toInclude('Connection rate limit');
|
expect(result.reason).toInclude('Connection rate limit');
|
||||||
|
|
||||||
// Clean up connections
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
securityManager.removeConnectionByIP(testIP, `conn${i}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('Route-level connection limits', async () => {
|
tap.test('Route-level connection limits', async () => {
|
||||||
const route: IRouteConfig = {
|
const route: IRouteConfig = {
|
||||||
name: 'test-route',
|
name: 'test-route',
|
||||||
match: { ports: 443 },
|
match: { ports: 443 },
|
||||||
action: { type: 'forward', target: { host: 'localhost', port: 8080 } },
|
action: { type: 'forward', targets: [{ host: 'localhost', port: 8080 }] },
|
||||||
security: {
|
security: {
|
||||||
maxConnections: 3
|
maxConnections: 3
|
||||||
}
|
}
|
||||||
@@ -93,7 +90,8 @@ tap.test('Route-level connection limits', async () => {
|
|||||||
clientIp: '192.168.1.103',
|
clientIp: '192.168.1.103',
|
||||||
serverIp: '0.0.0.0',
|
serverIp: '0.0.0.0',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
connectionId: 'test-conn'
|
connectionId: 'test-conn',
|
||||||
|
isTls: true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test with connection counts below limit
|
// Test with connection counts below limit
|
||||||
|
@@ -15,10 +15,10 @@ tap.test('should create a SmartCertManager instance', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 3000
|
port: 3000
|
||||||
},
|
}],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: 'auto',
|
certificate: 'auto',
|
||||||
|
@@ -73,10 +73,10 @@ tap.test('setup port proxy test environment', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -112,10 +112,10 @@ tap.test('should forward TCP connections to custom host', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -157,10 +157,10 @@ tap.test('should forward connections to custom IP', async () => {
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: targetServerPort
|
port: targetServerPort
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -252,10 +252,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: PROXY_PORT + 5
|
port: PROXY_PORT + 5
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -273,10 +273,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -311,10 +311,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: PROXY_PORT + 7
|
port: PROXY_PORT + 7
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -334,10 +334,10 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: TEST_SERVER_PORT
|
port: TEST_SERVER_PORT
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -377,10 +377,10 @@ tap.test('should use round robin for multiple target hosts in domain config', as
|
|||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward' as const,
|
type: 'forward' as const,
|
||||||
target: {
|
targets: [{
|
||||||
host: ['hostA', 'hostB'], // Array of hosts for round-robin
|
host: ['hostA', 'hostB'], // Array of hosts for round-robin
|
||||||
port: 80
|
port: 80
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,9 +400,9 @@ tap.test('should use round robin for multiple target hosts in domain config', as
|
|||||||
|
|
||||||
// For route-based approach, the actual round-robin logic happens in connection handling
|
// For route-based approach, the actual round-robin logic happens in connection handling
|
||||||
// Just make sure our config has the expected hosts
|
// Just make sure our config has the expected hosts
|
||||||
expect(Array.isArray(routeConfig.action.target.host)).toBeTrue();
|
expect(Array.isArray(routeConfig.action.targets![0].host)).toBeTrue();
|
||||||
expect(routeConfig.action.target.host).toContain('hostA');
|
expect(routeConfig.action.targets![0].host).toContain('hostA');
|
||||||
expect(routeConfig.action.target.host).toContain('hostB');
|
expect(routeConfig.action.targets![0].host).toContain('hostB');
|
||||||
});
|
});
|
||||||
|
|
||||||
// CLEANUP: Tear down all servers and proxies
|
// CLEANUP: Tear down all servers and proxies
|
||||||
|
@@ -30,7 +30,7 @@ tap.test('stuck connection cleanup - verify connections to hanging backends are
|
|||||||
match: { ports: 8589 },
|
match: { ports: 8589 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9997 }
|
targets: [{ host: 'localhost', port: 9997 }]
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@@ -17,7 +17,7 @@ tap.test('websocket keep-alive settings for SNI passthrough', async (tools) => {
|
|||||||
match: { ports: 8443, domains: 'test.local' },
|
match: { ports: 8443, domains: 'test.local' },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9443 },
|
targets: [{ host: 'localhost', port: 9443 }],
|
||||||
tls: { mode: 'passthrough' }
|
tls: { mode: 'passthrough' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ tap.test('long-lived connection survival test', async (tools) => {
|
|||||||
match: { ports: 8444 },
|
match: { ports: 8444 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: { host: 'localhost', port: 9444 }
|
targets: [{ host: 'localhost', port: 9444 }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -52,10 +52,10 @@ tap.test('zombie connection cleanup - verify inactivity check detects and cleans
|
|||||||
match: { ports: 8591 },
|
match: { ports: 8591 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 9998
|
port: 9998
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
@@ -71,10 +71,10 @@ tap.test('zombie connection cleanup - verify inactivity check detects and cleans
|
|||||||
match: { ports: 8590 },
|
match: { ports: 8590 },
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 8591
|
port: 8591
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
@@ -13,7 +13,8 @@ import {
|
|||||||
trackConnection,
|
trackConnection,
|
||||||
removeConnection,
|
removeConnection,
|
||||||
cleanupExpiredRateLimits,
|
cleanupExpiredRateLimits,
|
||||||
parseBasicAuthHeader
|
parseBasicAuthHeader,
|
||||||
|
normalizeIP
|
||||||
} from './security-utils.js';
|
} from './security-utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +79,15 @@ export class SharedSecurityManager {
|
|||||||
* @returns Number of connections from this IP
|
* @returns Number of connections from this IP
|
||||||
*/
|
*/
|
||||||
public getConnectionCountByIP(ip: string): number {
|
public getConnectionCountByIP(ip: string): number {
|
||||||
return this.connectionsByIP.get(ip)?.connections.size || 0;
|
// Check all normalized variants of the IP
|
||||||
|
const variants = normalizeIP(ip);
|
||||||
|
for (const variant of variants) {
|
||||||
|
const info = this.connectionsByIP.get(variant);
|
||||||
|
if (info) {
|
||||||
|
return info.connections.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +97,19 @@ export class SharedSecurityManager {
|
|||||||
* @param connectionId - The connection ID to associate
|
* @param connectionId - The connection ID to associate
|
||||||
*/
|
*/
|
||||||
public trackConnectionByIP(ip: string, connectionId: string): void {
|
public trackConnectionByIP(ip: string, connectionId: string): void {
|
||||||
trackConnection(ip, connectionId, this.connectionsByIP);
|
// Check if any variant already exists
|
||||||
|
const variants = normalizeIP(ip);
|
||||||
|
let existingKey: string | null = null;
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
if (this.connectionsByIP.has(variant)) {
|
||||||
|
existingKey = variant;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use existing key or the original IP
|
||||||
|
trackConnection(existingKey || ip, connectionId, this.connectionsByIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,7 +119,15 @@ export class SharedSecurityManager {
|
|||||||
* @param connectionId - The connection ID to remove
|
* @param connectionId - The connection ID to remove
|
||||||
*/
|
*/
|
||||||
public removeConnectionByIP(ip: string, connectionId: string): void {
|
public removeConnectionByIP(ip: string, connectionId: string): void {
|
||||||
removeConnection(ip, connectionId, this.connectionsByIP);
|
// Check all variants to find where the connection is tracked
|
||||||
|
const variants = normalizeIP(ip);
|
||||||
|
|
||||||
|
for (const variant of variants) {
|
||||||
|
if (this.connectionsByIP.has(variant)) {
|
||||||
|
removeConnection(variant, connectionId, this.connectionsByIP);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,76 +0,0 @@
|
|||||||
import type * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The primary forwarding types supported by SmartProxy
|
|
||||||
* Used for configuration compatibility
|
|
||||||
*/
|
|
||||||
export type TForwardingType =
|
|
||||||
| 'http-only' // HTTP forwarding only (no HTTPS)
|
|
||||||
| 'https-passthrough' // Pass-through TLS traffic (SNI forwarding)
|
|
||||||
| 'https-terminate-to-http' // Terminate TLS and forward to HTTP backend
|
|
||||||
| 'https-terminate-to-https'; // Terminate TLS and forward to HTTPS backend
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event types emitted by forwarding handlers
|
|
||||||
*/
|
|
||||||
export enum ForwardingHandlerEvents {
|
|
||||||
CONNECTED = 'connected',
|
|
||||||
DISCONNECTED = 'disconnected',
|
|
||||||
ERROR = 'error',
|
|
||||||
DATA_FORWARDED = 'data-forwarded',
|
|
||||||
HTTP_REQUEST = 'http-request',
|
|
||||||
HTTP_RESPONSE = 'http-response',
|
|
||||||
CERTIFICATE_NEEDED = 'certificate-needed',
|
|
||||||
CERTIFICATE_LOADED = 'certificate-loaded'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base interface for forwarding handlers
|
|
||||||
*/
|
|
||||||
export interface IForwardingHandler extends plugins.EventEmitter {
|
|
||||||
initialize(): Promise<void>;
|
|
||||||
handleConnection(socket: plugins.net.Socket): void;
|
|
||||||
handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route-based helpers are now available directly from route-patterns.ts
|
|
||||||
import {
|
|
||||||
createHttpRoute,
|
|
||||||
createHttpsTerminateRoute,
|
|
||||||
createHttpsPassthroughRoute,
|
|
||||||
createHttpToHttpsRedirect,
|
|
||||||
createCompleteHttpsServer,
|
|
||||||
createLoadBalancerRoute
|
|
||||||
} from '../../proxies/smart-proxy/utils/route-patterns.js';
|
|
||||||
|
|
||||||
export {
|
|
||||||
createHttpRoute,
|
|
||||||
createHttpsTerminateRoute,
|
|
||||||
createHttpsPassthroughRoute,
|
|
||||||
createHttpToHttpsRedirect,
|
|
||||||
createCompleteHttpsServer,
|
|
||||||
createLoadBalancerRoute
|
|
||||||
};
|
|
||||||
|
|
||||||
// Note: Legacy helper functions have been removed
|
|
||||||
// Please use the route-based helpers instead:
|
|
||||||
// - createHttpRoute
|
|
||||||
// - createHttpsTerminateRoute
|
|
||||||
// - createHttpsPassthroughRoute
|
|
||||||
// - createHttpToHttpsRedirect
|
|
||||||
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
|
|
||||||
|
|
||||||
// For backward compatibility, kept only the basic configuration interface
|
|
||||||
export interface IForwardConfig {
|
|
||||||
type: TForwardingType;
|
|
||||||
target: {
|
|
||||||
host: string | string[];
|
|
||||||
port: number | 'preserve' | ((ctx: any) => number);
|
|
||||||
};
|
|
||||||
http?: any;
|
|
||||||
https?: any;
|
|
||||||
acme?: any;
|
|
||||||
security?: any;
|
|
||||||
advanced?: any;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* Forwarding configuration exports
|
|
||||||
*
|
|
||||||
* Note: The legacy domain-based configuration has been replaced by route-based configuration.
|
|
||||||
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type {
|
|
||||||
TForwardingType,
|
|
||||||
IForwardConfig,
|
|
||||||
IForwardingHandler
|
|
||||||
} from './forwarding-types.js';
|
|
||||||
|
|
||||||
export {
|
|
||||||
ForwardingHandlerEvents
|
|
||||||
} from './forwarding-types.js';
|
|
||||||
|
|
||||||
// Import route helpers from route-patterns instead of deleted route-helpers
|
|
||||||
export {
|
|
||||||
createHttpRoute,
|
|
||||||
createHttpsTerminateRoute,
|
|
||||||
createHttpsPassthroughRoute,
|
|
||||||
createHttpToHttpsRedirect,
|
|
||||||
createCompleteHttpsServer,
|
|
||||||
createLoadBalancerRoute
|
|
||||||
} from '../../proxies/smart-proxy/utils/route-patterns.js';
|
|
@@ -1,189 +0,0 @@
|
|||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandler } from '../handlers/base-handler.js';
|
|
||||||
import { HttpForwardingHandler } from '../handlers/http-handler.js';
|
|
||||||
import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js';
|
|
||||||
import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js';
|
|
||||||
import { HttpsTerminateToHttpsHandler } from '../handlers/https-terminate-to-https-handler.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory for creating forwarding handlers based on the configuration type
|
|
||||||
*/
|
|
||||||
export class ForwardingHandlerFactory {
|
|
||||||
/**
|
|
||||||
* Create a forwarding handler based on the configuration
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
* @returns The appropriate forwarding handler
|
|
||||||
*/
|
|
||||||
public static createHandler(config: IForwardConfig): ForwardingHandler {
|
|
||||||
// Create the appropriate handler based on the forwarding type
|
|
||||||
switch (config.type) {
|
|
||||||
case 'http-only':
|
|
||||||
return new HttpForwardingHandler(config);
|
|
||||||
|
|
||||||
case 'https-passthrough':
|
|
||||||
return new HttpsPassthroughHandler(config);
|
|
||||||
|
|
||||||
case 'https-terminate-to-http':
|
|
||||||
return new HttpsTerminateToHttpHandler(config);
|
|
||||||
|
|
||||||
case 'https-terminate-to-https':
|
|
||||||
return new HttpsTerminateToHttpsHandler(config);
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Type system should prevent this, but just in case:
|
|
||||||
throw new Error(`Unknown forwarding type: ${(config as any).type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply default values to a forwarding configuration based on its type
|
|
||||||
* @param config The original forwarding configuration
|
|
||||||
* @returns A configuration with defaults applied
|
|
||||||
*/
|
|
||||||
public static applyDefaults(config: IForwardConfig): IForwardConfig {
|
|
||||||
// Create a deep copy of the configuration
|
|
||||||
const result: IForwardConfig = JSON.parse(JSON.stringify(config));
|
|
||||||
|
|
||||||
// Apply defaults based on forwarding type
|
|
||||||
switch (config.type) {
|
|
||||||
case 'http-only':
|
|
||||||
// Set defaults for HTTP-only mode
|
|
||||||
result.http = {
|
|
||||||
enabled: true,
|
|
||||||
...config.http
|
|
||||||
};
|
|
||||||
// Set default port and socket if not provided
|
|
||||||
if (!result.port) {
|
|
||||||
result.port = 80;
|
|
||||||
}
|
|
||||||
if (!result.socket) {
|
|
||||||
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'https-passthrough':
|
|
||||||
// Set defaults for HTTPS passthrough
|
|
||||||
result.https = {
|
|
||||||
forwardSni: true,
|
|
||||||
...config.https
|
|
||||||
};
|
|
||||||
// SNI forwarding doesn't do HTTP
|
|
||||||
result.http = {
|
|
||||||
enabled: false,
|
|
||||||
...config.http
|
|
||||||
};
|
|
||||||
// Set default port and socket if not provided
|
|
||||||
if (!result.port) {
|
|
||||||
result.port = 443;
|
|
||||||
}
|
|
||||||
if (!result.socket) {
|
|
||||||
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'https-terminate-to-http':
|
|
||||||
// Set defaults for HTTPS termination to HTTP
|
|
||||||
result.https = {
|
|
||||||
...config.https
|
|
||||||
};
|
|
||||||
// Support HTTP access by default in this mode
|
|
||||||
result.http = {
|
|
||||||
enabled: true,
|
|
||||||
redirectToHttps: true,
|
|
||||||
...config.http
|
|
||||||
};
|
|
||||||
// Enable ACME by default
|
|
||||||
result.acme = {
|
|
||||||
enabled: true,
|
|
||||||
maintenance: true,
|
|
||||||
...config.acme
|
|
||||||
};
|
|
||||||
// Set default port and socket if not provided
|
|
||||||
if (!result.port) {
|
|
||||||
result.port = 443;
|
|
||||||
}
|
|
||||||
if (!result.socket) {
|
|
||||||
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'https-terminate-to-https':
|
|
||||||
// Similar to terminate-to-http but with different target handling
|
|
||||||
result.https = {
|
|
||||||
...config.https
|
|
||||||
};
|
|
||||||
result.http = {
|
|
||||||
enabled: true,
|
|
||||||
redirectToHttps: true,
|
|
||||||
...config.http
|
|
||||||
};
|
|
||||||
result.acme = {
|
|
||||||
enabled: true,
|
|
||||||
maintenance: true,
|
|
||||||
...config.acme
|
|
||||||
};
|
|
||||||
// Set default port and socket if not provided
|
|
||||||
if (!result.port) {
|
|
||||||
result.port = 443;
|
|
||||||
}
|
|
||||||
if (!result.socket) {
|
|
||||||
result.socket = `/tmp/forwarding-${config.type}-${result.port}.sock`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a forwarding configuration
|
|
||||||
* @param config The configuration to validate
|
|
||||||
* @throws Error if the configuration is invalid
|
|
||||||
*/
|
|
||||||
public static validateConfig(config: IForwardConfig): void {
|
|
||||||
// Validate common properties
|
|
||||||
if (!config.target) {
|
|
||||||
throw new Error('Forwarding configuration must include a target');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.target.host || (Array.isArray(config.target.host) && config.target.host.length === 0)) {
|
|
||||||
throw new Error('Target must include a host or array of hosts');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate port if it's a number
|
|
||||||
if (typeof config.target.port === 'number') {
|
|
||||||
if (config.target.port <= 0 || config.target.port > 65535) {
|
|
||||||
throw new Error('Target must include a valid port (1-65535)');
|
|
||||||
}
|
|
||||||
} else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') {
|
|
||||||
throw new Error('Target port must be a number, "preserve", or a function');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type-specific validation
|
|
||||||
switch (config.type) {
|
|
||||||
case 'http-only':
|
|
||||||
// HTTP-only needs http.enabled to be true
|
|
||||||
if (config.http?.enabled === false) {
|
|
||||||
throw new Error('HTTP-only forwarding must have HTTP enabled');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'https-passthrough':
|
|
||||||
// HTTPS passthrough doesn't support HTTP
|
|
||||||
if (config.http?.enabled === true) {
|
|
||||||
throw new Error('HTTPS passthrough does not support HTTP');
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPS passthrough doesn't work with ACME
|
|
||||||
if (config.acme?.enabled === true) {
|
|
||||||
throw new Error('HTTPS passthrough does not support ACME');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'https-terminate-to-http':
|
|
||||||
case 'https-terminate-to-https':
|
|
||||||
// These modes support all options, nothing specific to validate
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* Forwarding factory implementations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ForwardingHandlerFactory } from './forwarding-factory.js';
|
|
@@ -1,155 +0,0 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import type {
|
|
||||||
IForwardConfig,
|
|
||||||
IForwardingHandler
|
|
||||||
} from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for all forwarding handlers
|
|
||||||
*/
|
|
||||||
export abstract class ForwardingHandler extends plugins.EventEmitter implements IForwardingHandler {
|
|
||||||
/**
|
|
||||||
* Create a new ForwardingHandler
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
*/
|
|
||||||
constructor(protected config: IForwardConfig) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the handler
|
|
||||||
* Base implementation does nothing, subclasses should override as needed
|
|
||||||
*/
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
// Base implementation - no initialization needed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a new socket connection
|
|
||||||
* @param socket The incoming socket connection
|
|
||||||
*/
|
|
||||||
public abstract handleConnection(socket: plugins.net.Socket): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an HTTP request
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
public abstract handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a target from the configuration, supporting round-robin selection
|
|
||||||
* @param incomingPort Optional incoming port for 'preserve' mode
|
|
||||||
* @returns A resolved target object with host and port
|
|
||||||
*/
|
|
||||||
protected getTargetFromConfig(incomingPort: number = 80): { host: string, port: number } {
|
|
||||||
const { target } = this.config;
|
|
||||||
|
|
||||||
// Handle round-robin host selection
|
|
||||||
if (Array.isArray(target.host)) {
|
|
||||||
if (target.host.length === 0) {
|
|
||||||
throw new Error('No target hosts specified');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple round-robin selection
|
|
||||||
const randomIndex = Math.floor(Math.random() * target.host.length);
|
|
||||||
return {
|
|
||||||
host: target.host[randomIndex],
|
|
||||||
port: this.resolvePort(target.port, incomingPort)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single host
|
|
||||||
return {
|
|
||||||
host: target.host,
|
|
||||||
port: this.resolvePort(target.port, incomingPort)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves a port value, handling 'preserve' and function ports
|
|
||||||
* @param port The port value to resolve
|
|
||||||
* @param incomingPort Optional incoming port to use for 'preserve' mode
|
|
||||||
*/
|
|
||||||
protected resolvePort(
|
|
||||||
port: number | 'preserve' | ((ctx: any) => number),
|
|
||||||
incomingPort: number = 80
|
|
||||||
): number {
|
|
||||||
if (typeof port === 'function') {
|
|
||||||
try {
|
|
||||||
// Create a minimal context for the function that includes the incoming port
|
|
||||||
const ctx = { port: incomingPort };
|
|
||||||
return port(ctx);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error resolving port function:', err);
|
|
||||||
return incomingPort; // Fall back to incoming port
|
|
||||||
}
|
|
||||||
} else if (port === 'preserve') {
|
|
||||||
return incomingPort; // Use the actual incoming port for 'preserve'
|
|
||||||
} else {
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect an HTTP request to HTTPS
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
protected redirectToHttps(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
||||||
const host = req.headers.host || '';
|
|
||||||
const path = req.url || '/';
|
|
||||||
const redirectUrl = `https://${host}${path}`;
|
|
||||||
|
|
||||||
res.writeHead(301, {
|
|
||||||
'Location': redirectUrl,
|
|
||||||
'Cache-Control': 'no-cache'
|
|
||||||
});
|
|
||||||
res.end(`Redirecting to ${redirectUrl}`);
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
|
||||||
statusCode: 301,
|
|
||||||
headers: { 'Location': redirectUrl },
|
|
||||||
size: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply custom headers from configuration
|
|
||||||
* @param headers The original headers
|
|
||||||
* @param variables Variables to replace in the headers
|
|
||||||
* @returns The headers with custom values applied
|
|
||||||
*/
|
|
||||||
protected applyCustomHeaders(
|
|
||||||
headers: Record<string, string | string[] | undefined>,
|
|
||||||
variables: Record<string, string>
|
|
||||||
): Record<string, string | string[] | undefined> {
|
|
||||||
const customHeaders = this.config.advanced?.headers || {};
|
|
||||||
const result = { ...headers };
|
|
||||||
|
|
||||||
// Apply custom headers with variable substitution
|
|
||||||
for (const [key, value] of Object.entries(customHeaders)) {
|
|
||||||
if (typeof value !== 'string') continue;
|
|
||||||
|
|
||||||
let processedValue = value;
|
|
||||||
|
|
||||||
// Replace variables in the header value
|
|
||||||
for (const [varName, varValue] of Object.entries(variables)) {
|
|
||||||
processedValue = processedValue.replace(`{${varName}}`, varValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = processedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the timeout for this connection from configuration
|
|
||||||
* @returns Timeout in milliseconds
|
|
||||||
*/
|
|
||||||
protected getTimeout(): number {
|
|
||||||
return this.config.advanced?.timeout || 60000; // Default: 60 seconds
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,163 +0,0 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
||||||
import { setupSocketHandlers } from '../../core/utils/socket-utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for HTTP-only forwarding
|
|
||||||
*/
|
|
||||||
export class HttpForwardingHandler extends ForwardingHandler {
|
|
||||||
/**
|
|
||||||
* Create a new HTTP forwarding handler
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
*/
|
|
||||||
constructor(config: IForwardConfig) {
|
|
||||||
super(config);
|
|
||||||
|
|
||||||
// Validate that this is an HTTP-only configuration
|
|
||||||
if (config.type !== 'http-only') {
|
|
||||||
throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the handler
|
|
||||||
* HTTP handler doesn't need special initialization
|
|
||||||
*/
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
// Basic initialization from parent class
|
|
||||||
await super.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a raw socket connection
|
|
||||||
* HTTP handler doesn't do much with raw sockets as it mainly processes
|
|
||||||
* parsed HTTP requests
|
|
||||||
*/
|
|
||||||
public handleConnection(socket: plugins.net.Socket): void {
|
|
||||||
// For HTTP, we mainly handle parsed requests, but we can still set up
|
|
||||||
// some basic connection tracking
|
|
||||||
const remoteAddress = socket.remoteAddress || 'unknown';
|
|
||||||
const localPort = socket.localPort || 80;
|
|
||||||
|
|
||||||
// Set up socket handlers with proper cleanup
|
|
||||||
const handleClose = (reason: string) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use custom timeout handler that doesn't close the socket
|
|
||||||
setupSocketHandlers(socket, handleClose, () => {
|
|
||||||
// For HTTP, we can be more aggressive with timeouts since connections are shorter
|
|
||||||
// But still don't close immediately - let the connection finish naturally
|
|
||||||
console.warn(`HTTP socket timeout from ${remoteAddress}`);
|
|
||||||
}, 'http');
|
|
||||||
|
|
||||||
socket.on('error', (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
localPort
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an HTTP request
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
||||||
// Get the local port from the request (for 'preserve' port handling)
|
|
||||||
const localPort = req.socket.localPort || 80;
|
|
||||||
|
|
||||||
// Get the target from configuration, passing the incoming port
|
|
||||||
const target = this.getTargetFromConfig(localPort);
|
|
||||||
|
|
||||||
// Create a custom headers object with variables for substitution
|
|
||||||
const variables = {
|
|
||||||
clientIp: req.socket.remoteAddress || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare headers, merging with any custom headers from config
|
|
||||||
const headers = this.applyCustomHeaders(req.headers, variables);
|
|
||||||
|
|
||||||
// Create the proxy request options
|
|
||||||
const options = {
|
|
||||||
hostname: target.host,
|
|
||||||
port: target.port,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the proxy request
|
|
||||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
|
||||||
// Copy status code and headers from the proxied response
|
|
||||||
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
|
||||||
|
|
||||||
// Pipe the proxy response to the client response
|
|
||||||
proxyRes.pipe(res);
|
|
||||||
|
|
||||||
// Track bytes for logging
|
|
||||||
let responseSize = 0;
|
|
||||||
proxyRes.on('data', (chunk) => {
|
|
||||||
responseSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyRes.on('end', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
size: responseSize
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors in the proxy request
|
|
||||||
proxyReq.on('error', (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
error: `Proxy request error: ${error.message}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an error response if headers haven't been sent yet
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Error forwarding request: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
// Just end the response if headers have already been sent
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track request details for logging
|
|
||||||
let requestSize = 0;
|
|
||||||
req.on('data', (chunk) => {
|
|
||||||
requestSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the request
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: req.headers,
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe the client request to the proxy request
|
|
||||||
if (req.readable) {
|
|
||||||
req.pipe(proxyReq);
|
|
||||||
} else {
|
|
||||||
proxyReq.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,185 +0,0 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
||||||
import { createIndependentSocketHandlers, setupSocketHandlers, createSocketWithErrorHandler } from '../../core/utils/socket-utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for HTTPS passthrough (SNI forwarding without termination)
|
|
||||||
*/
|
|
||||||
export class HttpsPassthroughHandler extends ForwardingHandler {
|
|
||||||
/**
|
|
||||||
* Create a new HTTPS passthrough handler
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
*/
|
|
||||||
constructor(config: IForwardConfig) {
|
|
||||||
super(config);
|
|
||||||
|
|
||||||
// Validate that this is an HTTPS passthrough configuration
|
|
||||||
if (config.type !== 'https-passthrough') {
|
|
||||||
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the handler
|
|
||||||
* HTTPS passthrough handler doesn't need special initialization
|
|
||||||
*/
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
// Basic initialization from parent class
|
|
||||||
await super.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a TLS/SSL socket connection by forwarding it without termination
|
|
||||||
* @param clientSocket The incoming socket from the client
|
|
||||||
*/
|
|
||||||
public handleConnection(clientSocket: plugins.net.Socket): void {
|
|
||||||
// Get the target from configuration
|
|
||||||
const target = this.getTargetFromConfig();
|
|
||||||
|
|
||||||
// Log the connection
|
|
||||||
const remoteAddress = clientSocket.remoteAddress || 'unknown';
|
|
||||||
const remotePort = clientSocket.remotePort || 0;
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
remotePort,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track data transfer for logging
|
|
||||||
let bytesSent = 0;
|
|
||||||
let bytesReceived = 0;
|
|
||||||
let serverSocket: plugins.net.Socket | null = null;
|
|
||||||
let cleanupClient: ((reason: string) => Promise<void>) | null = null;
|
|
||||||
let cleanupServer: ((reason: string) => Promise<void>) | null = null;
|
|
||||||
|
|
||||||
// Create a connection to the target server with immediate error handling
|
|
||||||
serverSocket = createSocketWithErrorHandler({
|
|
||||||
port: target.port,
|
|
||||||
host: target.host,
|
|
||||||
onError: async (error) => {
|
|
||||||
// Server connection failed - clean up client socket immediately
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
error: error.message,
|
|
||||||
code: (error as any).code || 'UNKNOWN',
|
|
||||||
remoteAddress,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up the client socket since we can't forward
|
|
||||||
if (!clientSocket.destroyed) {
|
|
||||||
clientSocket.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
bytesSent: 0,
|
|
||||||
bytesReceived: 0,
|
|
||||||
reason: `server_connection_failed: ${error.message}`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onConnect: () => {
|
|
||||||
// Connection successful - set up forwarding handlers
|
|
||||||
const handlers = createIndependentSocketHandlers(
|
|
||||||
clientSocket,
|
|
||||||
serverSocket!,
|
|
||||||
(reason) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
bytesSent,
|
|
||||||
bytesReceived,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
cleanupClient = handlers.cleanupClient;
|
|
||||||
cleanupServer = handlers.cleanupServer;
|
|
||||||
|
|
||||||
// Setup handlers with custom timeout handling that doesn't close connections
|
|
||||||
const timeout = this.getTimeout();
|
|
||||||
|
|
||||||
setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
|
|
||||||
// Just reset timeout, don't close
|
|
||||||
socket.setTimeout(timeout);
|
|
||||||
}, 'client');
|
|
||||||
|
|
||||||
setupSocketHandlers(serverSocket!, cleanupServer, (socket) => {
|
|
||||||
// Just reset timeout, don't close
|
|
||||||
socket.setTimeout(timeout);
|
|
||||||
}, 'server');
|
|
||||||
|
|
||||||
// Forward data from client to server
|
|
||||||
clientSocket.on('data', (data) => {
|
|
||||||
bytesSent += data.length;
|
|
||||||
|
|
||||||
// Check if server socket is writable
|
|
||||||
if (serverSocket && serverSocket.writable) {
|
|
||||||
const flushed = serverSocket.write(data);
|
|
||||||
|
|
||||||
// Handle backpressure
|
|
||||||
if (!flushed) {
|
|
||||||
clientSocket.pause();
|
|
||||||
serverSocket.once('drain', () => {
|
|
||||||
clientSocket.resume();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
|
|
||||||
direction: 'outbound',
|
|
||||||
bytes: data.length,
|
|
||||||
total: bytesSent
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Forward data from server to client
|
|
||||||
serverSocket!.on('data', (data) => {
|
|
||||||
bytesReceived += data.length;
|
|
||||||
|
|
||||||
// Check if client socket is writable
|
|
||||||
if (clientSocket.writable) {
|
|
||||||
const flushed = clientSocket.write(data);
|
|
||||||
|
|
||||||
// Handle backpressure
|
|
||||||
if (!flushed) {
|
|
||||||
serverSocket!.pause();
|
|
||||||
clientSocket.once('drain', () => {
|
|
||||||
serverSocket!.resume();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
|
|
||||||
direction: 'inbound',
|
|
||||||
bytes: data.length,
|
|
||||||
total: bytesReceived
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set initial timeouts - they will be reset on each timeout event
|
|
||||||
clientSocket.setTimeout(timeout);
|
|
||||||
serverSocket!.setTimeout(timeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an HTTP request - HTTPS passthrough doesn't support HTTP
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
||||||
// HTTPS passthrough doesn't support HTTP requests
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end('HTTP not supported for this domain');
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
|
||||||
statusCode: 404,
|
|
||||||
headers: { 'Content-Type': 'text/plain' },
|
|
||||||
size: 'HTTP not supported for this domain'.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,312 +0,0 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
||||||
import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for HTTPS termination with HTTP backend
|
|
||||||
*/
|
|
||||||
export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|
||||||
private tlsServer: plugins.tls.Server | null = null;
|
|
||||||
private secureContext: plugins.tls.SecureContext | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new HTTPS termination with HTTP backend handler
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
*/
|
|
||||||
constructor(config: IForwardConfig) {
|
|
||||||
super(config);
|
|
||||||
|
|
||||||
// Validate that this is an HTTPS terminate to HTTP configuration
|
|
||||||
if (config.type !== 'https-terminate-to-http') {
|
|
||||||
throw new Error(`Invalid configuration type for HttpsTerminateToHttpHandler: ${config.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the handler, setting up TLS context
|
|
||||||
*/
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
// We need to load or create TLS certificates
|
|
||||||
if (this.config.https?.customCert) {
|
|
||||||
// Use custom certificate from configuration
|
|
||||||
this.secureContext = plugins.tls.createSecureContext({
|
|
||||||
key: this.config.https.customCert.key,
|
|
||||||
cert: this.config.https.customCert.cert
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, {
|
|
||||||
source: 'config',
|
|
||||||
domain: this.config.target.host
|
|
||||||
});
|
|
||||||
} else if (this.config.acme?.enabled) {
|
|
||||||
// Request certificate through ACME if needed
|
|
||||||
this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, {
|
|
||||||
domain: Array.isArray(this.config.target.host)
|
|
||||||
? this.config.target.host[0]
|
|
||||||
: this.config.target.host,
|
|
||||||
useProduction: this.config.acme.production || false
|
|
||||||
});
|
|
||||||
|
|
||||||
// In a real implementation, we would wait for the certificate to be issued
|
|
||||||
// For now, we'll use a dummy context
|
|
||||||
this.secureContext = plugins.tls.createSecureContext({
|
|
||||||
key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----',
|
|
||||||
cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('HTTPS termination requires either a custom certificate or ACME enabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the secure context for TLS termination
|
|
||||||
* Called when a certificate is available
|
|
||||||
* @param context The secure context
|
|
||||||
*/
|
|
||||||
public setSecureContext(context: plugins.tls.SecureContext): void {
|
|
||||||
this.secureContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a TLS/SSL socket connection by terminating TLS and forwarding to HTTP backend
|
|
||||||
* @param clientSocket The incoming socket from the client
|
|
||||||
*/
|
|
||||||
public handleConnection(clientSocket: plugins.net.Socket): void {
|
|
||||||
// Make sure we have a secure context
|
|
||||||
if (!this.secureContext) {
|
|
||||||
clientSocket.destroy(new Error('TLS secure context not initialized'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteAddress = clientSocket.remoteAddress || 'unknown';
|
|
||||||
const remotePort = clientSocket.remotePort || 0;
|
|
||||||
|
|
||||||
// Create a TLS socket using our secure context
|
|
||||||
const tlsSocket = new plugins.tls.TLSSocket(clientSocket, {
|
|
||||||
secureContext: this.secureContext,
|
|
||||||
isServer: true,
|
|
||||||
server: this.tlsServer || undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
remotePort,
|
|
||||||
tls: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Variables to track connections
|
|
||||||
let backendSocket: plugins.net.Socket | null = null;
|
|
||||||
let dataBuffer = Buffer.alloc(0);
|
|
||||||
let connectionEstablished = false;
|
|
||||||
let forwardingSetup = false;
|
|
||||||
|
|
||||||
// Set up initial error handling for TLS socket
|
|
||||||
const tlsCleanupHandler = (reason: string) => {
|
|
||||||
if (!forwardingSetup) {
|
|
||||||
// If forwarding not set up yet, emit disconnected and cleanup
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
dataBuffer = Buffer.alloc(0);
|
|
||||||
connectionEstablished = false;
|
|
||||||
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
if (backendSocket && !backendSocket.destroyed) {
|
|
||||||
backendSocket.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If forwarding is setup, setupBidirectionalForwarding will handle cleanup
|
|
||||||
};
|
|
||||||
|
|
||||||
setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
|
|
||||||
|
|
||||||
// Set timeout
|
|
||||||
const timeout = this.getTimeout();
|
|
||||||
tlsSocket.setTimeout(timeout);
|
|
||||||
|
|
||||||
tlsSocket.on('timeout', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: 'TLS connection timeout'
|
|
||||||
});
|
|
||||||
tlsCleanupHandler('timeout');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle TLS data
|
|
||||||
tlsSocket.on('data', (data) => {
|
|
||||||
// If backend connection already established, just forward the data
|
|
||||||
if (connectionEstablished && backendSocket && !backendSocket.destroyed) {
|
|
||||||
backendSocket.write(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append to buffer
|
|
||||||
dataBuffer = Buffer.concat([dataBuffer, data]);
|
|
||||||
|
|
||||||
// Very basic HTTP parsing - in a real implementation, use http-parser
|
|
||||||
if (dataBuffer.includes(Buffer.from('\r\n\r\n')) && !connectionEstablished) {
|
|
||||||
const target = this.getTargetFromConfig();
|
|
||||||
|
|
||||||
// Create backend connection with immediate error handling
|
|
||||||
backendSocket = createSocketWithErrorHandler({
|
|
||||||
port: target.port,
|
|
||||||
host: target.host,
|
|
||||||
onError: (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
error: error.message,
|
|
||||||
code: (error as any).code || 'UNKNOWN',
|
|
||||||
remoteAddress,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up the TLS socket since we can't forward
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason: `backend_connection_failed: ${error.message}`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onConnect: () => {
|
|
||||||
connectionEstablished = true;
|
|
||||||
|
|
||||||
// Send buffered data
|
|
||||||
if (dataBuffer.length > 0) {
|
|
||||||
backendSocket!.write(dataBuffer);
|
|
||||||
dataBuffer = Buffer.alloc(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now set up bidirectional forwarding with proper cleanup
|
|
||||||
forwardingSetup = true;
|
|
||||||
setupBidirectionalForwarding(tlsSocket, backendSocket!, {
|
|
||||||
onCleanup: (reason) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
dataBuffer = Buffer.alloc(0);
|
|
||||||
connectionEstablished = false;
|
|
||||||
forwardingSetup = false;
|
|
||||||
},
|
|
||||||
enableHalfOpen: false // Close both when one closes
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Additional error logging for backend socket
|
|
||||||
backendSocket.on('error', (error) => {
|
|
||||||
if (!connectionEstablished) {
|
|
||||||
// Connection failed during setup
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: `Target connection error: ${error.message}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If connected, setupBidirectionalForwarding handles cleanup
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an HTTP request by forwarding to the HTTP backend
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
||||||
// Check if we should redirect to HTTPS
|
|
||||||
if (this.config.http?.redirectToHttps) {
|
|
||||||
this.redirectToHttps(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the target from configuration
|
|
||||||
const target = this.getTargetFromConfig();
|
|
||||||
|
|
||||||
// Create custom headers with variable substitution
|
|
||||||
const variables = {
|
|
||||||
clientIp: req.socket.remoteAddress || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare headers, merging with any custom headers from config
|
|
||||||
const headers = this.applyCustomHeaders(req.headers, variables);
|
|
||||||
|
|
||||||
// Create the proxy request options
|
|
||||||
const options = {
|
|
||||||
hostname: target.host,
|
|
||||||
port: target.port,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the proxy request
|
|
||||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
|
||||||
// Copy status code and headers from the proxied response
|
|
||||||
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
|
||||||
|
|
||||||
// Pipe the proxy response to the client response
|
|
||||||
proxyRes.pipe(res);
|
|
||||||
|
|
||||||
// Track response size for logging
|
|
||||||
let responseSize = 0;
|
|
||||||
proxyRes.on('data', (chunk) => {
|
|
||||||
responseSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyRes.on('end', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
size: responseSize
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors in the proxy request
|
|
||||||
proxyReq.on('error', (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
error: `Proxy request error: ${error.message}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an error response if headers haven't been sent yet
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Error forwarding request: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
// Just end the response if headers have already been sent
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track request details for logging
|
|
||||||
let requestSize = 0;
|
|
||||||
req.on('data', (chunk) => {
|
|
||||||
requestSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the request
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: req.headers,
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe the client request to the proxy request
|
|
||||||
if (req.readable) {
|
|
||||||
req.pipe(proxyReq);
|
|
||||||
} else {
|
|
||||||
proxyReq.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,297 +0,0 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
|
||||||
import type { IForwardConfig } from '../config/forwarding-types.js';
|
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
|
||||||
import { setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for HTTPS termination with HTTPS backend
|
|
||||||
*/
|
|
||||||
export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|
||||||
private secureContext: plugins.tls.SecureContext | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new HTTPS termination with HTTPS backend handler
|
|
||||||
* @param config The forwarding configuration
|
|
||||||
*/
|
|
||||||
constructor(config: IForwardConfig) {
|
|
||||||
super(config);
|
|
||||||
|
|
||||||
// Validate that this is an HTTPS terminate to HTTPS configuration
|
|
||||||
if (config.type !== 'https-terminate-to-https') {
|
|
||||||
throw new Error(`Invalid configuration type for HttpsTerminateToHttpsHandler: ${config.type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the handler, setting up TLS context
|
|
||||||
*/
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
// We need to load or create TLS certificates for termination
|
|
||||||
if (this.config.https?.customCert) {
|
|
||||||
// Use custom certificate from configuration
|
|
||||||
this.secureContext = plugins.tls.createSecureContext({
|
|
||||||
key: this.config.https.customCert.key,
|
|
||||||
cert: this.config.https.customCert.cert
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CERTIFICATE_LOADED, {
|
|
||||||
source: 'config',
|
|
||||||
domain: this.config.target.host
|
|
||||||
});
|
|
||||||
} else if (this.config.acme?.enabled) {
|
|
||||||
// Request certificate through ACME if needed
|
|
||||||
this.emit(ForwardingHandlerEvents.CERTIFICATE_NEEDED, {
|
|
||||||
domain: Array.isArray(this.config.target.host)
|
|
||||||
? this.config.target.host[0]
|
|
||||||
: this.config.target.host,
|
|
||||||
useProduction: this.config.acme.production || false
|
|
||||||
});
|
|
||||||
|
|
||||||
// In a real implementation, we would wait for the certificate to be issued
|
|
||||||
// For now, we'll use a dummy context
|
|
||||||
this.secureContext = plugins.tls.createSecureContext({
|
|
||||||
key: '-----BEGIN PRIVATE KEY-----\nDummy key\n-----END PRIVATE KEY-----',
|
|
||||||
cert: '-----BEGIN CERTIFICATE-----\nDummy cert\n-----END CERTIFICATE-----'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error('HTTPS termination requires either a custom certificate or ACME enabled');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the secure context for TLS termination
|
|
||||||
* Called when a certificate is available
|
|
||||||
* @param context The secure context
|
|
||||||
*/
|
|
||||||
public setSecureContext(context: plugins.tls.SecureContext): void {
|
|
||||||
this.secureContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a TLS/SSL socket connection by terminating TLS and creating a new TLS connection to backend
|
|
||||||
* @param clientSocket The incoming socket from the client
|
|
||||||
*/
|
|
||||||
public handleConnection(clientSocket: plugins.net.Socket): void {
|
|
||||||
// Make sure we have a secure context
|
|
||||||
if (!this.secureContext) {
|
|
||||||
clientSocket.destroy(new Error('TLS secure context not initialized'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteAddress = clientSocket.remoteAddress || 'unknown';
|
|
||||||
const remotePort = clientSocket.remotePort || 0;
|
|
||||||
|
|
||||||
// Create a TLS socket using our secure context
|
|
||||||
const tlsSocket = new plugins.tls.TLSSocket(clientSocket, {
|
|
||||||
secureContext: this.secureContext,
|
|
||||||
isServer: true
|
|
||||||
});
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.CONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
remotePort,
|
|
||||||
tls: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Variable to track backend socket
|
|
||||||
let backendSocket: plugins.tls.TLSSocket | null = null;
|
|
||||||
let isConnectedToBackend = false;
|
|
||||||
|
|
||||||
// Set up initial error handling for TLS socket
|
|
||||||
const tlsCleanupHandler = (reason: string) => {
|
|
||||||
if (!isConnectedToBackend) {
|
|
||||||
// If backend not connected yet, just emit disconnected event
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cleanup TLS socket if needed
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If connected to backend, setupBidirectionalForwarding will handle cleanup
|
|
||||||
};
|
|
||||||
|
|
||||||
setupSocketHandlers(tlsSocket, tlsCleanupHandler, undefined, 'tls');
|
|
||||||
|
|
||||||
// Set timeout
|
|
||||||
const timeout = this.getTimeout();
|
|
||||||
tlsSocket.setTimeout(timeout);
|
|
||||||
|
|
||||||
tlsSocket.on('timeout', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: 'TLS connection timeout'
|
|
||||||
});
|
|
||||||
tlsCleanupHandler('timeout');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get the target from configuration
|
|
||||||
const target = this.getTargetFromConfig();
|
|
||||||
|
|
||||||
// Set up the connection to the HTTPS backend
|
|
||||||
const connectToBackend = () => {
|
|
||||||
backendSocket = plugins.tls.connect({
|
|
||||||
host: target.host,
|
|
||||||
port: target.port,
|
|
||||||
// In a real implementation, we would configure TLS options
|
|
||||||
rejectUnauthorized: false // For testing only, never use in production
|
|
||||||
}, () => {
|
|
||||||
isConnectedToBackend = true;
|
|
||||||
|
|
||||||
this.emit(ForwardingHandlerEvents.DATA_FORWARDED, {
|
|
||||||
direction: 'outbound',
|
|
||||||
target: `${target.host}:${target.port}`,
|
|
||||||
tls: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up bidirectional forwarding with proper cleanup
|
|
||||||
setupBidirectionalForwarding(tlsSocket, backendSocket!, {
|
|
||||||
onCleanup: (reason) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
},
|
|
||||||
enableHalfOpen: false // Close both when one closes
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set timeout for backend socket
|
|
||||||
backendSocket!.setTimeout(timeout);
|
|
||||||
|
|
||||||
backendSocket!.on('timeout', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: 'Backend connection timeout'
|
|
||||||
});
|
|
||||||
// Let setupBidirectionalForwarding handle the cleanup
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle backend connection errors
|
|
||||||
backendSocket.on('error', (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress,
|
|
||||||
error: `Backend connection error: ${error.message}`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isConnectedToBackend) {
|
|
||||||
// Connection failed, clean up TLS socket
|
|
||||||
if (!tlsSocket.destroyed) {
|
|
||||||
tlsSocket.destroy();
|
|
||||||
}
|
|
||||||
this.emit(ForwardingHandlerEvents.DISCONNECTED, {
|
|
||||||
remoteAddress,
|
|
||||||
reason: `backend_connection_failed: ${error.message}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If connected, let setupBidirectionalForwarding handle cleanup
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wait for the TLS handshake to complete before connecting to backend
|
|
||||||
tlsSocket.on('secure', () => {
|
|
||||||
connectToBackend();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an HTTP request by forwarding to the HTTPS backend
|
|
||||||
* @param req The HTTP request
|
|
||||||
* @param res The HTTP response
|
|
||||||
*/
|
|
||||||
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
|
|
||||||
// Check if we should redirect to HTTPS
|
|
||||||
if (this.config.http?.redirectToHttps) {
|
|
||||||
this.redirectToHttps(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the target from configuration
|
|
||||||
const target = this.getTargetFromConfig();
|
|
||||||
|
|
||||||
// Create custom headers with variable substitution
|
|
||||||
const variables = {
|
|
||||||
clientIp: req.socket.remoteAddress || 'unknown'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare headers, merging with any custom headers from config
|
|
||||||
const headers = this.applyCustomHeaders(req.headers, variables);
|
|
||||||
|
|
||||||
// Create the proxy request options
|
|
||||||
const options = {
|
|
||||||
hostname: target.host,
|
|
||||||
port: target.port,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers,
|
|
||||||
// In a real implementation, we would configure TLS options
|
|
||||||
rejectUnauthorized: false // For testing only, never use in production
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the proxy request using HTTPS
|
|
||||||
const proxyReq = plugins.https.request(options, (proxyRes) => {
|
|
||||||
// Copy status code and headers from the proxied response
|
|
||||||
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
|
|
||||||
|
|
||||||
// Pipe the proxy response to the client response
|
|
||||||
proxyRes.pipe(res);
|
|
||||||
|
|
||||||
// Track response size for logging
|
|
||||||
let responseSize = 0;
|
|
||||||
proxyRes.on('data', (chunk) => {
|
|
||||||
responseSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
proxyRes.on('end', () => {
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_RESPONSE, {
|
|
||||||
statusCode: proxyRes.statusCode,
|
|
||||||
headers: proxyRes.headers,
|
|
||||||
size: responseSize
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors in the proxy request
|
|
||||||
proxyReq.on('error', (error) => {
|
|
||||||
this.emit(ForwardingHandlerEvents.ERROR, {
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
error: `Proxy request error: ${error.message}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send an error response if headers haven't been sent yet
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
||||||
res.end(`Error forwarding request: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
// Just end the response if headers have already been sent
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track request details for logging
|
|
||||||
let requestSize = 0;
|
|
||||||
req.on('data', (chunk) => {
|
|
||||||
requestSize += chunk.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the request
|
|
||||||
this.emit(ForwardingHandlerEvents.HTTP_REQUEST, {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: req.headers,
|
|
||||||
remoteAddress: req.socket.remoteAddress,
|
|
||||||
target: `${target.host}:${target.port}`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe the client request to the proxy request
|
|
||||||
if (req.readable) {
|
|
||||||
req.pipe(proxyReq);
|
|
||||||
} else {
|
|
||||||
proxyReq.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Forwarding handler implementations
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { ForwardingHandler } from './base-handler.js';
|
|
||||||
export { HttpForwardingHandler } from './http-handler.js';
|
|
||||||
export { HttpsPassthroughHandler } from './https-passthrough-handler.js';
|
|
||||||
export { HttpsTerminateToHttpHandler } from './https-terminate-to-http-handler.js';
|
|
||||||
export { HttpsTerminateToHttpsHandler } from './https-terminate-to-https-handler.js';
|
|
@@ -1,35 +0,0 @@
|
|||||||
/**
|
|
||||||
* Forwarding system module
|
|
||||||
* Provides a flexible and type-safe way to configure and manage various forwarding strategies
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Export handlers
|
|
||||||
export { ForwardingHandler } from './handlers/base-handler.js';
|
|
||||||
export * from './handlers/http-handler.js';
|
|
||||||
export * from './handlers/https-passthrough-handler.js';
|
|
||||||
export * from './handlers/https-terminate-to-http-handler.js';
|
|
||||||
export * from './handlers/https-terminate-to-https-handler.js';
|
|
||||||
|
|
||||||
// Export factory
|
|
||||||
export * from './factory/forwarding-factory.js';
|
|
||||||
|
|
||||||
// Export types - these include TForwardingType and IForwardConfig
|
|
||||||
export type {
|
|
||||||
TForwardingType,
|
|
||||||
IForwardConfig,
|
|
||||||
IForwardingHandler
|
|
||||||
} from './config/forwarding-types.js';
|
|
||||||
|
|
||||||
export {
|
|
||||||
ForwardingHandlerEvents
|
|
||||||
} from './config/forwarding-types.js';
|
|
||||||
|
|
||||||
// Export route helpers directly from route-patterns
|
|
||||||
export {
|
|
||||||
createHttpRoute,
|
|
||||||
createHttpsTerminateRoute,
|
|
||||||
createHttpsPassthroughRoute,
|
|
||||||
createHttpToHttpsRedirect,
|
|
||||||
createCompleteHttpsServer,
|
|
||||||
createLoadBalancerRoute
|
|
||||||
} from '../proxies/smart-proxy/utils/route-patterns.js';
|
|
@@ -32,7 +32,6 @@ export * from './core/models/common-types.js';
|
|||||||
export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js';
|
export type { IAcmeOptions } from './proxies/smart-proxy/models/interfaces.js';
|
||||||
|
|
||||||
// Modular exports for new architecture
|
// Modular exports for new architecture
|
||||||
export * as forwarding from './forwarding/index.js';
|
|
||||||
// Certificate module has been removed - use SmartCertManager instead
|
// Certificate module has been removed - use SmartCertManager instead
|
||||||
export * as tls from './tls/index.js';
|
export * as tls from './tls/index.js';
|
||||||
export * as routing from './routing/index.js';
|
export * as routing from './routing/index.js';
|
@@ -10,7 +10,7 @@ import { ConnectionPool } from './connection-pool.js';
|
|||||||
import { ContextCreator } from './context-creator.js';
|
import { ContextCreator } from './context-creator.js';
|
||||||
import { HttpRequestHandler } from './http-request-handler.js';
|
import { HttpRequestHandler } from './http-request-handler.js';
|
||||||
import { Http2RequestHandler } from './http2-request-handler.js';
|
import { Http2RequestHandler } from './http2-request-handler.js';
|
||||||
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
|
import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
|
||||||
import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js';
|
import type { IRouteContext, IHttpRouteContext } from '../../core/models/route-context.js';
|
||||||
import { toBaseContext } from '../../core/models/route-context.js';
|
import { toBaseContext } from '../../core/models/route-context.js';
|
||||||
import { TemplateUtils } from '../../core/utils/template-utils.js';
|
import { TemplateUtils } from '../../core/utils/template-utils.js';
|
||||||
@@ -99,6 +99,80 @@ export class RequestHandler {
|
|||||||
return { ...this.defaultHeaders };
|
return { ...this.defaultHeaders };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the appropriate target from the targets array based on sub-matching criteria
|
||||||
|
*/
|
||||||
|
private selectTarget(
|
||||||
|
targets: IRouteTarget[],
|
||||||
|
context: {
|
||||||
|
port: number;
|
||||||
|
path?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
method?: string;
|
||||||
|
}
|
||||||
|
): IRouteTarget | null {
|
||||||
|
// Sort targets by priority (higher first)
|
||||||
|
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
// Find the first matching target
|
||||||
|
for (const target of sortedTargets) {
|
||||||
|
if (!target.match) {
|
||||||
|
// No match criteria means this is a default/fallback target
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check port match
|
||||||
|
if (target.match.ports && !target.match.ports.includes(context.port)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path match (supports wildcards)
|
||||||
|
if (target.match.path && context.path) {
|
||||||
|
const pathPattern = target.match.path.replace(/\*/g, '.*');
|
||||||
|
const pathRegex = new RegExp(`^${pathPattern}$`);
|
||||||
|
if (!pathRegex.test(context.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check method match
|
||||||
|
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check headers match
|
||||||
|
if (target.match.headers && context.headers) {
|
||||||
|
let headersMatch = true;
|
||||||
|
for (const [key, pattern] of Object.entries(target.match.headers)) {
|
||||||
|
const headerValue = context.headers[key.toLowerCase()];
|
||||||
|
if (!headerValue) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
if (!pattern.test(headerValue)) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (headerValue !== pattern) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!headersMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All criteria matched
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matching target found, return the first target without match criteria (default)
|
||||||
|
return sortedTargets.find(t => !t.match) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply CORS headers to response if configured
|
* Apply CORS headers to response if configured
|
||||||
* Implements Phase 5.5: Context-aware CORS handling
|
* Implements Phase 5.5: Context-aware CORS handling
|
||||||
@@ -480,17 +554,31 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a matching route with function-based targets, use it
|
// If we found a matching route with forward action, select appropriate target
|
||||||
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.target) {
|
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
|
||||||
this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`);
|
this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`);
|
||||||
|
|
||||||
|
// Select the appropriate target from the targets array
|
||||||
|
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
|
||||||
|
port: routeContext.port,
|
||||||
|
path: routeContext.path,
|
||||||
|
headers: routeContext.headers,
|
||||||
|
method: routeContext.method
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedTarget) {
|
||||||
|
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
|
||||||
|
req.socket.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract target information, resolving functions if needed
|
// Extract target information, resolving functions if needed
|
||||||
let targetHost: string | string[];
|
let targetHost: string | string[];
|
||||||
let targetPort: number;
|
let targetPort: number;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check function cache for host and resolve or use cached value
|
// Check function cache for host and resolve or use cached value
|
||||||
if (typeof matchingRoute.action.target.host === 'function') {
|
if (typeof selectedTarget.host === 'function') {
|
||||||
// Generate a function ID for caching (use route name or ID if available)
|
// Generate a function ID for caching (use route name or ID if available)
|
||||||
const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
||||||
|
|
||||||
@@ -502,7 +590,7 @@ export class RequestHandler {
|
|||||||
this.logger.debug(`Using cached host value for ${functionId}`);
|
this.logger.debug(`Using cached host value for ${functionId}`);
|
||||||
} else {
|
} else {
|
||||||
// Resolve the function and cache the result
|
// Resolve the function and cache the result
|
||||||
const resolvedHost = matchingRoute.action.target.host(toBaseContext(routeContext));
|
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
||||||
targetHost = resolvedHost;
|
targetHost = resolvedHost;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
@@ -511,16 +599,16 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No cache available, just resolve
|
// No cache available, just resolve
|
||||||
const resolvedHost = matchingRoute.action.target.host(routeContext);
|
const resolvedHost = selectedTarget.host(routeContext);
|
||||||
targetHost = resolvedHost;
|
targetHost = resolvedHost;
|
||||||
this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetHost = matchingRoute.action.target.host;
|
targetHost = selectedTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check function cache for port and resolve or use cached value
|
// Check function cache for port and resolve or use cached value
|
||||||
if (typeof matchingRoute.action.target.port === 'function') {
|
if (typeof selectedTarget.port === 'function') {
|
||||||
// Generate a function ID for caching
|
// Generate a function ID for caching
|
||||||
const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
||||||
|
|
||||||
@@ -532,7 +620,7 @@ export class RequestHandler {
|
|||||||
this.logger.debug(`Using cached port value for ${functionId}`);
|
this.logger.debug(`Using cached port value for ${functionId}`);
|
||||||
} else {
|
} else {
|
||||||
// Resolve the function and cache the result
|
// Resolve the function and cache the result
|
||||||
const resolvedPort = matchingRoute.action.target.port(toBaseContext(routeContext));
|
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
|
||||||
targetPort = resolvedPort;
|
targetPort = resolvedPort;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
@@ -541,12 +629,12 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No cache available, just resolve
|
// No cache available, just resolve
|
||||||
const resolvedPort = matchingRoute.action.target.port(routeContext);
|
const resolvedPort = selectedTarget.port(routeContext);
|
||||||
targetPort = resolvedPort;
|
targetPort = resolvedPort;
|
||||||
this.logger.debug(`Resolved function-based port to: ${resolvedPort}`);
|
this.logger.debug(`Resolved function-based port to: ${resolvedPort}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number;
|
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select a single host if an array was provided
|
// Select a single host if an array was provided
|
||||||
@@ -626,17 +714,32 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a matching route with function-based targets, use it
|
// If we found a matching route with forward action, select appropriate target
|
||||||
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.target) {
|
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
|
||||||
this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`);
|
this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`);
|
||||||
|
|
||||||
|
// Select the appropriate target from the targets array
|
||||||
|
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
|
||||||
|
port: routeContext.port,
|
||||||
|
path: routeContext.path,
|
||||||
|
headers: routeContext.headers,
|
||||||
|
method: routeContext.method
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedTarget) {
|
||||||
|
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
|
||||||
|
stream.respond({ ':status': 502 });
|
||||||
|
stream.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract target information, resolving functions if needed
|
// Extract target information, resolving functions if needed
|
||||||
let targetHost: string | string[];
|
let targetHost: string | string[];
|
||||||
let targetPort: number;
|
let targetPort: number;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check function cache for host and resolve or use cached value
|
// Check function cache for host and resolve or use cached value
|
||||||
if (typeof matchingRoute.action.target.host === 'function') {
|
if (typeof selectedTarget.host === 'function') {
|
||||||
// Generate a function ID for caching (use route name or ID if available)
|
// Generate a function ID for caching (use route name or ID if available)
|
||||||
const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
||||||
|
|
||||||
@@ -648,7 +751,7 @@ export class RequestHandler {
|
|||||||
this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`);
|
this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`);
|
||||||
} else {
|
} else {
|
||||||
// Resolve the function and cache the result
|
// Resolve the function and cache the result
|
||||||
const resolvedHost = matchingRoute.action.target.host(toBaseContext(routeContext));
|
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
||||||
targetHost = resolvedHost;
|
targetHost = resolvedHost;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
@@ -657,16 +760,16 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No cache available, just resolve
|
// No cache available, just resolve
|
||||||
const resolvedHost = matchingRoute.action.target.host(routeContext);
|
const resolvedHost = selectedTarget.host(routeContext);
|
||||||
targetHost = resolvedHost;
|
targetHost = resolvedHost;
|
||||||
this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetHost = matchingRoute.action.target.host;
|
targetHost = selectedTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check function cache for port and resolve or use cached value
|
// Check function cache for port and resolve or use cached value
|
||||||
if (typeof matchingRoute.action.target.port === 'function') {
|
if (typeof selectedTarget.port === 'function') {
|
||||||
// Generate a function ID for caching
|
// Generate a function ID for caching
|
||||||
const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
|
||||||
|
|
||||||
@@ -678,7 +781,7 @@ export class RequestHandler {
|
|||||||
this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`);
|
this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`);
|
||||||
} else {
|
} else {
|
||||||
// Resolve the function and cache the result
|
// Resolve the function and cache the result
|
||||||
const resolvedPort = matchingRoute.action.target.port(toBaseContext(routeContext));
|
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
|
||||||
targetPort = resolvedPort;
|
targetPort = resolvedPort;
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
@@ -687,12 +790,12 @@ export class RequestHandler {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No cache available, just resolve
|
// No cache available, just resolve
|
||||||
const resolvedPort = matchingRoute.action.target.port(routeContext);
|
const resolvedPort = selectedTarget.port(routeContext);
|
||||||
targetPort = resolvedPort;
|
targetPort = resolvedPort;
|
||||||
this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`);
|
this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetPort = matchingRoute.action.target.port === 'preserve' ? routeContext.port : matchingRoute.action.target.port as number;
|
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select a single host if an array was provided
|
// Select a single host if an array was provided
|
||||||
|
@@ -3,7 +3,7 @@ import '../../core/models/socket-augmentation.js';
|
|||||||
import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger } from './models/types.js';
|
import { type IHttpProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger } from './models/types.js';
|
||||||
import { ConnectionPool } from './connection-pool.js';
|
import { ConnectionPool } from './connection-pool.js';
|
||||||
import { HttpRouter } from '../../routing/router/index.js';
|
import { HttpRouter } from '../../routing/router/index.js';
|
||||||
import type { IRouteConfig } from '../smart-proxy/models/route-types.js';
|
import type { IRouteConfig, IRouteTarget } from '../smart-proxy/models/route-types.js';
|
||||||
import type { IRouteContext } from '../../core/models/route-context.js';
|
import type { IRouteContext } from '../../core/models/route-context.js';
|
||||||
import { toBaseContext } from '../../core/models/route-context.js';
|
import { toBaseContext } from '../../core/models/route-context.js';
|
||||||
import { ContextCreator } from './context-creator.js';
|
import { ContextCreator } from './context-creator.js';
|
||||||
@@ -53,6 +53,80 @@ export class WebSocketHandler {
|
|||||||
this.securityManager.setRoutes(routes);
|
this.securityManager.setRoutes(routes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the appropriate target from the targets array based on sub-matching criteria
|
||||||
|
*/
|
||||||
|
private selectTarget(
|
||||||
|
targets: IRouteTarget[],
|
||||||
|
context: {
|
||||||
|
port: number;
|
||||||
|
path?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
method?: string;
|
||||||
|
}
|
||||||
|
): IRouteTarget | null {
|
||||||
|
// Sort targets by priority (higher first)
|
||||||
|
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
// Find the first matching target
|
||||||
|
for (const target of sortedTargets) {
|
||||||
|
if (!target.match) {
|
||||||
|
// No match criteria means this is a default/fallback target
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check port match
|
||||||
|
if (target.match.ports && !target.match.ports.includes(context.port)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path match (supports wildcards)
|
||||||
|
if (target.match.path && context.path) {
|
||||||
|
const pathPattern = target.match.path.replace(/\*/g, '.*');
|
||||||
|
const pathRegex = new RegExp(`^${pathPattern}$`);
|
||||||
|
if (!pathRegex.test(context.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check method match
|
||||||
|
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check headers match
|
||||||
|
if (target.match.headers && context.headers) {
|
||||||
|
let headersMatch = true;
|
||||||
|
for (const [key, pattern] of Object.entries(target.match.headers)) {
|
||||||
|
const headerValue = context.headers[key.toLowerCase()];
|
||||||
|
if (!headerValue) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
if (!pattern.test(headerValue)) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (headerValue !== pattern) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!headersMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All criteria matched
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matching target found, return the first target without match criteria (default)
|
||||||
|
return sortedTargets.find(t => !t.match) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize WebSocket server on an existing HTTPS server
|
* Initialize WebSocket server on an existing HTTPS server
|
||||||
*/
|
*/
|
||||||
@@ -146,9 +220,23 @@ export class WebSocketHandler {
|
|||||||
let destination: { host: string; port: number };
|
let destination: { host: string; port: number };
|
||||||
|
|
||||||
// If we found a route with the modern router, use it
|
// If we found a route with the modern router, use it
|
||||||
if (route && route.action.type === 'forward' && route.action.target) {
|
if (route && route.action.type === 'forward' && route.action.targets && route.action.targets.length > 0) {
|
||||||
this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
|
this.logger.debug(`Found matching WebSocket route: ${route.name || 'unnamed'}`);
|
||||||
|
|
||||||
|
// Select the appropriate target from the targets array
|
||||||
|
const selectedTarget = this.selectTarget(route.action.targets, {
|
||||||
|
port: routeContext.port,
|
||||||
|
path: routeContext.path,
|
||||||
|
headers: routeContext.headers,
|
||||||
|
method: routeContext.method
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selectedTarget) {
|
||||||
|
this.logger.error(`No matching target found for route ${route.name}`);
|
||||||
|
wsIncoming.close(1003, 'No matching target');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if WebSockets are enabled for this route
|
// Check if WebSockets are enabled for this route
|
||||||
if (route.action.websocket?.enabled === false) {
|
if (route.action.websocket?.enabled === false) {
|
||||||
this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
|
this.logger.debug(`WebSockets are disabled for route: ${route.name || 'unnamed'}`);
|
||||||
@@ -192,20 +280,20 @@ export class WebSocketHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Resolve host if it's a function
|
// Resolve host if it's a function
|
||||||
if (typeof route.action.target.host === 'function') {
|
if (typeof selectedTarget.host === 'function') {
|
||||||
const resolvedHost = route.action.target.host(toBaseContext(routeContext));
|
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
|
||||||
targetHost = resolvedHost;
|
targetHost = resolvedHost;
|
||||||
this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
this.logger.debug(`Resolved function-based host for WebSocket: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
|
||||||
} else {
|
} else {
|
||||||
targetHost = route.action.target.host;
|
targetHost = selectedTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve port if it's a function
|
// Resolve port if it's a function
|
||||||
if (typeof route.action.target.port === 'function') {
|
if (typeof selectedTarget.port === 'function') {
|
||||||
targetPort = route.action.target.port(toBaseContext(routeContext));
|
targetPort = selectedTarget.port(toBaseContext(routeContext));
|
||||||
this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
|
this.logger.debug(`Resolved function-based port for WebSocket: ${targetPort}`);
|
||||||
} else {
|
} else {
|
||||||
targetPort = route.action.target.port === 'preserve' ? routeContext.port : route.action.target.port as number;
|
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select a single host if an array was provided
|
// Select a single host if an array was provided
|
||||||
|
@@ -16,7 +16,6 @@ export interface IAcmeOptions {
|
|||||||
routeForwards?: any[];
|
routeForwards?: any[];
|
||||||
}
|
}
|
||||||
import type { IRouteConfig } from './route-types.js';
|
import type { IRouteConfig } from './route-types.js';
|
||||||
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provision object for static or HTTP-01 certificate
|
* Provision object for static or HTTP-01 certificate
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
// Certificate types removed - use local definition
|
// Certificate types removed - use local definition
|
||||||
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
|
|
||||||
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
|
import type { PortRange } from '../../../proxies/nftables-proxy/models/interfaces.js';
|
||||||
import type { IRouteContext } from '../../../core/models/route-context.js';
|
import type { IRouteContext } from '../../../core/models/route-context.js';
|
||||||
|
|
||||||
@@ -46,11 +45,36 @@ export interface IRouteMatch {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Target configuration for forwarding
|
* Target-specific match criteria for sub-routing within a route
|
||||||
|
*/
|
||||||
|
export interface ITargetMatch {
|
||||||
|
ports?: number[]; // Match specific ports from the route
|
||||||
|
path?: string; // Match specific paths (supports wildcards like /api/*)
|
||||||
|
headers?: Record<string, string | RegExp>; // Match specific HTTP headers
|
||||||
|
method?: string[]; // Match specific HTTP methods (GET, POST, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target configuration for forwarding with sub-matching and overrides
|
||||||
*/
|
*/
|
||||||
export interface IRouteTarget {
|
export interface IRouteTarget {
|
||||||
|
// Optional sub-matching criteria within the route
|
||||||
|
match?: ITargetMatch;
|
||||||
|
|
||||||
|
// Target destination
|
||||||
host: string | string[] | ((context: IRouteContext) => string | string[]); // Host or hosts with optional function for dynamic resolution
|
host: string | string[] | ((context: IRouteContext) => string | string[]); // Host or hosts with optional function for dynamic resolution
|
||||||
port: number | 'preserve' | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port)
|
port: number | 'preserve' | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping (use 'preserve' to keep the incoming port)
|
||||||
|
|
||||||
|
// Optional target-specific overrides (these override route-level settings)
|
||||||
|
tls?: IRouteTls; // Override route-level TLS settings
|
||||||
|
websocket?: IRouteWebSocket; // Override route-level WebSocket settings
|
||||||
|
loadBalancing?: IRouteLoadBalancing; // Override route-level load balancing
|
||||||
|
sendProxyProtocol?: boolean; // Override route-level proxy protocol setting
|
||||||
|
headers?: IRouteHeaders; // Override route-level headers
|
||||||
|
advanced?: IRouteAdvanced; // Override route-level advanced settings
|
||||||
|
|
||||||
|
// Priority for matching (higher values are checked first, default: 0)
|
||||||
|
priority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,19 +245,20 @@ export interface IRouteAction {
|
|||||||
// Basic routing
|
// Basic routing
|
||||||
type: TRouteActionType;
|
type: TRouteActionType;
|
||||||
|
|
||||||
// Target for forwarding
|
// Targets for forwarding (array supports multiple targets with sub-matching)
|
||||||
target?: IRouteTarget;
|
// Required for 'forward' action type
|
||||||
|
targets?: IRouteTarget[];
|
||||||
|
|
||||||
// TLS handling
|
// TLS handling (default for all targets, can be overridden per target)
|
||||||
tls?: IRouteTls;
|
tls?: IRouteTls;
|
||||||
|
|
||||||
// WebSocket support
|
// WebSocket support (default for all targets, can be overridden per target)
|
||||||
websocket?: IRouteWebSocket;
|
websocket?: IRouteWebSocket;
|
||||||
|
|
||||||
// Load balancing options
|
// Load balancing options (default for all targets, can be overridden per target)
|
||||||
loadBalancing?: IRouteLoadBalancing;
|
loadBalancing?: IRouteLoadBalancing;
|
||||||
|
|
||||||
// Advanced options
|
// Advanced options (default for all targets, can be overridden per target)
|
||||||
advanced?: IRouteAdvanced;
|
advanced?: IRouteAdvanced;
|
||||||
|
|
||||||
// Additional options for backend-specific settings
|
// Additional options for backend-specific settings
|
||||||
@@ -251,7 +276,7 @@ export interface IRouteAction {
|
|||||||
// Socket handler function (when type is 'socket-handler')
|
// Socket handler function (when type is 'socket-handler')
|
||||||
socketHandler?: TSocketHandler;
|
socketHandler?: TSocketHandler;
|
||||||
|
|
||||||
// PROXY protocol support
|
// PROXY protocol support (default for all targets, can be overridden per target)
|
||||||
sendProxyProtocol?: boolean;
|
sendProxyProtocol?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -123,39 +123,43 @@ export class NFTablesManager {
|
|||||||
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
private createNfTablesOptions(route: IRouteConfig): NfTableProxyOptions {
|
||||||
const { action } = route;
|
const { action } = route;
|
||||||
|
|
||||||
// Ensure we have a target
|
// Ensure we have targets
|
||||||
if (!action.target) {
|
if (!action.targets || action.targets.length === 0) {
|
||||||
throw new Error('Route must have a target to use NFTables forwarding');
|
throw new Error('Route must have targets to use NFTables forwarding');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NFTables can only handle a single target, so we use the first target without match criteria
|
||||||
|
// or the first target if all have match criteria
|
||||||
|
const defaultTarget = action.targets.find(t => !t.match) || action.targets[0];
|
||||||
|
|
||||||
// Convert port specifications
|
// Convert port specifications
|
||||||
const fromPorts = this.expandPortRange(route.match.ports);
|
const fromPorts = this.expandPortRange(route.match.ports);
|
||||||
|
|
||||||
// Determine target port
|
// Determine target port
|
||||||
let toPorts: number | PortRange | Array<number | PortRange>;
|
let toPorts: number | PortRange | Array<number | PortRange>;
|
||||||
|
|
||||||
if (action.target.port === 'preserve') {
|
if (defaultTarget.port === 'preserve') {
|
||||||
// 'preserve' means use the same ports as the source
|
// 'preserve' means use the same ports as the source
|
||||||
toPorts = fromPorts;
|
toPorts = fromPorts;
|
||||||
} else if (typeof action.target.port === 'function') {
|
} else if (typeof defaultTarget.port === 'function') {
|
||||||
// For function-based ports, we can't determine at setup time
|
// For function-based ports, we can't determine at setup time
|
||||||
// Use the "preserve" approach and let NFTables handle it
|
// Use the "preserve" approach and let NFTables handle it
|
||||||
toPorts = fromPorts;
|
toPorts = fromPorts;
|
||||||
} else {
|
} else {
|
||||||
toPorts = action.target.port;
|
toPorts = defaultTarget.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine target host
|
// Determine target host
|
||||||
let toHost: string;
|
let toHost: string;
|
||||||
if (typeof action.target.host === 'function') {
|
if (typeof defaultTarget.host === 'function') {
|
||||||
// Can't determine at setup time, use localhost as a placeholder
|
// Can't determine at setup time, use localhost as a placeholder
|
||||||
// and rely on run-time handling
|
// and rely on run-time handling
|
||||||
toHost = 'localhost';
|
toHost = 'localhost';
|
||||||
} else if (Array.isArray(action.target.host)) {
|
} else if (Array.isArray(defaultTarget.host)) {
|
||||||
// Use first host for now - NFTables will do simple round-robin
|
// Use first host for now - NFTables will do simple round-robin
|
||||||
toHost = action.target.host[0];
|
toHost = defaultTarget.host[0];
|
||||||
} else {
|
} else {
|
||||||
toHost = action.target.host;
|
toHost = defaultTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create options
|
// Create options
|
||||||
|
@@ -3,7 +3,7 @@ import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.
|
|||||||
import { logger } from '../../core/utils/logger.js';
|
import { logger } from '../../core/utils/logger.js';
|
||||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||||
// Route checking functions have been removed
|
// Route checking functions have been removed
|
||||||
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
|
import type { IRouteConfig, IRouteAction, IRouteTarget } from './models/route-types.js';
|
||||||
import type { IRouteContext } from '../../core/models/route-context.js';
|
import type { IRouteContext } from '../../core/models/route-context.js';
|
||||||
import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
|
||||||
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
||||||
@@ -657,6 +657,80 @@ export class RouteConnectionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select the appropriate target from the targets array based on sub-matching criteria
|
||||||
|
*/
|
||||||
|
private selectTarget(
|
||||||
|
targets: IRouteTarget[],
|
||||||
|
context: {
|
||||||
|
port: number;
|
||||||
|
path?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
method?: string;
|
||||||
|
}
|
||||||
|
): IRouteTarget | null {
|
||||||
|
// Sort targets by priority (higher first)
|
||||||
|
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
// Find the first matching target
|
||||||
|
for (const target of sortedTargets) {
|
||||||
|
if (!target.match) {
|
||||||
|
// No match criteria means this is a default/fallback target
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check port match
|
||||||
|
if (target.match.ports && !target.match.ports.includes(context.port)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path match (supports wildcards)
|
||||||
|
if (target.match.path && context.path) {
|
||||||
|
const pathPattern = target.match.path.replace(/\*/g, '.*');
|
||||||
|
const pathRegex = new RegExp(`^${pathPattern}$`);
|
||||||
|
if (!pathRegex.test(context.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check method match
|
||||||
|
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check headers match
|
||||||
|
if (target.match.headers && context.headers) {
|
||||||
|
let headersMatch = true;
|
||||||
|
for (const [key, pattern] of Object.entries(target.match.headers)) {
|
||||||
|
const headerValue = context.headers[key.toLowerCase()];
|
||||||
|
if (!headerValue) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
if (!pattern.test(headerValue)) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (headerValue !== pattern) {
|
||||||
|
headersMatch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!headersMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All criteria matched
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No matching target found, return the first target without match criteria (default)
|
||||||
|
return sortedTargets.find(t => !t.match) || null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a forward action for a route
|
* Handle a forward action for a route
|
||||||
*/
|
*/
|
||||||
@@ -731,14 +805,37 @@ export class RouteConnectionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should have a target configuration for forwarding
|
// Select the appropriate target from the targets array
|
||||||
if (!action.target) {
|
if (!action.targets || action.targets.length === 0) {
|
||||||
logger.log('error', `Forward action missing target configuration for connection ${connectionId}`, {
|
logger.log('error', `Forward action missing targets configuration for connection ${connectionId}`, {
|
||||||
connectionId,
|
connectionId,
|
||||||
component: 'route-handler'
|
component: 'route-handler'
|
||||||
});
|
});
|
||||||
socket.end();
|
socket.end();
|
||||||
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_target');
|
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_targets');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context for target selection
|
||||||
|
const targetSelectionContext = {
|
||||||
|
port: record.localPort,
|
||||||
|
path: undefined, // Will be populated from HTTP headers if available
|
||||||
|
headers: undefined, // Will be populated from HTTP headers if available
|
||||||
|
method: undefined // Will be populated from HTTP headers if available
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Extract path, headers, and method from initialChunk if it's HTTP
|
||||||
|
// For now, we'll select based on port only
|
||||||
|
|
||||||
|
const selectedTarget = this.selectTarget(action.targets, targetSelectionContext);
|
||||||
|
if (!selectedTarget) {
|
||||||
|
logger.log('error', `No matching target found for connection ${connectionId}`, {
|
||||||
|
connectionId,
|
||||||
|
port: targetSelectionContext.port,
|
||||||
|
component: 'route-handler'
|
||||||
|
});
|
||||||
|
socket.end();
|
||||||
|
this.smartProxy.connectionManager.cleanupConnection(record, 'no_matching_target');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,9 +856,9 @@ export class RouteConnectionHandler {
|
|||||||
|
|
||||||
// Determine host using function or static value
|
// Determine host using function or static value
|
||||||
let targetHost: string | string[];
|
let targetHost: string | string[];
|
||||||
if (typeof action.target.host === 'function') {
|
if (typeof selectedTarget.host === 'function') {
|
||||||
try {
|
try {
|
||||||
targetHost = action.target.host(routeContext);
|
targetHost = selectedTarget.host(routeContext);
|
||||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||||
logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
|
logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
|
||||||
connectionId,
|
connectionId,
|
||||||
@@ -780,7 +877,7 @@ export class RouteConnectionHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetHost = action.target.host;
|
targetHost = selectedTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an array of hosts, select one randomly for load balancing
|
// If an array of hosts, select one randomly for load balancing
|
||||||
@@ -790,9 +887,9 @@ export class RouteConnectionHandler {
|
|||||||
|
|
||||||
// Determine port using function or static value
|
// Determine port using function or static value
|
||||||
let targetPort: number;
|
let targetPort: number;
|
||||||
if (typeof action.target.port === 'function') {
|
if (typeof selectedTarget.port === 'function') {
|
||||||
try {
|
try {
|
||||||
targetPort = action.target.port(routeContext);
|
targetPort = selectedTarget.port(routeContext);
|
||||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||||
logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
|
logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
|
||||||
connectionId,
|
connectionId,
|
||||||
@@ -813,20 +910,27 @@ export class RouteConnectionHandler {
|
|||||||
this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
|
this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (action.target.port === 'preserve') {
|
} else if (selectedTarget.port === 'preserve') {
|
||||||
// Use incoming port if port is 'preserve'
|
// Use incoming port if port is 'preserve'
|
||||||
targetPort = record.localPort;
|
targetPort = record.localPort;
|
||||||
} else {
|
} else {
|
||||||
// Use static port from configuration
|
// Use static port from configuration
|
||||||
targetPort = action.target.port;
|
targetPort = selectedTarget.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the resolved host in the context
|
// Store the resolved host in the context
|
||||||
routeContext.targetHost = selectedHost;
|
routeContext.targetHost = selectedHost;
|
||||||
|
|
||||||
|
// Get effective settings (target overrides route-level settings)
|
||||||
|
const effectiveTls = selectedTarget.tls || action.tls;
|
||||||
|
const effectiveWebsocket = selectedTarget.websocket || action.websocket;
|
||||||
|
const effectiveSendProxyProtocol = selectedTarget.sendProxyProtocol !== undefined
|
||||||
|
? selectedTarget.sendProxyProtocol
|
||||||
|
: action.sendProxyProtocol;
|
||||||
|
|
||||||
// Determine if this needs TLS handling
|
// Determine if this needs TLS handling
|
||||||
if (action.tls) {
|
if (effectiveTls) {
|
||||||
switch (action.tls.mode) {
|
switch (effectiveTls.mode) {
|
||||||
case 'passthrough':
|
case 'passthrough':
|
||||||
// For TLS passthrough, just forward directly
|
// For TLS passthrough, just forward directly
|
||||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||||
@@ -853,9 +957,9 @@ export class RouteConnectionHandler {
|
|||||||
// For TLS termination, use HttpProxy
|
// For TLS termination, use HttpProxy
|
||||||
if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
|
if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
|
||||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||||
logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, {
|
logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host} for connection ${connectionId}`, {
|
||||||
connectionId,
|
connectionId,
|
||||||
targetHost: action.target.host,
|
targetHost: selectedTarget.host,
|
||||||
component: 'route-handler'
|
component: 'route-handler'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -929,10 +1033,10 @@ export class RouteConnectionHandler {
|
|||||||
} else {
|
} else {
|
||||||
// Basic forwarding
|
// Basic forwarding
|
||||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||||
logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, {
|
logger.log('info', `Using basic forwarding to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host}:${selectedTarget.port} for connection ${connectionId}`, {
|
||||||
connectionId,
|
connectionId,
|
||||||
targetHost: action.target.host,
|
targetHost: selectedTarget.host,
|
||||||
targetPort: action.target.port,
|
targetPort: selectedTarget.port,
|
||||||
component: 'route-handler'
|
component: 'route-handler'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -940,27 +1044,27 @@ export class RouteConnectionHandler {
|
|||||||
// Get the appropriate host value
|
// Get the appropriate host value
|
||||||
let targetHost: string;
|
let targetHost: string;
|
||||||
|
|
||||||
if (typeof action.target.host === 'function') {
|
if (typeof selectedTarget.host === 'function') {
|
||||||
// For function-based host, use the same routeContext created earlier
|
// For function-based host, use the same routeContext created earlier
|
||||||
const hostResult = action.target.host(routeContext);
|
const hostResult = selectedTarget.host(routeContext);
|
||||||
targetHost = Array.isArray(hostResult)
|
targetHost = Array.isArray(hostResult)
|
||||||
? hostResult[Math.floor(Math.random() * hostResult.length)]
|
? hostResult[Math.floor(Math.random() * hostResult.length)]
|
||||||
: hostResult;
|
: hostResult;
|
||||||
} else {
|
} else {
|
||||||
// For static host value
|
// For static host value
|
||||||
targetHost = Array.isArray(action.target.host)
|
targetHost = Array.isArray(selectedTarget.host)
|
||||||
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
? selectedTarget.host[Math.floor(Math.random() * selectedTarget.host.length)]
|
||||||
: action.target.host;
|
: selectedTarget.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine port - either function-based, static, or preserve incoming port
|
// Determine port - either function-based, static, or preserve incoming port
|
||||||
let targetPort: number;
|
let targetPort: number;
|
||||||
if (typeof action.target.port === 'function') {
|
if (typeof selectedTarget.port === 'function') {
|
||||||
targetPort = action.target.port(routeContext);
|
targetPort = selectedTarget.port(routeContext);
|
||||||
} else if (action.target.port === 'preserve') {
|
} else if (selectedTarget.port === 'preserve') {
|
||||||
targetPort = record.localPort;
|
targetPort = record.localPort;
|
||||||
} else {
|
} else {
|
||||||
targetPort = action.target.port;
|
targetPort = selectedTarget.port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the connection record and context with resolved values
|
// Update the connection record and context with resolved values
|
||||||
|
@@ -14,23 +14,12 @@ export * from './route-validators.js';
|
|||||||
// Export route utilities for route operations
|
// Export route utilities for route operations
|
||||||
export * from './route-utils.js';
|
export * from './route-utils.js';
|
||||||
|
|
||||||
// Export route patterns with renamed exports to avoid conflicts
|
// Export additional functions from route-helpers that weren't already exported
|
||||||
import {
|
|
||||||
createWebSocketRoute as createWebSocketPatternRoute,
|
|
||||||
createLoadBalancerRoute as createLoadBalancerPatternRoute,
|
|
||||||
createApiGatewayRoute,
|
|
||||||
addRateLimiting,
|
|
||||||
addBasicAuth,
|
|
||||||
addJwtAuth
|
|
||||||
} from './route-patterns.js';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createWebSocketPatternRoute,
|
|
||||||
createLoadBalancerPatternRoute,
|
|
||||||
createApiGatewayRoute,
|
createApiGatewayRoute,
|
||||||
addRateLimiting,
|
addRateLimiting,
|
||||||
addBasicAuth,
|
addBasicAuth,
|
||||||
addJwtAuth
|
addJwtAuth
|
||||||
};
|
} from './route-helpers.js';
|
||||||
|
|
||||||
// Migration utilities have been removed as they are no longer needed
|
// Migration utilities have been removed as they are no longer needed
|
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
|
||||||
|
import { mergeRouteConfigs } from './route-utils.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an HTTP-only route configuration
|
* Create an HTTP-only route configuration
|
||||||
@@ -42,7 +43,7 @@ export function createHttpRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target
|
targets: [target]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the route config
|
// Create the route config
|
||||||
@@ -82,7 +83,7 @@ export function createHttpsTerminateRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target,
|
targets: [target],
|
||||||
tls: {
|
tls: {
|
||||||
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
||||||
certificate: options.certificate || 'auto'
|
certificate: options.certificate || 'auto'
|
||||||
@@ -152,7 +153,7 @@ export function createHttpsPassthroughRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target,
|
targets: [target],
|
||||||
tls: {
|
tls: {
|
||||||
mode: 'passthrough'
|
mode: 'passthrough'
|
||||||
}
|
}
|
||||||
@@ -211,26 +212,62 @@ export function createCompleteHttpsServer(
|
|||||||
/**
|
/**
|
||||||
* Create a load balancer route (round-robin between multiple backend hosts)
|
* Create a load balancer route (round-robin between multiple backend hosts)
|
||||||
* @param domains Domain(s) to match
|
* @param domains Domain(s) to match
|
||||||
* @param hosts Array of backend hosts to load balance between
|
* @param backendsOrHosts Array of backend servers OR array of host strings (legacy)
|
||||||
* @param port Backend port
|
* @param portOrOptions Port number (legacy) OR options object
|
||||||
* @param options Additional route options
|
* @param options Additional route options (legacy)
|
||||||
* @returns Route configuration object
|
* @returns Route configuration object
|
||||||
*/
|
*/
|
||||||
export function createLoadBalancerRoute(
|
export function createLoadBalancerRoute(
|
||||||
domains: string | string[],
|
domains: string | string[],
|
||||||
hosts: string[],
|
backendsOrHosts: Array<{ host: string; port: number }> | string[],
|
||||||
port: number,
|
portOrOptions?: number | {
|
||||||
options: {
|
tls?: {
|
||||||
|
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
};
|
||||||
|
useTls?: boolean;
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
algorithm?: 'round-robin' | 'least-connections' | 'ip-hash';
|
||||||
|
healthCheck?: {
|
||||||
|
path: string;
|
||||||
|
interval: number;
|
||||||
|
timeout: number;
|
||||||
|
unhealthyThreshold: number;
|
||||||
|
healthyThreshold: number;
|
||||||
|
};
|
||||||
|
[key: string]: any;
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
tls?: {
|
tls?: {
|
||||||
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
};
|
};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = {}
|
}
|
||||||
): IRouteConfig {
|
): IRouteConfig {
|
||||||
|
// Handle legacy signature: (domains, hosts[], port, options)
|
||||||
|
let backends: Array<{ host: string; port: number }>;
|
||||||
|
let finalOptions: any;
|
||||||
|
|
||||||
|
if (Array.isArray(backendsOrHosts) && backendsOrHosts.length > 0 && typeof backendsOrHosts[0] === 'string') {
|
||||||
|
// Legacy signature
|
||||||
|
const hosts = backendsOrHosts as string[];
|
||||||
|
const port = portOrOptions as number;
|
||||||
|
backends = hosts.map(host => ({ host, port }));
|
||||||
|
finalOptions = options || {};
|
||||||
|
} else {
|
||||||
|
// New signature
|
||||||
|
backends = backendsOrHosts as Array<{ host: string; port: number }>;
|
||||||
|
finalOptions = (portOrOptions as any) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract hosts and ensure all backends use the same port
|
||||||
|
const port = backends[0].port;
|
||||||
|
const hosts = backends.map(backend => backend.host);
|
||||||
|
|
||||||
// Create route match
|
// Create route match
|
||||||
const match: IRouteMatch = {
|
const match: IRouteMatch = {
|
||||||
ports: options.match?.ports || (options.tls ? 443 : 80),
|
ports: finalOptions.match?.ports || (finalOptions.tls || finalOptions.useTls ? 443 : 80),
|
||||||
domains
|
domains
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,14 +280,22 @@ export function createLoadBalancerRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target
|
targets: [target]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add TLS configuration if provided
|
// Add TLS configuration if provided
|
||||||
if (options.tls) {
|
if (finalOptions.tls || finalOptions.useTls) {
|
||||||
action.tls = {
|
action.tls = {
|
||||||
mode: options.tls.mode,
|
mode: finalOptions.tls?.mode || 'terminate',
|
||||||
certificate: options.tls.certificate || 'auto'
|
certificate: finalOptions.tls?.certificate || finalOptions.certificate || 'auto'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add load balancing options
|
||||||
|
if (finalOptions.algorithm || finalOptions.healthCheck) {
|
||||||
|
action.loadBalancing = {
|
||||||
|
algorithm: finalOptions.algorithm || 'round-robin',
|
||||||
|
healthCheck: finalOptions.healthCheck
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +303,8 @@ export function createLoadBalancerRoute(
|
|||||||
return {
|
return {
|
||||||
match,
|
match,
|
||||||
action,
|
action,
|
||||||
name: options.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
name: finalOptions.name || `Load Balancer for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||||
...options
|
...finalOptions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,7 +348,7 @@ export function createApiRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target
|
targets: [target]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add TLS configuration if using HTTPS
|
// Add TLS configuration if using HTTPS
|
||||||
@@ -339,16 +384,26 @@ export function createApiRoute(
|
|||||||
/**
|
/**
|
||||||
* Create a WebSocket route configuration
|
* Create a WebSocket route configuration
|
||||||
* @param domains Domain(s) to match
|
* @param domains Domain(s) to match
|
||||||
* @param wsPath WebSocket path (e.g., "/ws")
|
* @param targetOrPath Target server OR WebSocket path (legacy)
|
||||||
* @param target Target WebSocket server host and port
|
* @param targetOrOptions Target server (legacy) OR options
|
||||||
* @param options Additional route options
|
* @param options Additional route options (legacy)
|
||||||
* @returns Route configuration object
|
* @returns Route configuration object
|
||||||
*/
|
*/
|
||||||
export function createWebSocketRoute(
|
export function createWebSocketRoute(
|
||||||
domains: string | string[],
|
domains: string | string[],
|
||||||
wsPath: string,
|
targetOrPath: { host: string | string[]; port: number } | string,
|
||||||
target: { host: string | string[]; port: number },
|
targetOrOptions?: { host: string | string[]; port: number } | {
|
||||||
options: {
|
useTls?: boolean;
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
path?: string;
|
||||||
|
httpPort?: number | number[];
|
||||||
|
httpsPort?: number | number[];
|
||||||
|
pingInterval?: number;
|
||||||
|
pingTimeout?: number;
|
||||||
|
name?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
useTls?: boolean;
|
useTls?: boolean;
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
httpPort?: number | number[];
|
httpPort?: number | number[];
|
||||||
@@ -357,16 +412,33 @@ export function createWebSocketRoute(
|
|||||||
pingTimeout?: number;
|
pingTimeout?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = {}
|
}
|
||||||
): IRouteConfig {
|
): IRouteConfig {
|
||||||
|
// Handle different signatures
|
||||||
|
let target: { host: string | string[]; port: number };
|
||||||
|
let wsPath: string;
|
||||||
|
let finalOptions: any;
|
||||||
|
|
||||||
|
if (typeof targetOrPath === 'string') {
|
||||||
|
// Legacy signature: (domains, path, target, options)
|
||||||
|
wsPath = targetOrPath;
|
||||||
|
target = targetOrOptions as { host: string | string[]; port: number };
|
||||||
|
finalOptions = options || {};
|
||||||
|
} else {
|
||||||
|
// New signature: (domains, target, options)
|
||||||
|
target = targetOrPath;
|
||||||
|
finalOptions = (targetOrOptions as any) || {};
|
||||||
|
wsPath = finalOptions.path || '/ws';
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize WebSocket path
|
// Normalize WebSocket path
|
||||||
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
|
||||||
|
|
||||||
// Create route match
|
// Create route match
|
||||||
const match: IRouteMatch = {
|
const match: IRouteMatch = {
|
||||||
ports: options.useTls
|
ports: finalOptions.useTls
|
||||||
? (options.httpsPort || 443)
|
? (finalOptions.httpsPort || 443)
|
||||||
: (options.httpPort || 80),
|
: (finalOptions.httpPort || 80),
|
||||||
domains,
|
domains,
|
||||||
path: normalizedPath
|
path: normalizedPath
|
||||||
};
|
};
|
||||||
@@ -374,19 +446,19 @@ export function createWebSocketRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target,
|
targets: [target],
|
||||||
websocket: {
|
websocket: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
pingInterval: options.pingInterval || 30000, // 30 seconds
|
pingInterval: finalOptions.pingInterval || 30000, // 30 seconds
|
||||||
pingTimeout: options.pingTimeout || 5000 // 5 seconds
|
pingTimeout: finalOptions.pingTimeout || 5000 // 5 seconds
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add TLS configuration if using HTTPS
|
// Add TLS configuration if using HTTPS
|
||||||
if (options.useTls) {
|
if (finalOptions.useTls) {
|
||||||
action.tls = {
|
action.tls = {
|
||||||
mode: 'terminate',
|
mode: 'terminate',
|
||||||
certificate: options.certificate || 'auto'
|
certificate: finalOptions.certificate || 'auto'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,9 +466,9 @@ export function createWebSocketRoute(
|
|||||||
return {
|
return {
|
||||||
match,
|
match,
|
||||||
action,
|
action,
|
||||||
name: options.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
name: finalOptions.name || `WebSocket Route ${normalizedPath} for ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
||||||
priority: options.priority || 100, // Higher priority for WebSocket routes
|
priority: finalOptions.priority || 100, // Higher priority for WebSocket routes
|
||||||
...options
|
...finalOptions
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,10 +504,10 @@ export function createPortMappingRoute(options: {
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: options.targetHost,
|
host: options.targetHost,
|
||||||
port: options.portMapper
|
port: options.portMapper
|
||||||
}
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the route config
|
// Create the route config
|
||||||
@@ -500,10 +572,10 @@ export function createDynamicRoute(options: {
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: options.targetHost,
|
host: options.targetHost,
|
||||||
port: options.portMapper
|
port: options.portMapper
|
||||||
}
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the route config
|
// Create the route config
|
||||||
@@ -548,10 +620,10 @@ export function createSmartLoadBalancer(options: {
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: hostSelector,
|
host: hostSelector,
|
||||||
port: options.portMapper
|
port: options.portMapper
|
||||||
}
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the route config
|
// Create the route config
|
||||||
@@ -609,10 +681,10 @@ export function createNfTablesRoute(
|
|||||||
// Create route action
|
// Create route action
|
||||||
const action: IRouteAction = {
|
const action: IRouteAction = {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
target: {
|
targets: [{
|
||||||
host: target.host,
|
host: target.host,
|
||||||
port: target.port
|
port: target.port
|
||||||
},
|
}],
|
||||||
forwardingEngine: 'nftables',
|
forwardingEngine: 'nftables',
|
||||||
nftables: {
|
nftables: {
|
||||||
protocol: options.protocol || 'tcp',
|
protocol: options.protocol || 'tcp',
|
||||||
@@ -1030,3 +1102,152 @@ export const SocketHandlers = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an API Gateway route pattern
|
||||||
|
* @param domains Domain(s) to match
|
||||||
|
* @param apiBasePath Base path for API endpoints (e.g., '/api')
|
||||||
|
* @param target Target host and port
|
||||||
|
* @param options Additional route options
|
||||||
|
* @returns API route configuration
|
||||||
|
*/
|
||||||
|
export function createApiGatewayRoute(
|
||||||
|
domains: string | string[],
|
||||||
|
apiBasePath: string,
|
||||||
|
target: { host: string | string[]; port: number },
|
||||||
|
options: {
|
||||||
|
useTls?: boolean;
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
addCorsHeaders?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
} = {}
|
||||||
|
): IRouteConfig {
|
||||||
|
// Normalize apiBasePath to ensure it starts with / and doesn't end with /
|
||||||
|
const normalizedPath = apiBasePath.startsWith('/')
|
||||||
|
? apiBasePath
|
||||||
|
: `/${apiBasePath}`;
|
||||||
|
|
||||||
|
// Add wildcard to path to match all API endpoints
|
||||||
|
const apiPath = normalizedPath.endsWith('/')
|
||||||
|
? `${normalizedPath}*`
|
||||||
|
: `${normalizedPath}/*`;
|
||||||
|
|
||||||
|
// Create base route
|
||||||
|
const baseRoute = options.useTls
|
||||||
|
? createHttpsTerminateRoute(domains, target, {
|
||||||
|
certificate: options.certificate || 'auto'
|
||||||
|
})
|
||||||
|
: createHttpRoute(domains, target);
|
||||||
|
|
||||||
|
// Add API-specific configurations
|
||||||
|
const apiRoute: Partial<IRouteConfig> = {
|
||||||
|
match: {
|
||||||
|
...baseRoute.match,
|
||||||
|
path: apiPath
|
||||||
|
},
|
||||||
|
name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
||||||
|
priority: options.priority || 100 // Higher priority for specific path matching
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add CORS headers if requested
|
||||||
|
if (options.addCorsHeaders) {
|
||||||
|
apiRoute.headers = {
|
||||||
|
response: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeRouteConfigs(baseRoute, apiRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a rate limiting route pattern
|
||||||
|
* @param baseRoute Base route to add rate limiting to
|
||||||
|
* @param rateLimit Rate limiting configuration
|
||||||
|
* @returns Route with rate limiting
|
||||||
|
*/
|
||||||
|
export function addRateLimiting(
|
||||||
|
baseRoute: IRouteConfig,
|
||||||
|
rateLimit: {
|
||||||
|
maxRequests: number;
|
||||||
|
window: number; // Time window in seconds
|
||||||
|
keyBy?: 'ip' | 'path' | 'header';
|
||||||
|
headerName?: string; // Required if keyBy is 'header'
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return mergeRouteConfigs(baseRoute, {
|
||||||
|
security: {
|
||||||
|
rateLimit: {
|
||||||
|
enabled: true,
|
||||||
|
maxRequests: rateLimit.maxRequests,
|
||||||
|
window: rateLimit.window,
|
||||||
|
keyBy: rateLimit.keyBy || 'ip',
|
||||||
|
headerName: rateLimit.headerName,
|
||||||
|
errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a basic authentication route pattern
|
||||||
|
* @param baseRoute Base route to add authentication to
|
||||||
|
* @param auth Authentication configuration
|
||||||
|
* @returns Route with basic authentication
|
||||||
|
*/
|
||||||
|
export function addBasicAuth(
|
||||||
|
baseRoute: IRouteConfig,
|
||||||
|
auth: {
|
||||||
|
users: Array<{ username: string; password: string }>;
|
||||||
|
realm?: string;
|
||||||
|
excludePaths?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return mergeRouteConfigs(baseRoute, {
|
||||||
|
security: {
|
||||||
|
basicAuth: {
|
||||||
|
enabled: true,
|
||||||
|
users: auth.users,
|
||||||
|
realm: auth.realm || 'Restricted Area',
|
||||||
|
excludePaths: auth.excludePaths || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JWT authentication route pattern
|
||||||
|
* @param baseRoute Base route to add JWT authentication to
|
||||||
|
* @param jwt JWT authentication configuration
|
||||||
|
* @returns Route with JWT authentication
|
||||||
|
*/
|
||||||
|
export function addJwtAuth(
|
||||||
|
baseRoute: IRouteConfig,
|
||||||
|
jwt: {
|
||||||
|
secret: string;
|
||||||
|
algorithm?: string;
|
||||||
|
issuer?: string;
|
||||||
|
audience?: string;
|
||||||
|
expiresIn?: number; // Time in seconds
|
||||||
|
excludePaths?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return mergeRouteConfigs(baseRoute, {
|
||||||
|
security: {
|
||||||
|
jwtAuth: {
|
||||||
|
enabled: true,
|
||||||
|
secret: jwt.secret,
|
||||||
|
algorithm: jwt.algorithm || 'HS256',
|
||||||
|
issuer: jwt.issuer,
|
||||||
|
audience: jwt.audience,
|
||||||
|
expiresIn: jwt.expiresIn,
|
||||||
|
excludePaths: jwt.excludePaths || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -1,403 +0,0 @@
|
|||||||
/**
|
|
||||||
* Route Patterns
|
|
||||||
*
|
|
||||||
* This file provides pre-defined route patterns for common use cases.
|
|
||||||
* These patterns can be used as templates for creating route configurations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
|
|
||||||
import { mergeRouteConfigs } from './route-utils.js';
|
|
||||||
import { SocketHandlers } from './route-helpers.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a basic HTTP route configuration
|
|
||||||
*/
|
|
||||||
export function createHttpRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
|
||||||
options: Partial<IRouteConfig> = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
const route: IRouteConfig = {
|
|
||||||
match: {
|
|
||||||
domains,
|
|
||||||
ports: 80
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: target.host,
|
|
||||||
port: target.port
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: options.name || `HTTP: ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(route, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTPS route with TLS termination
|
|
||||||
*/
|
|
||||||
export function createHttpsTerminateRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
|
||||||
options: Partial<IRouteConfig> & {
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
reencrypt?: boolean;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
const route: IRouteConfig = {
|
|
||||||
match: {
|
|
||||||
domains,
|
|
||||||
ports: 443
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: target.host,
|
|
||||||
port: target.port
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: options.name || `HTTPS (terminate): ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(route, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTPS route with TLS passthrough
|
|
||||||
*/
|
|
||||||
export function createHttpsPassthroughRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
|
||||||
options: Partial<IRouteConfig> = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
const route: IRouteConfig = {
|
|
||||||
match: {
|
|
||||||
domains,
|
|
||||||
ports: 443
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: target.host,
|
|
||||||
port: target.port
|
|
||||||
},
|
|
||||||
tls: {
|
|
||||||
mode: 'passthrough'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: options.name || `HTTPS (passthrough): ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(route, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTP to HTTPS redirect route
|
|
||||||
*/
|
|
||||||
export function createHttpToHttpsRedirect(
|
|
||||||
domains: string | string[],
|
|
||||||
options: Partial<IRouteConfig> & {
|
|
||||||
redirectCode?: 301 | 302 | 307 | 308;
|
|
||||||
preservePath?: boolean;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
const route: IRouteConfig = {
|
|
||||||
match: {
|
|
||||||
domains,
|
|
||||||
ports: 80
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: 'socket-handler',
|
|
||||||
socketHandler: SocketHandlers.httpRedirect(
|
|
||||||
options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
|
|
||||||
options.redirectCode || 301
|
|
||||||
)
|
|
||||||
},
|
|
||||||
name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}`
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(route, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a complete HTTPS server with redirect from HTTP
|
|
||||||
*/
|
|
||||||
export function createCompleteHttpsServer(
|
|
||||||
domains: string | string[],
|
|
||||||
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
|
|
||||||
options: Partial<IRouteConfig> & {
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
tlsMode?: 'terminate' | 'passthrough' | 'terminate-and-reencrypt';
|
|
||||||
redirectCode?: 301 | 302 | 307 | 308;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig[] {
|
|
||||||
// Create the TLS route based on the selected mode
|
|
||||||
const tlsRoute = options.tlsMode === 'passthrough'
|
|
||||||
? createHttpsPassthroughRoute(domains, target, options)
|
|
||||||
: createHttpsTerminateRoute(domains, target, {
|
|
||||||
...options,
|
|
||||||
reencrypt: options.tlsMode === 'terminate-and-reencrypt'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the HTTP to HTTPS redirect route
|
|
||||||
const redirectRoute = createHttpToHttpsRedirect(domains, {
|
|
||||||
redirectCode: options.redirectCode,
|
|
||||||
preservePath: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return [tlsRoute, redirectRoute];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an API Gateway route pattern
|
|
||||||
* @param domains Domain(s) to match
|
|
||||||
* @param apiBasePath Base path for API endpoints (e.g., '/api')
|
|
||||||
* @param target Target host and port
|
|
||||||
* @param options Additional route options
|
|
||||||
* @returns API route configuration
|
|
||||||
*/
|
|
||||||
export function createApiGatewayRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
apiBasePath: string,
|
|
||||||
target: { host: string | string[]; port: number },
|
|
||||||
options: {
|
|
||||||
useTls?: boolean;
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
addCorsHeaders?: boolean;
|
|
||||||
[key: string]: any;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
// Normalize apiBasePath to ensure it starts with / and doesn't end with /
|
|
||||||
const normalizedPath = apiBasePath.startsWith('/')
|
|
||||||
? apiBasePath
|
|
||||||
: `/${apiBasePath}`;
|
|
||||||
|
|
||||||
// Add wildcard to path to match all API endpoints
|
|
||||||
const apiPath = normalizedPath.endsWith('/')
|
|
||||||
? `${normalizedPath}*`
|
|
||||||
: `${normalizedPath}/*`;
|
|
||||||
|
|
||||||
// Create base route
|
|
||||||
const baseRoute = options.useTls
|
|
||||||
? createHttpsTerminateRoute(domains, target, {
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
})
|
|
||||||
: createHttpRoute(domains, target);
|
|
||||||
|
|
||||||
// Add API-specific configurations
|
|
||||||
const apiRoute: Partial<IRouteConfig> = {
|
|
||||||
match: {
|
|
||||||
...baseRoute.match,
|
|
||||||
path: apiPath
|
|
||||||
},
|
|
||||||
name: options.name || `API Gateway: ${apiPath} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
|
||||||
priority: options.priority || 100 // Higher priority for specific path matching
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add CORS headers if requested
|
|
||||||
if (options.addCorsHeaders) {
|
|
||||||
apiRoute.headers = {
|
|
||||||
response: {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
||||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
||||||
'Access-Control-Max-Age': '86400'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeRouteConfigs(baseRoute, apiRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a WebSocket route pattern
|
|
||||||
* @param domains Domain(s) to match
|
|
||||||
* @param target WebSocket server host and port
|
|
||||||
* @param options Additional route options
|
|
||||||
* @returns WebSocket route configuration
|
|
||||||
*/
|
|
||||||
export function createWebSocketRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
target: { host: string | string[]; port: number },
|
|
||||||
options: {
|
|
||||||
useTls?: boolean;
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
path?: string;
|
|
||||||
[key: string]: any;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
// Create base route
|
|
||||||
const baseRoute = options.useTls
|
|
||||||
? createHttpsTerminateRoute(domains, target, {
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
})
|
|
||||||
: createHttpRoute(domains, target);
|
|
||||||
|
|
||||||
// Add WebSocket-specific configurations
|
|
||||||
const wsRoute: Partial<IRouteConfig> = {
|
|
||||||
match: {
|
|
||||||
...baseRoute.match,
|
|
||||||
path: options.path || '/ws',
|
|
||||||
headers: {
|
|
||||||
'Upgrade': 'websocket'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
...baseRoute.action,
|
|
||||||
websocket: {
|
|
||||||
enabled: true,
|
|
||||||
pingInterval: options.pingInterval || 30000, // 30 seconds
|
|
||||||
pingTimeout: options.pingTimeout || 5000 // 5 seconds
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: options.name || `WebSocket: ${Array.isArray(domains) ? domains.join(', ') : domains} -> ${Array.isArray(target.host) ? target.host.join(', ') : target.host}:${target.port}`,
|
|
||||||
priority: options.priority || 100 // Higher priority for WebSocket routes
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(baseRoute, wsRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a load balancer route pattern
|
|
||||||
* @param domains Domain(s) to match
|
|
||||||
* @param backends Array of backend servers
|
|
||||||
* @param options Additional route options
|
|
||||||
* @returns Load balancer route configuration
|
|
||||||
*/
|
|
||||||
export function createLoadBalancerRoute(
|
|
||||||
domains: string | string[],
|
|
||||||
backends: Array<{ host: string; port: number }>,
|
|
||||||
options: {
|
|
||||||
useTls?: boolean;
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
algorithm?: 'round-robin' | 'least-connections' | 'ip-hash';
|
|
||||||
healthCheck?: {
|
|
||||||
path: string;
|
|
||||||
interval: number;
|
|
||||||
timeout: number;
|
|
||||||
unhealthyThreshold: number;
|
|
||||||
healthyThreshold: number;
|
|
||||||
};
|
|
||||||
[key: string]: any;
|
|
||||||
} = {}
|
|
||||||
): IRouteConfig {
|
|
||||||
// Extract hosts and ensure all backends use the same port
|
|
||||||
const port = backends[0].port;
|
|
||||||
const hosts = backends.map(backend => backend.host);
|
|
||||||
|
|
||||||
// Create route with multiple hosts for load balancing
|
|
||||||
const baseRoute = options.useTls
|
|
||||||
? createHttpsTerminateRoute(domains, { host: hosts, port }, {
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
})
|
|
||||||
: createHttpRoute(domains, { host: hosts, port });
|
|
||||||
|
|
||||||
// Add load balancing specific configurations
|
|
||||||
const lbRoute: Partial<IRouteConfig> = {
|
|
||||||
action: {
|
|
||||||
...baseRoute.action,
|
|
||||||
loadBalancing: {
|
|
||||||
algorithm: options.algorithm || 'round-robin',
|
|
||||||
healthCheck: options.healthCheck
|
|
||||||
}
|
|
||||||
},
|
|
||||||
name: options.name || `Load Balancer: ${Array.isArray(domains) ? domains.join(', ') : domains}`,
|
|
||||||
priority: options.priority || 50
|
|
||||||
};
|
|
||||||
|
|
||||||
return mergeRouteConfigs(baseRoute, lbRoute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a rate limiting route pattern
|
|
||||||
* @param baseRoute Base route to add rate limiting to
|
|
||||||
* @param rateLimit Rate limiting configuration
|
|
||||||
* @returns Route with rate limiting
|
|
||||||
*/
|
|
||||||
export function addRateLimiting(
|
|
||||||
baseRoute: IRouteConfig,
|
|
||||||
rateLimit: {
|
|
||||||
maxRequests: number;
|
|
||||||
window: number; // Time window in seconds
|
|
||||||
keyBy?: 'ip' | 'path' | 'header';
|
|
||||||
headerName?: string; // Required if keyBy is 'header'
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return mergeRouteConfigs(baseRoute, {
|
|
||||||
security: {
|
|
||||||
rateLimit: {
|
|
||||||
enabled: true,
|
|
||||||
maxRequests: rateLimit.maxRequests,
|
|
||||||
window: rateLimit.window,
|
|
||||||
keyBy: rateLimit.keyBy || 'ip',
|
|
||||||
headerName: rateLimit.headerName,
|
|
||||||
errorMessage: rateLimit.errorMessage || 'Rate limit exceeded. Please try again later.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a basic authentication route pattern
|
|
||||||
* @param baseRoute Base route to add authentication to
|
|
||||||
* @param auth Authentication configuration
|
|
||||||
* @returns Route with basic authentication
|
|
||||||
*/
|
|
||||||
export function addBasicAuth(
|
|
||||||
baseRoute: IRouteConfig,
|
|
||||||
auth: {
|
|
||||||
users: Array<{ username: string; password: string }>;
|
|
||||||
realm?: string;
|
|
||||||
excludePaths?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return mergeRouteConfigs(baseRoute, {
|
|
||||||
security: {
|
|
||||||
basicAuth: {
|
|
||||||
enabled: true,
|
|
||||||
users: auth.users,
|
|
||||||
realm: auth.realm || 'Restricted Area',
|
|
||||||
excludePaths: auth.excludePaths || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a JWT authentication route pattern
|
|
||||||
* @param baseRoute Base route to add JWT authentication to
|
|
||||||
* @param jwt JWT authentication configuration
|
|
||||||
* @returns Route with JWT authentication
|
|
||||||
*/
|
|
||||||
export function addJwtAuth(
|
|
||||||
baseRoute: IRouteConfig,
|
|
||||||
jwt: {
|
|
||||||
secret: string;
|
|
||||||
algorithm?: string;
|
|
||||||
issuer?: string;
|
|
||||||
audience?: string;
|
|
||||||
expiresIn?: number; // Time in seconds
|
|
||||||
excludePaths?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return mergeRouteConfigs(baseRoute, {
|
|
||||||
security: {
|
|
||||||
jwtAuth: {
|
|
||||||
enabled: true,
|
|
||||||
secret: jwt.secret,
|
|
||||||
algorithm: jwt.algorithm || 'HS256',
|
|
||||||
issuer: jwt.issuer,
|
|
||||||
audience: jwt.audience,
|
|
||||||
expiresIn: jwt.expiresIn,
|
|
||||||
excludePaths: jwt.excludePaths || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@@ -66,12 +66,9 @@ export function mergeRouteConfigs(
|
|||||||
// Otherwise merge the action properties
|
// Otherwise merge the action properties
|
||||||
mergedRoute.action = { ...mergedRoute.action };
|
mergedRoute.action = { ...mergedRoute.action };
|
||||||
|
|
||||||
// Merge target
|
// Merge targets
|
||||||
if (overrideRoute.action.target) {
|
if (overrideRoute.action.targets) {
|
||||||
mergedRoute.action.target = {
|
mergedRoute.action.targets = overrideRoute.action.targets;
|
||||||
...mergedRoute.action.target,
|
|
||||||
...overrideRoute.action.target
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge TLS options
|
// Merge TLS options
|
||||||
|
@@ -102,29 +102,43 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
|
|||||||
errors.push(`Invalid action type: ${action.type}`);
|
errors.push(`Invalid action type: ${action.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate target for 'forward' action
|
// Validate targets for 'forward' action
|
||||||
if (action.type === 'forward') {
|
if (action.type === 'forward') {
|
||||||
if (!action.target) {
|
if (!action.targets || !Array.isArray(action.targets) || action.targets.length === 0) {
|
||||||
errors.push('Target is required for forward action');
|
errors.push('Targets array is required for forward action');
|
||||||
} else {
|
} else {
|
||||||
|
// Validate each target
|
||||||
|
action.targets.forEach((target, index) => {
|
||||||
// Validate target host
|
// Validate target host
|
||||||
if (!action.target.host) {
|
if (!target.host) {
|
||||||
errors.push('Target host is required');
|
errors.push(`Target[${index}] host is required`);
|
||||||
} else if (typeof action.target.host !== 'string' &&
|
} else if (typeof target.host !== 'string' &&
|
||||||
!Array.isArray(action.target.host) &&
|
!Array.isArray(target.host) &&
|
||||||
typeof action.target.host !== 'function') {
|
typeof target.host !== 'function') {
|
||||||
errors.push('Target host must be a string, array of strings, or function');
|
errors.push(`Target[${index}] host must be a string, array of strings, or function`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate target port
|
// Validate target port
|
||||||
if (action.target.port === undefined) {
|
if (target.port === undefined) {
|
||||||
errors.push('Target port is required');
|
errors.push(`Target[${index}] port is required`);
|
||||||
} else if (typeof action.target.port !== 'number' &&
|
} else if (typeof target.port !== 'number' &&
|
||||||
typeof action.target.port !== 'function') {
|
typeof target.port !== 'function' &&
|
||||||
errors.push('Target port must be a number or a function');
|
target.port !== 'preserve') {
|
||||||
} else if (typeof action.target.port === 'number' && !isValidPort(action.target.port)) {
|
errors.push(`Target[${index}] port must be a number, 'preserve', or a function`);
|
||||||
errors.push('Target port must be between 1 and 65535');
|
} else if (typeof target.port === 'number' && !isValidPort(target.port)) {
|
||||||
|
errors.push(`Target[${index}] port must be between 1 and 65535`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate match criteria if present
|
||||||
|
if (target.match) {
|
||||||
|
if (target.match.ports && !Array.isArray(target.match.ports)) {
|
||||||
|
errors.push(`Target[${index}] match.ports must be an array`);
|
||||||
|
}
|
||||||
|
if (target.match.method && !Array.isArray(target.match.method)) {
|
||||||
|
errors.push(`Target[${index}] match.method must be an array`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate TLS options for forward actions
|
// Validate TLS options for forward actions
|
||||||
@@ -242,7 +256,10 @@ export function hasRequiredPropertiesForAction(route: IRouteConfig, actionType:
|
|||||||
|
|
||||||
switch (actionType) {
|
switch (actionType) {
|
||||||
case 'forward':
|
case 'forward':
|
||||||
return !!route.action.target && !!route.action.target.host && !!route.action.target.port;
|
return !!route.action.targets &&
|
||||||
|
Array.isArray(route.action.targets) &&
|
||||||
|
route.action.targets.length > 0 &&
|
||||||
|
route.action.targets.every(t => t.host && t.port !== undefined);
|
||||||
case 'socket-handler':
|
case 'socket-handler':
|
||||||
return !!route.action.socketHandler && typeof route.action.socketHandler === 'function';
|
return !!route.action.socketHandler && typeof route.action.socketHandler === 'function';
|
||||||
default:
|
default:
|
||||||
|
Reference in New Issue
Block a user