fix(network-proxy, route-utils, route-manager): Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
This commit is contained in:
parent
0fe0692e43
commit
bb54ea8192
@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-14 - 16.0.3 - fix(network-proxy, route-utils, route-manager)
|
||||||
|
Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
|
||||||
|
|
||||||
|
- Updated matchIpPattern and matchIpCidr to normalize IPv6-mapped IPv4 addresses.
|
||||||
|
- Replaced legacy 'domain' field references with 'domains' in route configurations.
|
||||||
|
- Removed deprecated methods for converting legacy proxy configs and legacy route helpers.
|
||||||
|
- Adjusted test cases (event system, route utils, network proxy function targets) to use modern interfaces.
|
||||||
|
- Improved logging and error messages in route-manager and route-utils for better debugging.
|
||||||
|
|
||||||
## 2025-05-10 - 16.0.2 - fix(test/certificate-provisioning)
|
## 2025-05-10 - 16.0.2 - fix(test/certificate-provisioning)
|
||||||
Update certificate provisioning tests with updated port mapping and ACME options; use accountEmail instead of contactEmail, adjust auto-api route creation to use HTTPS terminate helper, and refine expectations for wildcard passthrough domains.
|
Update certificate provisioning tests with updated port mapping and ACME options; use accountEmail instead of contactEmail, adjust auto-api route creation to use HTTPS terminate helper, and refine expectations for wildcard passthrough domains.
|
||||||
|
|
||||||
|
192
readme.plan.md
192
readme.plan.md
@ -1,139 +1,103 @@
|
|||||||
# Enhanced NetworkProxy with Native Route-Based Configuration
|
# SmartProxy Configuration Troubleshooting
|
||||||
|
|
||||||
## Project Goal
|
## IPv6/IPv4 Mapping Issue
|
||||||
Transform NetworkProxy to natively use route-based configurations (`IRouteConfig`) as its primary configuration format, completely eliminating translation layers while maintaining backward compatibility through adapter methods for existing code.
|
|
||||||
|
|
||||||
## Current Status
|
### Problem Identified
|
||||||
|
The SmartProxy is failing to match connections for wildcard domains (like `*.lossless.digital`) when IP restrictions are in place. After extensive debugging, the root cause has been identified:
|
||||||
|
|
||||||
The current implementation uses:
|
When a connection comes in from an IPv4 address (e.g., `212.95.99.130`), the Node.js server receives it as an IPv6-mapped IPv4 address with the format `::ffff:212.95.99.130`. However, the route configuration is expecting the exact string `212.95.99.130`, causing a mismatch.
|
||||||
- SmartProxy has a rich `IRouteConfig` format with match/action pattern
|
|
||||||
- NetworkProxy uses a simpler `IReverseProxyConfig` focused on hostname and destination
|
|
||||||
- `NetworkProxyBridge` translates between these formats, losing information
|
|
||||||
- Dynamic function-based hosts and ports aren't supported in NetworkProxy
|
|
||||||
- Duplicate configuration logic exists across components
|
|
||||||
|
|
||||||
## Planned Enhancements
|
From the debug logs:
|
||||||
|
```
|
||||||
|
[DEBUG] Route rejected: clientIp mismatch. Request: ::ffff:212.95.99.130, Route patterns: ["212.95.99.130"]
|
||||||
|
```
|
||||||
|
|
||||||
### Phase 1: Convert NetworkProxy to Native Route Configuration
|
### Solution
|
||||||
- [x] 1.1 Refactor NetworkProxy to use `IRouteConfig` as its primary internal format
|
|
||||||
- [x] 1.3 Update all internal processing to work directly with route configs
|
|
||||||
- [x] 1.4 Add a type-safe context object matching SmartProxy's
|
|
||||||
- [x] 1.5 Ensure backward compatibility for all existing NetworkProxy methods
|
|
||||||
- [x] 1.6 Remove `IReverseProxyConfig` usage in NetworkProxy
|
|
||||||
|
|
||||||
### Phase 2: Native Route Configuration Processing
|
To fix this issue, update the route configurations to include both formats of the IP address. Here's how to modify the affected route:
|
||||||
- [x] 2.1 Make `updateRouteConfigs(routes: IRouteConfig[])` the primary configuration method
|
|
||||||
- [x] 2.3 Implement a full RouteManager in NetworkProxy (reusing code from SmartProxy if possible)
|
|
||||||
- [x] 2.4 Support all route matching criteria (domains, paths, headers, clientIp)
|
|
||||||
- [x] 2.5 Handle priority-based route matching and conflict resolution
|
|
||||||
- [x] 2.6 Update certificate management to work with routes directly
|
|
||||||
|
|
||||||
### Phase 3: Simplify NetworkProxyBridge
|
```typescript
|
||||||
- [x] 3.1 Update NetworkProxyBridge to directly pass route configs to NetworkProxy
|
// Wildcard domain route for *.lossless.digital
|
||||||
- [x] 3.2 Remove all translation/conversion logic in the bridge
|
{
|
||||||
- [x] 3.3 Simplify domain registration from routes to Port80Handler
|
match: {
|
||||||
- [x] 3.4 Make the bridge a lightweight pass-through component
|
ports: 443,
|
||||||
- [x] 3.5 Add comprehensive logging for route synchronization
|
domains: ['*.lossless.digital'],
|
||||||
- [x] 3.6 Streamline certificate handling between components
|
clientIp: ['212.95.99.130', '::ffff:212.95.99.130'], // Include both formats
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: '212.95.99.130',
|
||||||
|
port: 443
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough'
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['212.95.99.130', '::ffff:212.95.99.130'] // Include both formats
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'Wildcard lossless.digital route (IP restricted)'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Phase 4: Native Function-Based Target Support
|
### Alternative Long-Term Fix
|
||||||
- [x] 4.1 Implement IRouteContext creation in NetworkProxy's request handler
|
|
||||||
- [x] 4.2 Add direct support for function-based host evaluation
|
|
||||||
- [x] 4.3 Add direct support for function-based port evaluation
|
|
||||||
- [x] 4.4 Implement caching for function results to improve performance
|
|
||||||
- [x] 4.5 Add comprehensive error handling for function execution
|
|
||||||
- [x] 4.6 Share context object implementation with SmartProxy
|
|
||||||
|
|
||||||
### Phase 5: Enhanced HTTP Features Using Route Logic
|
A more robust solution would be to modify the SmartProxy codebase to automatically handle IPv6-mapped IPv4 addresses by normalizing them before comparison. This would involve:
|
||||||
- [x] 5.1 Implement full route-based header manipulation
|
|
||||||
- [x] 5.2 Add support for URL rewriting using route context
|
|
||||||
- [x] 5.3 Support template variable resolution for strings
|
|
||||||
- [x] 5.4 Implement route security features (IP filtering, rate limiting)
|
|
||||||
- [x] 5.5 Add context-aware CORS handling
|
|
||||||
- [x] 5.6 Enable route-based WebSocket upgrades
|
|
||||||
|
|
||||||
### Phase 6: Testing, Documentation and Code Sharing
|
1. Modifying the `matchIpPattern` function in `route-manager.ts` to normalize IPv6-mapped IPv4 addresses:
|
||||||
- [x] 6.1 Create comprehensive tests for native route configuration
|
|
||||||
- [x] 6.2 Add specific tests for function-based targets
|
|
||||||
- [x] 6.3 Document NetworkProxy's native route capabilities
|
|
||||||
- [x] 6.4 Create shared utilities between SmartProxy and NetworkProxy
|
|
||||||
- [x] 6.5 Provide migration guide for direct NetworkProxy users
|
|
||||||
- [ ] 6.6 Benchmark performance improvements
|
|
||||||
|
|
||||||
### Phase 7: Unify Component Architecture
|
```typescript
|
||||||
- [x] 7.1 Implement a shared RouteManager used by both SmartProxy and NetworkProxy
|
private matchIpPattern(pattern: string, ip: string): boolean {
|
||||||
- [x] 7.2 Extract common route matching logic to a shared utility module
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
- [x] 7.3 Consolidate duplicate security management code
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
- [x] 7.4 Remove all legacy NetworkProxyBridge conversion code
|
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||||
- [x] 7.5 Make the NetworkProxyBridge a pure proxy pass-through component
|
|
||||||
- [x] 7.6 Standardize event naming and handling across components
|
|
||||||
|
|
||||||
### Phase 8: Certificate Management Consolidation
|
// Handle exact match with normalized addresses
|
||||||
- [x] 8.1 Create a unified CertificateManager component
|
if (normalizedPattern === normalizedIp) {
|
||||||
- [x] 8.2 Centralize certificate storage and renewal logic
|
return true;
|
||||||
- [x] 8.3 Simplify ACME challenge handling across proxies
|
}
|
||||||
- [x] 8.4 Implement shared certificate events for all components
|
|
||||||
- [x] 8.5 Remove redundant certificate synchronization logic
|
|
||||||
- [x] 8.6 Standardize SNI handling between different proxies
|
|
||||||
|
|
||||||
### Phase 9: Context and Configuration Standardization
|
// Rest of the existing function...
|
||||||
- [x] 9.1 Implement a single shared IRouteContext class
|
}
|
||||||
- [x] 9.2 Remove all duplicate context creation logic
|
```
|
||||||
- [x] 9.3 Standardize option interfaces across components
|
|
||||||
- [x] 9.4 Create shared default configurations
|
|
||||||
- [x] 9.5 Implement a unified configuration validation system
|
|
||||||
- [x] 9.6 Add runtime type checking for configurations
|
|
||||||
|
|
||||||
### Phase 10: Component Consolidation
|
2. Making similar modifications to other IP-related functions in the codebase.
|
||||||
- [x] 10.1 Merge SmartProxy and NetworkProxy functionality where appropriate
|
|
||||||
- [x] 10.2 Create a unified connection pool management system
|
|
||||||
- [x] 10.3 Standardize timeout handling across components
|
|
||||||
- [x] 10.4 Implement shared logging and monitoring
|
|
||||||
- [x] 10.5 Remove all deprecated methods and legacy compatibility
|
|
||||||
- [x] 10.6 Reduce API surface area to essentials
|
|
||||||
|
|
||||||
### Phase 11: Performance Optimization & Advanced Features
|
## Wild Card Domain Matching Issue
|
||||||
- [ ] 11.1 Conduct comprehensive performance benchmarking
|
|
||||||
- [ ] 11.2 Optimize memory usage in high-connection scenarios
|
|
||||||
- [ ] 11.3 Implement connection pooling for backend targets
|
|
||||||
- [ ] 11.4 Add support for HTTP/3 and QUIC protocols
|
|
||||||
- [ ] 11.5 Enhance WebSocket support with compression and multiplexing
|
|
||||||
- [ ] 11.6 Add advanced observability through metrics and tracing integration
|
|
||||||
|
|
||||||
## Benefits of Simplified Architecture
|
### Explanation
|
||||||
|
|
||||||
1. **Reduced Duplication**:
|
The wildcard domain matching in SmartProxy works as follows:
|
||||||
- Shared route processing logic
|
|
||||||
- Single certificate management system
|
|
||||||
- Unified context objects
|
|
||||||
|
|
||||||
2. **Simplified Codebase**:
|
1. When a pattern like `*.lossless.digital` is specified, it's converted to a regex: `/^.*\.lossless\.digital$/i`
|
||||||
- Fewer managers with cleaner responsibilities
|
2. This correctly matches any subdomain like `my.lossless.digital`, `api.lossless.digital`, etc.
|
||||||
- Consistent APIs across components
|
3. However, it does NOT match the apex domain `lossless.digital` (without a subdomain)
|
||||||
- Reduced complexity in bridge components
|
|
||||||
|
|
||||||
3. **Improved Maintainability**:
|
If you need to match both the apex domain and subdomains, use a list:
|
||||||
- Easier to understand component relationships
|
```typescript
|
||||||
- Consolidated logic for critical operations
|
domains: ['lossless.digital', '*.lossless.digital']
|
||||||
- Clearer separation of concerns
|
```
|
||||||
|
|
||||||
4. **Enhanced Performance**:
|
## Debugging SmartProxy
|
||||||
- Less overhead in communication between components
|
|
||||||
- Reduced memory usage through shared objects
|
|
||||||
- More efficient request processing
|
|
||||||
|
|
||||||
5. **Better Developer Experience**:
|
To debug routing issues in SmartProxy:
|
||||||
- Consistent conceptual model across system
|
|
||||||
- More intuitive configuration interface
|
|
||||||
- Simplified debugging and troubleshooting
|
|
||||||
|
|
||||||
## Implementation Approach
|
1. Add detailed logging to the `route-manager.js` file in the `dist_ts` directory:
|
||||||
|
- `findMatchingRoute` method - to see what criteria are being checked
|
||||||
|
- `matchRouteDomain` method - to see domain matching logic
|
||||||
|
- `matchDomain` method - to see pattern matching
|
||||||
|
- `matchIpPattern` method - to see IP matching logic
|
||||||
|
|
||||||
The implementation of phases 7-10 will focus on gradually consolidating duplicate functionality:
|
2. Run the proxy with debugging enabled:
|
||||||
|
```
|
||||||
|
pnpm run startNew
|
||||||
|
```
|
||||||
|
|
||||||
1. First, implement shared managers and utilities to be used by both proxies
|
3. Monitor the logs for detailed information about the routing process and identify where matches are failing.
|
||||||
2. Then consolidate certificate management to simplify ACME handling
|
|
||||||
3. Create standardized context objects and configurations
|
|
||||||
4. Finally, merge overlapping functionality between proxy components
|
|
||||||
|
|
||||||
This approach will maintain compatibility with existing code while progressively simplifying the architecture to reduce complexity and improve performance.
|
## Priority and Route Order
|
||||||
|
|
||||||
|
Remember that routes are evaluated in priority order (higher priority first). If multiple routes could match the same request, ensure that the more specific routes have higher priority.
|
||||||
|
|
||||||
|
When routes have the same priority (or none specified), they're evaluated in the order they're defined in the configuration.
|
@ -1,22 +1,20 @@
|
|||||||
import { expect } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||||||
import {
|
import {
|
||||||
EventSystem,
|
EventSystem,
|
||||||
ProxyEvents,
|
ProxyEvents,
|
||||||
ComponentType
|
ComponentType
|
||||||
} from '../../../ts/core/utils/event-system.js';
|
} from '../../../ts/core/utils/event-system.js';
|
||||||
|
|
||||||
// Test event system
|
// Setup function for creating a new event system
|
||||||
expect.describe('Event System', async () => {
|
function setupEventSystem(): { eventSystem: EventSystem, receivedEvents: any[] } {
|
||||||
let eventSystem: EventSystem;
|
const eventSystem = new EventSystem(ComponentType.SMART_PROXY, 'test-id');
|
||||||
let receivedEvents: any[] = [];
|
const receivedEvents: any[] = [];
|
||||||
|
return { eventSystem, receivedEvents };
|
||||||
|
}
|
||||||
|
|
||||||
// Set up a new event system before each test
|
tap.test('Event System - certificate events with correct structure', async () => {
|
||||||
expect.beforeEach(() => {
|
const { eventSystem, receivedEvents } = setupEventSystem();
|
||||||
eventSystem = new EventSystem(ComponentType.SMART_PROXY, 'test-id');
|
|
||||||
receivedEvents = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
expect.it('should emit certificate events with correct structure', async () => {
|
|
||||||
// Set up listeners
|
// Set up listeners
|
||||||
eventSystem.on(ProxyEvents.CERTIFICATE_ISSUED, (data) => {
|
eventSystem.on(ProxyEvents.CERTIFICATE_ISSUED, (data) => {
|
||||||
receivedEvents.push({
|
receivedEvents.push({
|
||||||
@ -49,24 +47,26 @@ expect.describe('Event System', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify events
|
// Verify events
|
||||||
expect(receivedEvents.length).to.equal(2);
|
expect(receivedEvents.length).toEqual(2);
|
||||||
|
|
||||||
// Check issuance event
|
// Check issuance event
|
||||||
expect(receivedEvents[0].type).to.equal('issued');
|
expect(receivedEvents[0].type).toEqual('issued');
|
||||||
expect(receivedEvents[0].data.domain).to.equal('example.com');
|
expect(receivedEvents[0].data.domain).toEqual('example.com');
|
||||||
expect(receivedEvents[0].data.certificate).to.equal('cert-content');
|
expect(receivedEvents[0].data.certificate).toEqual('cert-content');
|
||||||
expect(receivedEvents[0].data.componentType).to.equal(ComponentType.SMART_PROXY);
|
expect(receivedEvents[0].data.componentType).toEqual(ComponentType.SMART_PROXY);
|
||||||
expect(receivedEvents[0].data.componentId).to.equal('test-id');
|
expect(receivedEvents[0].data.componentId).toEqual('test-id');
|
||||||
expect(receivedEvents[0].data.timestamp).to.be.a('number');
|
expect(typeof receivedEvents[0].data.timestamp).toEqual('number');
|
||||||
|
|
||||||
// Check renewal event
|
// Check renewal event
|
||||||
expect(receivedEvents[1].type).to.equal('renewed');
|
expect(receivedEvents[1].type).toEqual('renewed');
|
||||||
expect(receivedEvents[1].data.domain).to.equal('example.com');
|
expect(receivedEvents[1].data.domain).toEqual('example.com');
|
||||||
expect(receivedEvents[1].data.isRenewal).to.be.true;
|
expect(receivedEvents[1].data.isRenewal).toEqual(true);
|
||||||
expect(receivedEvents[1].data.expiryDate).to.deep.equal(new Date('2026-01-01'));
|
expect(receivedEvents[1].data.expiryDate).toEqual(new Date('2026-01-01'));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should emit component lifecycle events', async () => {
|
tap.test('Event System - component lifecycle events', async () => {
|
||||||
|
const { eventSystem, receivedEvents } = setupEventSystem();
|
||||||
|
|
||||||
// Set up listeners
|
// Set up listeners
|
||||||
eventSystem.on(ProxyEvents.COMPONENT_STARTED, (data) => {
|
eventSystem.on(ProxyEvents.COMPONENT_STARTED, (data) => {
|
||||||
receivedEvents.push({
|
receivedEvents.push({
|
||||||
@ -87,19 +87,21 @@ expect.describe('Event System', async () => {
|
|||||||
eventSystem.emitComponentStopped('TestComponent');
|
eventSystem.emitComponentStopped('TestComponent');
|
||||||
|
|
||||||
// Verify events
|
// Verify events
|
||||||
expect(receivedEvents.length).to.equal(2);
|
expect(receivedEvents.length).toEqual(2);
|
||||||
|
|
||||||
// Check started event
|
// Check started event
|
||||||
expect(receivedEvents[0].type).to.equal('started');
|
expect(receivedEvents[0].type).toEqual('started');
|
||||||
expect(receivedEvents[0].data.name).to.equal('TestComponent');
|
expect(receivedEvents[0].data.name).toEqual('TestComponent');
|
||||||
expect(receivedEvents[0].data.version).to.equal('1.0.0');
|
expect(receivedEvents[0].data.version).toEqual('1.0.0');
|
||||||
|
|
||||||
// Check stopped event
|
// Check stopped event
|
||||||
expect(receivedEvents[1].type).to.equal('stopped');
|
expect(receivedEvents[1].type).toEqual('stopped');
|
||||||
expect(receivedEvents[1].data.name).to.equal('TestComponent');
|
expect(receivedEvents[1].data.name).toEqual('TestComponent');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should emit connection events', async () => {
|
tap.test('Event System - connection events', async () => {
|
||||||
|
const { eventSystem, receivedEvents } = setupEventSystem();
|
||||||
|
|
||||||
// Set up listeners
|
// Set up listeners
|
||||||
eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, (data) => {
|
eventSystem.on(ProxyEvents.CONNECTION_ESTABLISHED, (data) => {
|
||||||
receivedEvents.push({
|
receivedEvents.push({
|
||||||
@ -131,21 +133,23 @@ expect.describe('Event System', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify events
|
// Verify events
|
||||||
expect(receivedEvents.length).to.equal(2);
|
expect(receivedEvents.length).toEqual(2);
|
||||||
|
|
||||||
// Check established event
|
// Check established event
|
||||||
expect(receivedEvents[0].type).to.equal('established');
|
expect(receivedEvents[0].type).toEqual('established');
|
||||||
expect(receivedEvents[0].data.connectionId).to.equal('conn-123');
|
expect(receivedEvents[0].data.connectionId).toEqual('conn-123');
|
||||||
expect(receivedEvents[0].data.clientIp).to.equal('192.168.1.1');
|
expect(receivedEvents[0].data.clientIp).toEqual('192.168.1.1');
|
||||||
expect(receivedEvents[0].data.port).to.equal(443);
|
expect(receivedEvents[0].data.port).toEqual(443);
|
||||||
expect(receivedEvents[0].data.isTls).to.be.true;
|
expect(receivedEvents[0].data.isTls).toEqual(true);
|
||||||
|
|
||||||
// Check closed event
|
// Check closed event
|
||||||
expect(receivedEvents[1].type).to.equal('closed');
|
expect(receivedEvents[1].type).toEqual('closed');
|
||||||
expect(receivedEvents[1].data.connectionId).to.equal('conn-123');
|
expect(receivedEvents[1].data.connectionId).toEqual('conn-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should support once and off subscription methods', async () => {
|
tap.test('Event System - once and off subscription methods', async () => {
|
||||||
|
const { eventSystem, receivedEvents } = setupEventSystem();
|
||||||
|
|
||||||
// Set up a listener that should fire only once
|
// Set up a listener that should fire only once
|
||||||
eventSystem.once(ProxyEvents.CONNECTION_ESTABLISHED, (data) => {
|
eventSystem.once(ProxyEvents.CONNECTION_ESTABLISHED, (data) => {
|
||||||
receivedEvents.push({
|
receivedEvents.push({
|
||||||
@ -189,14 +193,15 @@ expect.describe('Event System', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verify events
|
// Verify events
|
||||||
expect(receivedEvents.length).to.equal(3);
|
expect(receivedEvents.length).toEqual(3);
|
||||||
expect(receivedEvents[0].type).to.equal('once');
|
expect(receivedEvents[0].type).toEqual('once');
|
||||||
expect(receivedEvents[0].data.connectionId).to.equal('conn-1');
|
expect(receivedEvents[0].data.connectionId).toEqual('conn-1');
|
||||||
|
|
||||||
expect(receivedEvents[1].type).to.equal('persistent');
|
expect(receivedEvents[1].type).toEqual('persistent');
|
||||||
expect(receivedEvents[1].data.connectionId).to.equal('conn-1');
|
expect(receivedEvents[1].data.connectionId).toEqual('conn-1');
|
||||||
|
|
||||||
expect(receivedEvents[2].type).to.equal('persistent');
|
expect(receivedEvents[2].type).toEqual('persistent');
|
||||||
expect(receivedEvents[2].data.connectionId).to.equal('conn-2');
|
expect(receivedEvents[2].data.connectionId).toEqual('conn-2');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -1,89 +1,82 @@
|
|||||||
import { expect } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||||||
import * as routeUtils from '../../../ts/core/utils/route-utils.js';
|
import * as routeUtils from '../../../ts/core/utils/route-utils.js';
|
||||||
|
|
||||||
// Test domain matching
|
// Test domain matching
|
||||||
expect.describe('Route Utils - Domain Matching', async () => {
|
tap.test('Route Utils - Domain Matching - exact domains', async () => {
|
||||||
expect.it('should match exact domains', async () => {
|
expect(routeUtils.matchDomain('example.com', 'example.com')).toEqual(true);
|
||||||
expect(routeUtils.matchDomain('example.com', 'example.com')).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match wildcard domains', async () => {
|
tap.test('Route Utils - Domain Matching - wildcard domains', async () => {
|
||||||
expect(routeUtils.matchDomain('*.example.com', 'sub.example.com')).to.be.true;
|
expect(routeUtils.matchDomain('*.example.com', 'sub.example.com')).toEqual(true);
|
||||||
expect(routeUtils.matchDomain('*.example.com', 'another.sub.example.com')).to.be.true;
|
expect(routeUtils.matchDomain('*.example.com', 'another.sub.example.com')).toEqual(true);
|
||||||
expect(routeUtils.matchDomain('*.example.com', 'example.com')).to.be.false;
|
expect(routeUtils.matchDomain('*.example.com', 'example.com')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match domains case-insensitively', async () => {
|
tap.test('Route Utils - Domain Matching - case insensitivity', async () => {
|
||||||
expect(routeUtils.matchDomain('example.com', 'EXAMPLE.com')).to.be.true;
|
expect(routeUtils.matchDomain('example.com', 'EXAMPLE.com')).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match routes with multiple domain patterns', async () => {
|
tap.test('Route Utils - Domain Matching - multiple domain patterns', async () => {
|
||||||
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'example.com')).to.be.true;
|
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'example.com')).toEqual(true);
|
||||||
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'sub.test.com')).to.be.true;
|
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'sub.test.com')).toEqual(true);
|
||||||
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'something.else')).to.be.false;
|
expect(routeUtils.matchRouteDomain(['example.com', '*.test.com'], 'something.else')).toEqual(false);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test path matching
|
// Test path matching
|
||||||
expect.describe('Route Utils - Path Matching', async () => {
|
tap.test('Route Utils - Path Matching - exact paths', async () => {
|
||||||
expect.it('should match exact paths', async () => {
|
expect(routeUtils.matchPath('/api/users', '/api/users')).toEqual(true);
|
||||||
expect(routeUtils.matchPath('/api/users', '/api/users')).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match wildcard paths', async () => {
|
tap.test('Route Utils - Path Matching - wildcard paths', async () => {
|
||||||
expect(routeUtils.matchPath('/api/*', '/api/users')).to.be.true;
|
expect(routeUtils.matchPath('/api/*', '/api/users')).toEqual(true);
|
||||||
expect(routeUtils.matchPath('/api/*', '/api/products')).to.be.true;
|
expect(routeUtils.matchPath('/api/*', '/api/products')).toEqual(true);
|
||||||
expect(routeUtils.matchPath('/api/*', '/something/else')).to.be.false;
|
expect(routeUtils.matchPath('/api/*', '/something/else')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match complex wildcard patterns', async () => {
|
tap.test('Route Utils - Path Matching - complex wildcard patterns', async () => {
|
||||||
expect(routeUtils.matchPath('/api/*/details', '/api/users/details')).to.be.true;
|
expect(routeUtils.matchPath('/api/*/details', '/api/users/details')).toEqual(true);
|
||||||
expect(routeUtils.matchPath('/api/*/details', '/api/products/details')).to.be.true;
|
expect(routeUtils.matchPath('/api/*/details', '/api/products/details')).toEqual(true);
|
||||||
expect(routeUtils.matchPath('/api/*/details', '/api/users/other')).to.be.false;
|
expect(routeUtils.matchPath('/api/*/details', '/api/users/other')).toEqual(false);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test IP matching
|
// Test IP matching
|
||||||
expect.describe('Route Utils - IP Matching', async () => {
|
tap.test('Route Utils - IP Matching - exact IPs', async () => {
|
||||||
expect.it('should match exact IPs', async () => {
|
expect(routeUtils.matchIpPattern('192.168.1.1', '192.168.1.1')).toEqual(true);
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.1', '192.168.1.1')).to.be.true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match wildcard IPs', async () => {
|
tap.test('Route Utils - IP Matching - wildcard IPs', async () => {
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.1.100')).to.be.true;
|
expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.1.100')).toEqual(true);
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.2.1')).to.be.false;
|
expect(routeUtils.matchIpPattern('192.168.1.*', '192.168.2.1')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should match CIDR notation', async () => {
|
tap.test('Route Utils - IP Matching - CIDR notation', async () => {
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.1.100')).to.be.true;
|
expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.1.100')).toEqual(true);
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.2.1')).to.be.false;
|
expect(routeUtils.matchIpPattern('192.168.1.0/24', '192.168.2.1')).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should handle IPv6-mapped IPv4 addresses', async () => {
|
tap.test('Route Utils - IP Matching - IPv6-mapped IPv4 addresses', async () => {
|
||||||
expect(routeUtils.matchIpPattern('192.168.1.1', '::ffff:192.168.1.1')).to.be.true;
|
expect(routeUtils.matchIpPattern('192.168.1.1', '::ffff:192.168.1.1')).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.it('should correctly authorize IPs based on allow/block lists', async () => {
|
tap.test('Route Utils - IP Matching - IP authorization with allow/block lists', async () => {
|
||||||
// With allow and block lists
|
// With allow and block lists
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).to.be.true;
|
expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'], ['192.168.1.5'])).toEqual(true);
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).to.be.false;
|
expect(routeUtils.isIpAuthorized('192.168.1.5', ['192.168.1.*'], ['192.168.1.5'])).toEqual(false);
|
||||||
|
|
||||||
// With only allow list
|
// With only allow list
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'])).to.be.true;
|
expect(routeUtils.isIpAuthorized('192.168.1.1', ['192.168.1.*'])).toEqual(true);
|
||||||
expect(routeUtils.isIpAuthorized('192.168.2.1', ['192.168.1.*'])).to.be.false;
|
expect(routeUtils.isIpAuthorized('192.168.2.1', ['192.168.1.*'])).toEqual(false);
|
||||||
|
|
||||||
// With only block list
|
// With only block list
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.5', undefined, ['192.168.1.5'])).to.be.false;
|
expect(routeUtils.isIpAuthorized('192.168.1.5', undefined, ['192.168.1.5'])).toEqual(false);
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.1', undefined, ['192.168.1.5'])).to.be.true;
|
expect(routeUtils.isIpAuthorized('192.168.1.1', undefined, ['192.168.1.5'])).toEqual(true);
|
||||||
|
|
||||||
// With wildcard in allow list
|
// With wildcard in allow list
|
||||||
expect(routeUtils.isIpAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).to.be.true;
|
expect(routeUtils.isIpAuthorized('192.168.1.1', ['*'], ['192.168.1.5'])).toEqual(true);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test route specificity calculation
|
// Test route specificity calculation
|
||||||
expect.describe('Route Utils - Route Specificity', async () => {
|
tap.test('Route Utils - Route Specificity - calculating correctly', async () => {
|
||||||
expect.it('should calculate route specificity correctly', async () => {
|
|
||||||
const basicRoute = { domains: 'example.com' };
|
const basicRoute = { domains: 'example.com' };
|
||||||
const pathRoute = { domains: 'example.com', path: '/api' };
|
const pathRoute = { domains: 'example.com', path: '/api' };
|
||||||
const wildcardPathRoute = { domains: 'example.com', path: '/api/*' };
|
const wildcardPathRoute = { domains: 'example.com', path: '/api/*' };
|
||||||
@ -96,21 +89,22 @@ expect.describe('Route Utils - Route Specificity', async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Path routes should have higher specificity than domain-only routes
|
// Path routes should have higher specificity than domain-only routes
|
||||||
expect(routeUtils.calculateRouteSpecificity(pathRoute))
|
expect(routeUtils.calculateRouteSpecificity(pathRoute) >
|
||||||
.to.be.greaterThan(routeUtils.calculateRouteSpecificity(basicRoute));
|
routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true);
|
||||||
|
|
||||||
// Exact path routes should have higher specificity than wildcard path routes
|
// Exact path routes should have higher specificity than wildcard path routes
|
||||||
expect(routeUtils.calculateRouteSpecificity(pathRoute))
|
expect(routeUtils.calculateRouteSpecificity(pathRoute) >
|
||||||
.to.be.greaterThan(routeUtils.calculateRouteSpecificity(wildcardPathRoute));
|
routeUtils.calculateRouteSpecificity(wildcardPathRoute)).toEqual(true);
|
||||||
|
|
||||||
// Routes with headers should have higher specificity than routes without
|
// Routes with headers should have higher specificity than routes without
|
||||||
expect(routeUtils.calculateRouteSpecificity(headerRoute))
|
expect(routeUtils.calculateRouteSpecificity(headerRoute) >
|
||||||
.to.be.greaterThan(routeUtils.calculateRouteSpecificity(basicRoute));
|
routeUtils.calculateRouteSpecificity(basicRoute)).toEqual(true);
|
||||||
|
|
||||||
// Complex routes should have the highest specificity
|
// Complex routes should have the highest specificity
|
||||||
expect(routeUtils.calculateRouteSpecificity(complexRoute))
|
expect(routeUtils.calculateRouteSpecificity(complexRoute) >
|
||||||
.to.be.greaterThan(routeUtils.calculateRouteSpecificity(pathRoute));
|
routeUtils.calculateRouteSpecificity(pathRoute)).toEqual(true);
|
||||||
expect(routeUtils.calculateRouteSpecificity(complexRoute))
|
expect(routeUtils.calculateRouteSpecificity(complexRoute) >
|
||||||
.to.be.greaterThan(routeUtils.calculateRouteSpecificity(headerRoute));
|
routeUtils.calculateRouteSpecificity(headerRoute)).toEqual(true);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@ -1,24 +1,22 @@
|
|||||||
import { expect, tap } from '@push.rocks/tapbundle';
|
import { expect, tap } from '@push.rocks/tapbundle';
|
||||||
|
import * as plugins from '../ts/plugins.js';
|
||||||
import { NetworkProxy } from '../ts/proxies/network-proxy/index.js';
|
import { NetworkProxy } from '../ts/proxies/network-proxy/index.js';
|
||||||
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
import type { IRouteContext } from '../ts/core/models/route-context.js';
|
import type { IRouteContext } from '../ts/core/models/route-context.js';
|
||||||
import * as http from 'http';
|
|
||||||
import * as https from 'https';
|
|
||||||
import * as http2 from 'http2';
|
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
// Declare variables for tests
|
// Declare variables for tests
|
||||||
let networkProxy: NetworkProxy;
|
let networkProxy: NetworkProxy;
|
||||||
let testServer: http.Server;
|
let testServer: plugins.http.Server;
|
||||||
let testServerHttp2: http2.Http2Server;
|
let testServerHttp2: plugins.http2.Http2Server;
|
||||||
let serverPort: number;
|
let serverPort: number;
|
||||||
let serverPortHttp2: number;
|
let serverPortHttp2: number;
|
||||||
|
|
||||||
// Setup test environment
|
// Setup test environment
|
||||||
tap.test('setup NetworkProxy function-based targets test environment', async () => {
|
tap.test('setup NetworkProxy function-based targets test environment', async () => {
|
||||||
// Create simple HTTP server to respond to requests
|
// Create simple HTTP server to respond to requests
|
||||||
testServer = http.createServer((req, res) => {
|
testServer = plugins.http.createServer((req, res) => {
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
url: req.url,
|
url: req.url,
|
||||||
@ -29,7 +27,7 @@ tap.test('setup NetworkProxy function-based targets test environment', async ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create simple HTTP/2 server to respond to requests
|
// Create simple HTTP/2 server to respond to requests
|
||||||
testServerHttp2 = http2.createServer();
|
testServerHttp2 = plugins.http2.createServer();
|
||||||
testServerHttp2.on('stream', (stream, headers) => {
|
testServerHttp2.on('stream', (stream, headers) => {
|
||||||
stream.respond({
|
stream.respond({
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@ -82,10 +80,10 @@ tap.test('should support static host/port routes', async () => {
|
|||||||
const routes: IRouteConfig[] = [
|
const routes: IRouteConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'static-route',
|
name: 'static-route',
|
||||||
domain: 'example.com',
|
|
||||||
priority: 100,
|
priority: 100,
|
||||||
match: {
|
match: {
|
||||||
domain: 'example.com'
|
domains: 'example.com',
|
||||||
|
ports: 0
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
@ -124,10 +122,10 @@ tap.test('should support function-based host', async () => {
|
|||||||
const routes: IRouteConfig[] = [
|
const routes: IRouteConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'function-host-route',
|
name: 'function-host-route',
|
||||||
domain: 'function.example.com',
|
|
||||||
priority: 100,
|
priority: 100,
|
||||||
match: {
|
match: {
|
||||||
domain: 'function.example.com'
|
domains: 'function.example.com',
|
||||||
|
ports: 0
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
@ -169,10 +167,10 @@ tap.test('should support function-based port', async () => {
|
|||||||
const routes: IRouteConfig[] = [
|
const routes: IRouteConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'function-port-route',
|
name: 'function-port-route',
|
||||||
domain: 'function-port.example.com',
|
|
||||||
priority: 100,
|
priority: 100,
|
||||||
match: {
|
match: {
|
||||||
domain: 'function-port.example.com'
|
domains: 'function-port.example.com',
|
||||||
|
ports: 0
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
@ -214,10 +212,10 @@ tap.test('should support function-based host AND port', async () => {
|
|||||||
const routes: IRouteConfig[] = [
|
const routes: IRouteConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'function-both-route',
|
name: 'function-both-route',
|
||||||
domain: 'function-both.example.com',
|
|
||||||
priority: 100,
|
priority: 100,
|
||||||
match: {
|
match: {
|
||||||
domain: 'function-both.example.com'
|
domains: 'function-both.example.com',
|
||||||
|
ports: 0
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
@ -260,10 +258,10 @@ tap.test('should support context-based routing with path', async () => {
|
|||||||
const routes: IRouteConfig[] = [
|
const routes: IRouteConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'context-path-route',
|
name: 'context-path-route',
|
||||||
domain: 'context.example.com',
|
|
||||||
priority: 100,
|
priority: 100,
|
||||||
match: {
|
match: {
|
||||||
domain: 'context.example.com'
|
domains: 'context.example.com',
|
||||||
|
ports: 0
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'forward',
|
type: 'forward',
|
||||||
@ -338,10 +336,10 @@ tap.test('cleanup NetworkProxy function-based targets test environment', async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to make HTTPS requests with self-signed certificate support
|
// Helper function to make HTTPS requests with self-signed certificate support
|
||||||
async function makeRequest(options: http.RequestOptions): Promise<{ statusCode: number, headers: http.IncomingHttpHeaders, body: string }> {
|
async function makeRequest(options: plugins.http.RequestOptions): Promise<{ statusCode: number, headers: plugins.http.IncomingHttpHeaders, body: string }> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Use HTTPS with rejectUnauthorized: false to accept self-signed certificates
|
// Use HTTPS with rejectUnauthorized: false to accept self-signed certificates
|
||||||
const req = https.request({
|
const req = plugins.https.request({
|
||||||
...options,
|
...options,
|
||||||
rejectUnauthorized: false, // Accept self-signed certificates
|
rejectUnauthorized: false, // Accept self-signed certificates
|
||||||
}, (res) => {
|
}, (res) => {
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '16.0.2',
|
version: '16.0.3',
|
||||||
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.'
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared Route Context Interface
|
* Shared Route Context Interface
|
||||||
*
|
*
|
||||||
@ -42,8 +44,8 @@ export interface IRouteContext {
|
|||||||
* Used only in NetworkProxy for HTTP request handling
|
* Used only in NetworkProxy for HTTP request handling
|
||||||
*/
|
*/
|
||||||
export interface IHttpRouteContext extends IRouteContext {
|
export interface IHttpRouteContext extends IRouteContext {
|
||||||
req?: any; // http.IncomingMessage
|
req?: plugins.http.IncomingMessage;
|
||||||
res?: any; // http.ServerResponse
|
res?: plugins.http.ServerResponse;
|
||||||
method?: string; // HTTP method (GET, POST, etc.)
|
method?: string; // HTTP method (GET, POST, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ export interface IHttpRouteContext extends IRouteContext {
|
|||||||
* Used only in NetworkProxy for HTTP/2 request handling
|
* Used only in NetworkProxy for HTTP/2 request handling
|
||||||
*/
|
*/
|
||||||
export interface IHttp2RouteContext extends IHttpRouteContext {
|
export interface IHttp2RouteContext extends IHttpRouteContext {
|
||||||
stream?: any; // http2.Http2Stream
|
stream?: plugins.http2.ServerHttp2Stream;
|
||||||
headers?: Record<string, string>; // HTTP/2 pseudo-headers like :method, :path
|
headers?: Record<string, string>; // HTTP/2 pseudo-headers like :method, :path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
* @returns Whether the domain matches the pattern
|
* @returns Whether the domain matches the pattern
|
||||||
*/
|
*/
|
||||||
export function matchDomain(pattern: string, domain: string): boolean {
|
export function matchDomain(pattern: string, domain: string): boolean {
|
||||||
// Handle exact match
|
// Handle exact match (case-insensitive)
|
||||||
if (pattern === domain) {
|
if (pattern.toLowerCase() === domain.toLowerCase()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,9 +139,13 @@ export function matchIpCidr(cidr: string, ip: string): boolean {
|
|||||||
try {
|
try {
|
||||||
const { subnet, bits } = parsed;
|
const { subnet, bits } = parsed;
|
||||||
|
|
||||||
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
|
const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet;
|
||||||
|
|
||||||
// Convert IP addresses to numeric values
|
// Convert IP addresses to numeric values
|
||||||
const ipNum = ipToNumber(ip);
|
const ipNum = ipToNumber(normalizedIp);
|
||||||
const subnetNum = ipToNumber(subnet);
|
const subnetNum = ipToNumber(normalizedSubnet);
|
||||||
|
|
||||||
// Calculate subnet mask
|
// Calculate subnet mask
|
||||||
const maskNum = ~(2 ** (32 - bits) - 1);
|
const maskNum = ~(2 ** (32 - bits) - 1);
|
||||||
@ -161,26 +165,41 @@ export function matchIpCidr(cidr: string, ip: string): boolean {
|
|||||||
* @returns Whether the IP matches the pattern
|
* @returns Whether the IP matches the pattern
|
||||||
*/
|
*/
|
||||||
export function matchIpPattern(pattern: string, ip: string): boolean {
|
export function matchIpPattern(pattern: string, ip: string): boolean {
|
||||||
// Handle exact match
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
if (pattern === ip) {
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
|
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||||
|
|
||||||
|
// Handle exact match with all variations
|
||||||
|
if (pattern === ip || normalizedPattern === normalizedIp ||
|
||||||
|
pattern === normalizedIp || normalizedPattern === ip) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle "all" wildcard
|
// Handle "all" wildcard
|
||||||
if (pattern === '*') {
|
if (pattern === '*' || normalizedPattern === '*') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
||||||
if (pattern.includes('/')) {
|
if (pattern.includes('/')) {
|
||||||
return matchIpCidr(pattern, ip);
|
return matchIpCidr(pattern, normalizedIp) ||
|
||||||
|
(normalizedPattern !== pattern && matchIpCidr(normalizedPattern, normalizedIp));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle glob pattern (e.g., 192.168.1.*)
|
// Handle glob pattern (e.g., 192.168.1.*)
|
||||||
if (pattern.includes('*')) {
|
if (pattern.includes('*')) {
|
||||||
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
const regex = new RegExp(`^${regexPattern}$`);
|
||||||
return regex.test(ip);
|
if (regex.test(ip) || regex.test(normalizedIp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pattern was normalized, also test with normalized pattern
|
||||||
|
if (normalizedPattern !== pattern) {
|
||||||
|
const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||||
|
const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`);
|
||||||
|
return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -291,12 +291,15 @@ export class RouteManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Match an IP pattern against an IP
|
* Match an IP pattern against an IP
|
||||||
|
* Supports exact matches, wildcard patterns, and CIDR notation
|
||||||
*/
|
*/
|
||||||
private matchIp(pattern: string, ip: string): boolean {
|
private matchIp(pattern: string, ip: string): boolean {
|
||||||
|
// Exact match
|
||||||
if (pattern === ip) {
|
if (pattern === ip) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wildcard matching (e.g., 192.168.0.*)
|
||||||
if (pattern.includes('*')) {
|
if (pattern.includes('*')) {
|
||||||
const regexPattern = pattern
|
const regexPattern = pattern
|
||||||
.replace(/\./g, '\\.')
|
.replace(/\./g, '\\.')
|
||||||
@ -306,10 +309,65 @@ export class RouteManager {
|
|||||||
return regex.test(ip);
|
return regex.test(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement CIDR matching
|
// CIDR matching (e.g., 192.168.0.0/24)
|
||||||
|
if (pattern.includes('/')) {
|
||||||
|
try {
|
||||||
|
const [subnet, bits] = pattern.split('/');
|
||||||
|
|
||||||
|
// Convert IP addresses to numeric format for comparison
|
||||||
|
const ipBinary = this.ipToBinary(ip);
|
||||||
|
const subnetBinary = this.ipToBinary(subnet);
|
||||||
|
|
||||||
|
if (!ipBinary || !subnetBinary) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the subnet mask from CIDR notation
|
||||||
|
const mask = parseInt(bits, 10);
|
||||||
|
if (isNaN(mask) || mask < 0 || mask > 32) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the first 'mask' bits match between IP and subnet
|
||||||
|
return ipBinary.slice(0, mask) === subnetBinary.slice(0, mask);
|
||||||
|
} catch (error) {
|
||||||
|
// If we encounter any error during CIDR matching, return false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an IP address to its binary representation
|
||||||
|
* @param ip The IP address to convert
|
||||||
|
* @returns Binary string representation or null if invalid
|
||||||
|
*/
|
||||||
|
private ipToBinary(ip: string): string | null {
|
||||||
|
// Handle IPv4 addresses only for now
|
||||||
|
const parts = ip.split('.');
|
||||||
|
|
||||||
|
// Validate IP format
|
||||||
|
if (parts.length !== 4) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert each octet to 8-bit binary and concatenate
|
||||||
|
try {
|
||||||
|
return parts
|
||||||
|
.map(part => {
|
||||||
|
const num = parseInt(part, 10);
|
||||||
|
if (isNaN(num) || num < 0 || num > 255) {
|
||||||
|
throw new Error('Invalid IP octet');
|
||||||
|
}
|
||||||
|
return num.toString(2).padStart(8, '0');
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -500,68 +500,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`);
|
this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Legacy methods have been removed.
|
||||||
* @deprecated Use updateRouteConfigs instead
|
// Please use updateRouteConfigs() directly with modern route-based configuration.
|
||||||
* Legacy method for updating proxy configurations using IReverseProxyConfig
|
|
||||||
* This method is maintained for backward compatibility
|
|
||||||
*/
|
|
||||||
public async updateProxyConfigs(
|
|
||||||
proxyConfigsArg: IReverseProxyConfig[]
|
|
||||||
): Promise<void> {
|
|
||||||
this.logger.info(`Converting ${proxyConfigsArg.length} legacy configs to route configs`);
|
|
||||||
|
|
||||||
// Convert legacy configs to route configs
|
|
||||||
const routes: IRouteConfig[] = proxyConfigsArg.map(config =>
|
|
||||||
convertLegacyConfigToRouteConfig(config, this.options.port)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use the primary method
|
|
||||||
return this.updateRouteConfigs(routes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use route-based configuration instead
|
|
||||||
* Converts SmartProxy domain configurations to NetworkProxy configs
|
|
||||||
* This method is maintained for backward compatibility
|
|
||||||
*/
|
|
||||||
public convertSmartProxyConfigs(
|
|
||||||
domainConfigs: Array<{
|
|
||||||
domains: string[];
|
|
||||||
targetIPs?: string[];
|
|
||||||
allowedIPs?: string[];
|
|
||||||
}>,
|
|
||||||
sslKeyPair?: { key: string; cert: string }
|
|
||||||
): IReverseProxyConfig[] {
|
|
||||||
this.logger.warn('convertSmartProxyConfigs is deprecated - use route-based configuration instead');
|
|
||||||
|
|
||||||
const proxyConfigs: IReverseProxyConfig[] = [];
|
|
||||||
|
|
||||||
// Use default certificates if not provided
|
|
||||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
|
||||||
const sslKey = sslKeyPair?.key || defaultCerts.key;
|
|
||||||
const sslCert = sslKeyPair?.cert || defaultCerts.cert;
|
|
||||||
|
|
||||||
for (const domainConfig of domainConfigs) {
|
|
||||||
// Each domain in the domains array gets its own config
|
|
||||||
for (const domain of domainConfig.domains) {
|
|
||||||
// Skip non-hostname patterns (like IP addresses)
|
|
||||||
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyConfigs.push({
|
|
||||||
hostName: domain,
|
|
||||||
destinationIps: domainConfig.targetIPs || ['localhost'],
|
|
||||||
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
|
||||||
privateKey: sslKey,
|
|
||||||
publicKey: sslCert
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
|
||||||
return proxyConfigs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds default headers to be included in all responses
|
* Adds default headers to be included in all responses
|
||||||
@ -650,62 +590,4 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
public getRouteConfigs(): IRouteConfig[] {
|
public getRouteConfigs(): IRouteConfig[] {
|
||||||
return this.routeManager.getRoutes();
|
return this.routeManager.getRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use getRouteConfigs instead
|
|
||||||
* Gets all proxy configurations currently in use in the legacy format
|
|
||||||
* This method is maintained for backward compatibility
|
|
||||||
*/
|
|
||||||
public getProxyConfigs(): IReverseProxyConfig[] {
|
|
||||||
this.logger.warn('getProxyConfigs is deprecated - use getRouteConfigs instead');
|
|
||||||
|
|
||||||
// Create legacy proxy configs from our route configurations
|
|
||||||
const legacyConfigs: IReverseProxyConfig[] = [];
|
|
||||||
const currentRoutes = this.routeManager.getRoutes();
|
|
||||||
|
|
||||||
for (const route of currentRoutes) {
|
|
||||||
// Skip non-forward routes or routes without domains
|
|
||||||
if (route.action.type !== 'forward' || !route.match.domains || !route.action.target) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip routes with function-based targets
|
|
||||||
if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get domains
|
|
||||||
const domains = Array.isArray(route.match.domains)
|
|
||||||
? route.match.domains.filter(d => !d.includes('*'))
|
|
||||||
: route.match.domains.includes('*') ? [] : [route.match.domains];
|
|
||||||
|
|
||||||
// Get certificate
|
|
||||||
let privateKey = '';
|
|
||||||
let publicKey = '';
|
|
||||||
|
|
||||||
if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') {
|
|
||||||
privateKey = route.action.tls.certificate.key;
|
|
||||||
publicKey = route.action.tls.certificate.cert;
|
|
||||||
} else {
|
|
||||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
|
||||||
privateKey = defaultCerts.key;
|
|
||||||
publicKey = defaultCerts.cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create legacy config for each domain
|
|
||||||
for (const domain of domains) {
|
|
||||||
legacyConfigs.push({
|
|
||||||
hostName: domain,
|
|
||||||
destinationIps: Array.isArray(route.action.target.host)
|
|
||||||
? route.action.target.host
|
|
||||||
: [route.action.target.host],
|
|
||||||
destinationPorts: [route.action.target.port],
|
|
||||||
privateKey,
|
|
||||||
publicKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return legacyConfigs;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -661,150 +661,6 @@ export class RequestHandler {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// Find target based on hostname
|
|
||||||
const proxyConfig = this.router.routeReq(req);
|
|
||||||
|
|
||||||
if (!proxyConfig) {
|
|
||||||
// No matching proxy configuration
|
|
||||||
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('Not Found: No proxy configuration for this host');
|
|
||||||
|
|
||||||
// Increment failed requests counter
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementFailedRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get destination IP using round-robin if multiple IPs configured
|
|
||||||
const destination = this.connectionPool.getNextTarget(
|
|
||||||
proxyConfig.destinationIps,
|
|
||||||
proxyConfig.destinationPorts[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create options for the proxy request
|
|
||||||
const options: plugins.http.RequestOptions = {
|
|
||||||
hostname: destination.host,
|
|
||||||
port: destination.port,
|
|
||||||
path: req.url,
|
|
||||||
method: req.method,
|
|
||||||
headers: { ...req.headers }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove host header to avoid issues with virtual hosts on target server
|
|
||||||
// The host header should match the target server's expected hostname
|
|
||||||
if (options.headers && options.headers.host) {
|
|
||||||
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
|
||||||
options.headers.host = `${destination.host}:${destination.port}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Proxying request to ${destination.host}:${destination.port}${req.url}`,
|
|
||||||
{ method: req.method }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create proxy request
|
|
||||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
|
||||||
// Copy status code
|
|
||||||
res.statusCode = proxyRes.statusCode || 500;
|
|
||||||
|
|
||||||
// Copy headers from proxy response to client response
|
|
||||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
res.setHeader(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pipe proxy response to client response
|
|
||||||
proxyRes.pipe(res);
|
|
||||||
|
|
||||||
// Increment served requests counter when the response finishes
|
|
||||||
res.on('finish', () => {
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementRequestsServed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the completed request
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.debug(
|
|
||||||
`Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`,
|
|
||||||
{ duration, statusCode: res.statusCode }
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle proxy request errors
|
|
||||||
proxyReq.on('error', (error) => {
|
|
||||||
const duration = Date.now() - startTime;
|
|
||||||
this.logger.error(
|
|
||||||
`Proxy error for ${req.method} ${req.url}: ${error.message}`,
|
|
||||||
{ duration, error: error.message }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increment failed requests counter
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementFailedRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if headers have already been sent
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.statusCode = 502;
|
|
||||||
res.end(`Bad Gateway: ${error.message}`);
|
|
||||||
} else {
|
|
||||||
// If headers already sent, just close the connection
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pipe request body to proxy request and handle client-side errors
|
|
||||||
req.pipe(proxyReq);
|
|
||||||
|
|
||||||
// Handle client disconnection
|
|
||||||
req.on('error', (error) => {
|
|
||||||
this.logger.debug(`Client connection error: ${error.message}`);
|
|
||||||
proxyReq.destroy();
|
|
||||||
|
|
||||||
// Increment failed requests counter on client errors
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementFailedRequests();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle response errors
|
|
||||||
res.on('error', (error) => {
|
|
||||||
this.logger.debug(`Response error: ${error.message}`);
|
|
||||||
proxyReq.destroy();
|
|
||||||
|
|
||||||
// Increment failed requests counter on response errors
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementFailedRequests();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Handle any unexpected errors
|
|
||||||
this.logger.error(
|
|
||||||
`Unexpected error handling request: ${error.message}`,
|
|
||||||
{ error: error.stack }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increment failed requests counter
|
|
||||||
if (this.metricsTracker) {
|
|
||||||
this.metricsTracker.incrementFailedRequests();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.statusCode = 500;
|
|
||||||
res.end('Internal Server Error');
|
|
||||||
} else {
|
|
||||||
res.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +56,7 @@ export interface IRouteContext {
|
|||||||
routeId?: string; // The ID of the matched route
|
routeId?: string; // The ID of the matched route
|
||||||
|
|
||||||
// Target information (resolved from dynamic mapping)
|
// Target information (resolved from dynamic mapping)
|
||||||
targetHost?: string; // The resolved target host
|
targetHost?: string | string[]; // The resolved target host(s)
|
||||||
targetPort?: number; // The resolved target port
|
targetPort?: number; // The resolved target port
|
||||||
|
|
||||||
// Additional properties
|
// Additional properties
|
||||||
@ -68,8 +68,8 @@ export interface IRouteContext {
|
|||||||
* Target configuration for forwarding
|
* Target configuration for forwarding
|
||||||
*/
|
*/
|
||||||
export interface IRouteTarget {
|
export interface IRouteTarget {
|
||||||
host: string | string[] | ((context: any) => string | string[]); // Support static or dynamic host selection with any compatible context
|
host: string | string[] | ((context: IRouteContext) => string | string[]); // Host or hosts with optional function for dynamic resolution
|
||||||
port: number | ((context: any) => number); // Support static or dynamic port mapping with any compatible context
|
port: number | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping
|
||||||
preservePort?: boolean; // Use incoming port as target port (ignored if port is a function)
|
preservePort?: boolean; // Use incoming port as target port (ignored if port is a function)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,8 @@ export interface IRouteAuthentication {
|
|||||||
oauthClientId?: string;
|
oauthClientId?: string;
|
||||||
oauthClientSecret?: string;
|
oauthClientSecret?: string;
|
||||||
oauthRedirectUri?: string;
|
oauthRedirectUri?: string;
|
||||||
[key: string]: any; // Allow additional auth-specific options
|
// Specific options for different auth types
|
||||||
|
options?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,498 +0,0 @@
|
|||||||
import type {
|
|
||||||
IRouteConfig,
|
|
||||||
IRouteMatch,
|
|
||||||
IRouteAction,
|
|
||||||
IRouteTarget,
|
|
||||||
IRouteTls,
|
|
||||||
IRouteRedirect,
|
|
||||||
IRouteSecurity,
|
|
||||||
IRouteAdvanced,
|
|
||||||
TPortRange
|
|
||||||
} from './models/route-types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Basic helper function to create a route configuration
|
|
||||||
*/
|
|
||||||
export function createRoute(
|
|
||||||
match: IRouteMatch,
|
|
||||||
action: IRouteAction,
|
|
||||||
metadata?: {
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return {
|
|
||||||
match,
|
|
||||||
action,
|
|
||||||
...metadata
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a basic HTTP route configuration
|
|
||||||
*/
|
|
||||||
export function createHttpRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 80
|
|
||||||
domains?: string | string[];
|
|
||||||
path?: string;
|
|
||||||
target: IRouteTarget;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || 80,
|
|
||||||
...(options.domains ? { domains: options.domains } : {}),
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: options.target,
|
|
||||||
...(options.headers || options.security ? {
|
|
||||||
advanced: {
|
|
||||||
...(options.headers ? { headers: options.headers } : {})
|
|
||||||
},
|
|
||||||
...(options.security ? { security: options.security } : {})
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'HTTP Route',
|
|
||||||
description: options.description,
|
|
||||||
priority: options.priority,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTPS route configuration with TLS termination
|
|
||||||
*/
|
|
||||||
export function createHttpsRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 443
|
|
||||||
domains: string | string[];
|
|
||||||
path?: string;
|
|
||||||
target: IRouteTarget;
|
|
||||||
tlsMode?: 'terminate' | 'terminate-and-reencrypt';
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || 443,
|
|
||||||
domains: options.domains,
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: options.target,
|
|
||||||
tls: {
|
|
||||||
mode: options.tlsMode || 'terminate',
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
},
|
|
||||||
...(options.headers || options.security ? {
|
|
||||||
advanced: {
|
|
||||||
...(options.headers ? { headers: options.headers } : {})
|
|
||||||
},
|
|
||||||
...(options.security ? { security: options.security } : {})
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'HTTPS Route',
|
|
||||||
description: options.description,
|
|
||||||
priority: options.priority,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTPS passthrough route configuration
|
|
||||||
*/
|
|
||||||
export function createPassthroughRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 443
|
|
||||||
domains?: string | string[];
|
|
||||||
target: IRouteTarget;
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || 443,
|
|
||||||
...(options.domains ? { domains: options.domains } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: options.target,
|
|
||||||
tls: {
|
|
||||||
mode: 'passthrough'
|
|
||||||
},
|
|
||||||
...(options.security ? { security: options.security } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'HTTPS Passthrough Route',
|
|
||||||
description: options.description,
|
|
||||||
priority: options.priority,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a redirect route configuration
|
|
||||||
*/
|
|
||||||
export function createRedirectRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 80
|
|
||||||
domains?: string | string[];
|
|
||||||
path?: string;
|
|
||||||
redirectTo: string;
|
|
||||||
statusCode?: 301 | 302 | 307 | 308;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || 80,
|
|
||||||
...(options.domains ? { domains: options.domains } : {}),
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'redirect',
|
|
||||||
redirect: {
|
|
||||||
to: options.redirectTo,
|
|
||||||
status: options.statusCode || 301
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'Redirect Route',
|
|
||||||
description: options.description,
|
|
||||||
priority: options.priority,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an HTTP to HTTPS redirect route configuration
|
|
||||||
*/
|
|
||||||
export function createHttpToHttpsRedirect(
|
|
||||||
options: {
|
|
||||||
domains: string | string[];
|
|
||||||
statusCode?: 301 | 302 | 307 | 308;
|
|
||||||
name?: string;
|
|
||||||
priority?: number;
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
|
|
||||||
|
|
||||||
return createRedirectRoute({
|
|
||||||
ports: 80,
|
|
||||||
domains: options.domains,
|
|
||||||
redirectTo: 'https://{domain}{path}',
|
|
||||||
statusCode: options.statusCode || 301,
|
|
||||||
name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
|
|
||||||
priority: options.priority || 100 // High priority for redirects
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a block route configuration
|
|
||||||
*/
|
|
||||||
export function createBlockRoute(
|
|
||||||
options: {
|
|
||||||
ports: number | number[];
|
|
||||||
domains?: string | string[];
|
|
||||||
clientIp?: string[];
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports,
|
|
||||||
...(options.domains ? { domains: options.domains } : {}),
|
|
||||||
...(options.clientIp ? { clientIp: options.clientIp } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'Block Route',
|
|
||||||
description: options.description,
|
|
||||||
priority: options.priority || 1000, // Very high priority for blocks
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a load balancer route configuration
|
|
||||||
*/
|
|
||||||
export function createLoadBalancerRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 443
|
|
||||||
domains: string | string[];
|
|
||||||
path?: string;
|
|
||||||
targets: string[]; // Array of host names/IPs for load balancing
|
|
||||||
targetPort: number;
|
|
||||||
tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
const useTls = options.tlsMode !== undefined;
|
|
||||||
const defaultPort = useTls ? 443 : 80;
|
|
||||||
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || defaultPort,
|
|
||||||
domains: options.domains,
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: options.targets,
|
|
||||||
port: options.targetPort
|
|
||||||
},
|
|
||||||
...(useTls ? {
|
|
||||||
tls: {
|
|
||||||
mode: options.tlsMode!,
|
|
||||||
...(options.tlsMode !== 'passthrough' && options.certificate ? {
|
|
||||||
certificate: options.certificate
|
|
||||||
} : {})
|
|
||||||
}
|
|
||||||
} : {}),
|
|
||||||
...(options.headers || options.security ? {
|
|
||||||
advanced: {
|
|
||||||
...(options.headers ? { headers: options.headers } : {})
|
|
||||||
},
|
|
||||||
...(options.security ? { security: options.security } : {})
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'Load Balanced Route',
|
|
||||||
description: options.description || `Load balancing across ${options.targets.length} backends`,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a complete HTTPS server configuration with HTTP redirect
|
|
||||||
*/
|
|
||||||
export function createHttpsServer(
|
|
||||||
options: {
|
|
||||||
domains: string | string[];
|
|
||||||
target: IRouteTarget;
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
addHttpRedirect?: boolean;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
): IRouteConfig[] {
|
|
||||||
const routes: IRouteConfig[] = [];
|
|
||||||
const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
|
|
||||||
|
|
||||||
// Add HTTPS route
|
|
||||||
routes.push(createHttpsRoute({
|
|
||||||
domains: options.domains,
|
|
||||||
target: options.target,
|
|
||||||
certificate: options.certificate || 'auto',
|
|
||||||
security: options.security,
|
|
||||||
name: options.name || `HTTPS Server for ${domainArray.join(', ')}`
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Add HTTP to HTTPS redirect if requested
|
|
||||||
if (options.addHttpRedirect !== false) {
|
|
||||||
routes.push(createHttpToHttpsRedirect({
|
|
||||||
domains: options.domains,
|
|
||||||
name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
|
|
||||||
priority: 100
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return routes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a port range configuration from various input formats
|
|
||||||
*/
|
|
||||||
export function createPortRange(
|
|
||||||
ports: number | number[] | string | Array<{ from: number; to: number }>
|
|
||||||
): TPortRange {
|
|
||||||
// If it's a string like "80,443" or "8000-9000", parse it
|
|
||||||
if (typeof ports === 'string') {
|
|
||||||
if (ports.includes('-')) {
|
|
||||||
// Handle range like "8000-9000"
|
|
||||||
const [start, end] = ports.split('-').map(p => parseInt(p.trim(), 10));
|
|
||||||
return [{ from: start, to: end }];
|
|
||||||
} else if (ports.includes(',')) {
|
|
||||||
// Handle comma-separated list like "80,443,8080"
|
|
||||||
return ports.split(',').map(p => parseInt(p.trim(), 10));
|
|
||||||
} else {
|
|
||||||
// Handle single port as string
|
|
||||||
return parseInt(ports.trim(), 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise return as is
|
|
||||||
return ports;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a security configuration object
|
|
||||||
*/
|
|
||||||
export function createSecurityConfig(
|
|
||||||
options: {
|
|
||||||
allowedIps?: string[];
|
|
||||||
blockedIps?: string[];
|
|
||||||
maxConnections?: number;
|
|
||||||
authentication?: {
|
|
||||||
type: 'basic' | 'digest' | 'oauth';
|
|
||||||
// Auth-specific options
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
): IRouteSecurity {
|
|
||||||
return {
|
|
||||||
...(options.allowedIps ? { allowedIps: options.allowedIps } : {}),
|
|
||||||
...(options.blockedIps ? { blockedIps: options.blockedIps } : {}),
|
|
||||||
...(options.maxConnections ? { maxConnections: options.maxConnections } : {}),
|
|
||||||
...(options.authentication ? { authentication: options.authentication } : {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a static file server route
|
|
||||||
*/
|
|
||||||
export function createStaticFileRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 80
|
|
||||||
domains: string | string[];
|
|
||||||
path?: string;
|
|
||||||
targetDirectory: string;
|
|
||||||
tlsMode?: 'terminate' | 'terminate-and-reencrypt';
|
|
||||||
certificate?: 'auto' | { key: string; cert: string };
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
security?: IRouteSecurity;
|
|
||||||
name?: string;
|
|
||||||
description?: string;
|
|
||||||
priority?: number;
|
|
||||||
tags?: string[];
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
const useTls = options.tlsMode !== undefined;
|
|
||||||
const defaultPort = useTls ? 443 : 80;
|
|
||||||
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || defaultPort,
|
|
||||||
domains: options.domains,
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: 'localhost', // Static file serving is typically handled locally
|
|
||||||
port: 0, // Special value indicating a static file server
|
|
||||||
preservePort: false
|
|
||||||
},
|
|
||||||
...(useTls ? {
|
|
||||||
tls: {
|
|
||||||
mode: options.tlsMode!,
|
|
||||||
certificate: options.certificate || 'auto'
|
|
||||||
}
|
|
||||||
} : {}),
|
|
||||||
advanced: {
|
|
||||||
...(options.headers ? { headers: options.headers } : {}),
|
|
||||||
staticFiles: {
|
|
||||||
root: options.targetDirectory,
|
|
||||||
index: ['index.html', 'index.htm'],
|
|
||||||
directory: options.targetDirectory // For backward compatibility
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...(options.security ? { security: options.security } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'Static File Server',
|
|
||||||
description: options.description || `Serving static files from ${options.targetDirectory}`,
|
|
||||||
priority: options.priority,
|
|
||||||
tags: options.tags
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a test route for debugging purposes
|
|
||||||
*/
|
|
||||||
export function createTestRoute(
|
|
||||||
options: {
|
|
||||||
ports?: number | number[]; // Default: 8000
|
|
||||||
domains?: string | string[];
|
|
||||||
path?: string;
|
|
||||||
response?: {
|
|
||||||
status?: number;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
body?: string;
|
|
||||||
};
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
): IRouteConfig {
|
|
||||||
return createRoute(
|
|
||||||
{
|
|
||||||
ports: options.ports || 8000,
|
|
||||||
...(options.domains ? { domains: options.domains } : {}),
|
|
||||||
...(options.path ? { path: options.path } : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'forward',
|
|
||||||
target: {
|
|
||||||
host: 'test', // Special value indicating a test route
|
|
||||||
port: 0
|
|
||||||
},
|
|
||||||
advanced: {
|
|
||||||
testResponse: {
|
|
||||||
status: options.response?.status || 200,
|
|
||||||
headers: options.response?.headers || { 'Content-Type': 'text/plain' },
|
|
||||||
body: options.response?.body || 'Test route is working!'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: options.name || 'Test Route',
|
|
||||||
description: 'Route for testing and debugging',
|
|
||||||
priority: 500,
|
|
||||||
tags: ['test', 'debug']
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Route helpers for SmartProxy
|
|
||||||
*
|
|
||||||
* This module provides helper functions for creating various types of route configurations
|
|
||||||
* to be used with the SmartProxy system.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Re-export all functions from the route-helpers.ts file
|
|
||||||
export * from '../route-helpers.js';
|
|
@ -244,21 +244,36 @@ export class RouteManager extends plugins.EventEmitter {
|
|||||||
* Match an IP against a pattern
|
* Match an IP against a pattern
|
||||||
*/
|
*/
|
||||||
private matchIpPattern(pattern: string, ip: string): boolean {
|
private matchIpPattern(pattern: string, ip: string): boolean {
|
||||||
// Handle exact match
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
if (pattern === ip) {
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
|
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||||
|
|
||||||
|
// Handle exact match with normalized addresses
|
||||||
|
if (pattern === ip || normalizedPattern === normalizedIp ||
|
||||||
|
pattern === normalizedIp || normalizedPattern === ip) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
||||||
if (pattern.includes('/')) {
|
if (pattern.includes('/')) {
|
||||||
return this.matchIpCidr(pattern, ip);
|
return this.matchIpCidr(pattern, normalizedIp) ||
|
||||||
|
(normalizedPattern !== pattern && this.matchIpCidr(normalizedPattern, normalizedIp));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle glob pattern (e.g., 192.168.1.*)
|
// Handle glob pattern (e.g., 192.168.1.*)
|
||||||
if (pattern.includes('*')) {
|
if (pattern.includes('*')) {
|
||||||
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
const regex = new RegExp(`^${regexPattern}$`);
|
||||||
return regex.test(ip);
|
if (regex.test(ip) || regex.test(normalizedIp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pattern was normalized, also test with normalized pattern
|
||||||
|
if (normalizedPattern !== pattern) {
|
||||||
|
const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||||
|
const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`);
|
||||||
|
return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -274,9 +289,13 @@ export class RouteManager extends plugins.EventEmitter {
|
|||||||
const [subnet, bits] = cidr.split('/');
|
const [subnet, bits] = cidr.split('/');
|
||||||
const mask = parseInt(bits, 10);
|
const mask = parseInt(bits, 10);
|
||||||
|
|
||||||
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
|
const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet;
|
||||||
|
|
||||||
// Convert IP addresses to numeric values
|
// Convert IP addresses to numeric values
|
||||||
const ipNum = this.ipToNumber(ip);
|
const ipNum = this.ipToNumber(normalizedIp);
|
||||||
const subnetNum = this.ipToNumber(subnet);
|
const subnetNum = this.ipToNumber(normalizedSubnet);
|
||||||
|
|
||||||
// Calculate subnet mask
|
// Calculate subnet mask
|
||||||
const maskNum = ~(2 ** (32 - mask) - 1);
|
const maskNum = ~(2 ** (32 - mask) - 1);
|
||||||
@ -293,7 +312,10 @@ export class RouteManager extends plugins.EventEmitter {
|
|||||||
* Convert an IP address to a numeric value
|
* Convert an IP address to a numeric value
|
||||||
*/
|
*/
|
||||||
private ipToNumber(ip: string): number {
|
private ipToNumber(ip: string): number {
|
||||||
const parts = ip.split('.').map(part => parseInt(part, 10));
|
// Normalize IPv6-mapped IPv4 addresses
|
||||||
|
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||||
|
|
||||||
|
const parts = normalizedIp.split('.').map(part => parseInt(part, 10));
|
||||||
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user