change to route based approach

This commit is contained in:
Philipp Kunz 2025-05-10 00:01:02 +00:00
parent 552f4c246b
commit 28022ebe87
13 changed files with 3594 additions and 1167 deletions

View File

@ -1,8 +1,8 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "13.1.3", "version": "14.0.0",
"private": false, "private": false,
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication 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",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
"type": "module", "type": "module",

967
readme.md

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# SmartProxy Fully Unified Configuration Plan # SmartProxy Fully Unified Configuration Plan (Updated)
## Project Goal ## Project Goal
Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by: Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by:
@ -6,6 +6,7 @@ Redesign SmartProxy's configuration for a more elegant, unified, and comprehensi
2. Eliminating the confusion between domain configs and port forwarding 2. Eliminating the confusion between domain configs and port forwarding
3. Providing a clear, declarative API that makes the intent obvious 3. Providing a clear, declarative API that makes the intent obvious
4. Enhancing maintainability and extensibility for future features 4. Enhancing maintainability and extensibility for future features
5. Completely removing legacy code to eliminate technical debt
## Current Issues ## Current Issues
@ -132,177 +133,14 @@ interface ISmartProxyOptions {
} }
``` ```
### Example Configuration ## Revised Implementation Plan
```typescript
const proxy = new SmartProxy({
// All routing is configured in a single array
routes: [
// Basic HTTPS server with automatic certificates
{
match: {
ports: 443,
domains: ['example.com', '*.example.com']
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: 8080
},
tls: {
mode: 'terminate',
certificate: 'auto' // Use ACME
}
},
name: 'Main HTTPS Server'
},
// HTTP to HTTPS redirect
{
match: {
ports: 80,
domains: ['example.com', '*.example.com']
},
action: {
type: 'redirect',
redirect: {
to: 'https://{domain}{path}',
status: 301
}
},
name: 'HTTP to HTTPS Redirect'
},
// Admin portal with IP restriction
{
match: {
ports: 8443,
domains: 'admin.example.com'
},
action: {
type: 'forward',
target: {
host: 'admin-backend',
port: 3000
},
tls: {
mode: 'terminate',
certificate: 'auto'
},
security: {
allowedIps: ['192.168.1.*', '10.0.0.*'],
maxConnections: 10
}
},
priority: 100, // Higher priority than default rules
name: 'Admin Portal'
},
// Port range for direct forwarding
{
match: {
ports: [{ from: 10000, to: 10010 }],
// No domains = all domains or direct IP
},
action: {
type: 'forward',
target: {
host: 'backend-server',
port: 10000,
preservePort: true // Use same port number as incoming
},
tls: {
mode: 'passthrough' // Direct TCP forwarding
}
},
name: 'Dynamic Port Range'
},
// Path-based routing
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/v1/*'
},
action: {
type: 'forward',
target: {
host: 'api-v1-service',
port: 8001
},
tls: {
mode: 'terminate'
}
},
name: 'API v1 Endpoints'
},
// Load balanced backend
{
match: {
ports: 443,
domains: 'app.example.com'
},
action: {
type: 'forward',
target: {
// Round-robin load balancing
host: [
'app-server-1',
'app-server-2',
'app-server-3'
],
port: 8080
},
tls: {
mode: 'terminate'
},
advanced: {
headers: {
'X-Served-By': '{server}',
'X-Client-IP': '{clientIp}'
}
}
},
name: 'Load Balanced App Servers'
}
],
// Global defaults
defaults: {
target: {
host: 'localhost',
port: 8080
},
security: {
maxConnections: 1000,
// Global security defaults
}
},
// ACME configuration for auto certificates
acme: {
enabled: true,
email: 'admin@example.com',
production: true,
renewThresholdDays: 30
},
// Other global settings
// ...
});
```
## Implementation Plan
### Phase 1: Core Design & Interface Definition ### Phase 1: Core Design & Interface Definition
1. **Define New Core Interfaces**: 1. **Define New Core Interfaces**:
- Create `IRouteConfig` interface with `match` and `action` branches - Create `IRouteConfig` interface with `match` and `action` branches
- Define all sub-interfaces for matching and actions - Define all sub-interfaces for matching and actions
- Update `ISmartProxyOptions` to use `routes` array - Create new `ISmartProxyOptions` to use `routes` array exclusively
- Define template variable system for dynamic values - Define template variable system for dynamic values
2. **Create Helper Functions**: 2. **Create Helper Functions**:
@ -319,78 +157,56 @@ const proxy = new SmartProxy({
### Phase 2: Core Implementation ### Phase 2: Core Implementation
1. **Create RouteManager**: 1. **Create RouteManager**:
- Replaces both PortRangeManager and DomainConfigManager - Build a new RouteManager to replace both PortRangeManager and DomainConfigManager
- Handles all routing decisions in one place - Implement port and domain matching in one unified system
- Provides fast lookup from port+domain+path to action - Create efficient route lookup algorithms
- Manages server instances for different ports
2. **Update ConnectionHandler**: 2. **Implement New ConnectionHandler**:
- Simplify to work with the unified route system - Create a new ConnectionHandler built from scratch for routes
- Implement templating system for dynamic values - Implement the routing logic with the new match/action pattern
- Support more sophisticated routing rules - Support template processing for headers and other dynamic values
3. **Implement New SmartProxy Core**: 3. **Implement New SmartProxy Core**:
- Rewrite initialization to use route-based configuration - Create new SmartProxy implementation using routes exclusively
- Create network servers based on port specifications - Build network servers based on port specifications
- Manage TLS contexts and certificates - Manage TLS contexts and certificates
### Phase 3: Feature Implementation ### Phase 3: Legacy Code Removal
1. **Path-Based Routing**: 1. **Identify Legacy Components**:
- Implement HTTP path matching - Create an inventory of all files and components to be removed
- Add wildcard and regex support for paths - Document dependencies between legacy components
- Create route differentiation based on HTTP method - Create a removal plan that minimizes disruption
2. **Enhanced Security**: 2. **Remove Legacy Components**:
- Implement per-route security rules - Remove PortRangeManager and related code
- Add authentication methods (basic, digest, etc.) - Remove DomainConfigManager and related code
- Support for IP-based access control - Remove old ConnectionHandler implementation
- Remove other legacy components
3. **TLS Management**: 3. **Clean Interface Adaptations**:
- Support multiple certificate types (auto, manual, wildcard) - Remove all legacy interfaces and types
- Implement certificate selection based on SNI - Update type exports to only expose route-based interfaces
- Support different TLS modes per route - Remove any adapter or backward compatibility code
4. **Metrics & Monitoring**: ### Phase 4: Updated Documentation & Examples
- Per-route statistics
- Named route tracking for better visibility
- Tag-based grouping of metrics
### Phase 4: Backward Compatibility
1. **Legacy Adapter Layer**:
- Convert old configuration format to route-based format
- Map fromPort/toPort/domainConfigs to unified routes
- Preserve all functionality during migration
2. **API Compatibility**:
- Support both old and new configuration methods
- Add deprecation warnings when using legacy properties
- Provide migration utilities
3. **Documentation**:
- Clear migration guide for existing users
- Examples mapping old config to new config
- Timeline for deprecation
### Phase 5: Documentation & Examples
1. **Update Core Documentation**: 1. **Update Core Documentation**:
- Rewrite README.md with a focus on route-based configuration - Rewrite README.md with a focus on route-based configuration exclusively
- Create interface reference documentation - Create interface reference documentation
- Document all template variables - Document all template variables
2. **Create Example Library**: 2. **Create Example Library**:
- Common configuration patterns - Common configuration patterns using the new API
- Complex use cases for advanced features - Complex use cases for advanced features
- Infrastructure-as-code examples - Infrastructure-as-code examples
3. **Add Validation Tooling**: 3. **Add Validation Tools**:
- Configuration validator to check for issues - Configuration validator to check for issues
- Schema definitions for IDE autocomplete - Schema definitions for IDE autocomplete
- Runtime validation helpers - Runtime validation helpers
### Phase 6: Testing ### Phase 5: Testing
1. **Unit Tests**: 1. **Unit Tests**:
- Test route matching logic - Test route matching logic
@ -398,378 +214,103 @@ const proxy = new SmartProxy({
- Test template processing - Test template processing
2. **Integration Tests**: 2. **Integration Tests**:
- Verify full proxy flows - Verify full proxy flows with the new system
- Test complex routing scenarios - Test complex routing scenarios
- Check backward compatibility - Ensure all features work as expected
3. **Performance Testing**: 3. **Performance Testing**:
- Benchmark routing performance - Benchmark routing performance
- Evaluate memory usage - Evaluate memory usage
- Test with large numbers of routes - Test with large numbers of routes
## Benefits of the New Approach
1. **Truly Unified Configuration**:
- One "source of truth" for all routing
- Entire routing flow visible in a single configuration
- No overlapping or conflicting configuration systems
2. **Declarative Intent**:
- Configuration clearly states what to match and what action to take
- Metadata provides context and documentation inline
- Easy to understand the purpose of each route
3. **Advanced Routing Capabilities**:
- Path-based routing with pattern matching
- Client IP-based conditional routing
- Fine-grained security controls
4. **Composable and Extensible**:
- Each route is a self-contained unit of configuration
- Routes can be grouped by tags or priority
- New match criteria or actions can be added without breaking changes
5. **Better Developer Experience**:
- Clear, consistent configuration pattern
- Helper functions for common scenarios
- Better error messages and validation
## Example Use Cases
### 1. Complete Reverse Proxy with Auto SSL
```typescript
const proxy = new SmartProxy({
routes: [
// HTTPS server for all domains
{
match: { ports: 443 },
action: {
type: 'forward',
target: { host: 'localhost', port: 8080 },
tls: { mode: 'terminate', certificate: 'auto' }
}
},
// HTTP to HTTPS redirect
{
match: { ports: 80 },
action: {
type: 'redirect',
redirect: { to: 'https://{domain}{path}', status: 301 }
}
}
],
acme: {
enabled: true,
email: 'admin@example.com',
production: true
}
});
```
### 2. Microservices API Gateway
```typescript
const apiGateway = new SmartProxy({
routes: [
// Users API
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/users/*'
},
action: {
type: 'forward',
target: { host: 'users-service', port: 8001 },
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'Users Service'
},
// Products API
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/products/*'
},
action: {
type: 'forward',
target: { host: 'products-service', port: 8002 },
tls: { mode: 'terminate', certificate: 'auto' }
},
name: 'Products Service'
},
// Orders API with authentication
{
match: {
ports: 443,
domains: 'api.example.com',
path: '/orders/*'
},
action: {
type: 'forward',
target: { host: 'orders-service', port: 8003 },
tls: { mode: 'terminate', certificate: 'auto' },
security: {
authentication: {
type: 'basic',
users: { 'admin': 'password' }
}
}
},
name: 'Orders Service (Protected)'
}
],
acme: {
enabled: true,
email: 'admin@example.com'
}
});
```
### 3. Multi-Environment Setup
```typescript
const environments = {
production: {
target: { host: 'prod-backend', port: 8080 },
security: { maxConnections: 1000 }
},
staging: {
target: { host: 'staging-backend', port: 8080 },
security: {
allowedIps: ['10.0.0.*', '192.168.1.*'],
maxConnections: 100
}
},
development: {
target: { host: 'localhost', port: 3000 },
security: {
allowedIps: ['127.0.0.1'],
maxConnections: 10
}
}
};
// Select environment based on hostname
const proxy = new SmartProxy({
routes: [
// Production environment
{
match: {
ports: [80, 443],
domains: ['app.example.com', 'www.example.com']
},
action: {
type: 'forward',
target: environments.production.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.production.security
},
name: 'Production Environment'
},
// Staging environment
{
match: {
ports: [80, 443],
domains: 'staging.example.com'
},
action: {
type: 'forward',
target: environments.staging.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.staging.security
},
name: 'Staging Environment'
},
// Development environment
{
match: {
ports: [80, 443],
domains: 'dev.example.com'
},
action: {
type: 'forward',
target: environments.development.target,
tls: { mode: 'terminate', certificate: 'auto' },
security: environments.development.security
},
name: 'Development Environment'
}
],
acme: { enabled: true, email: 'admin@example.com' }
});
```
## Implementation Strategy ## Implementation Strategy
### Code Organization ### Code Organization
1. **New Files**: 1. **New Files**:
- `route-manager.ts` (core routing engine) - `route-config.ts` - Core route interfaces
- `route-types.ts` (interface definitions) - `route-manager.ts` - Route matching and management
- `route-helpers.ts` (helper functions) - `route-connection-handler.ts` - Connection handling with routes
- `route-matcher.ts` (matching logic) - `route-smart-proxy.ts` - Main SmartProxy implementation
- `template-engine.ts` (for variable substitution) - `template-engine.ts` - For variable substitution
2. **Modified Files**: 2. **File Removal**:
- `smart-proxy.ts` (update to use route-based configuration) - Remove `port-range-manager.ts`
- `connection-handler.ts` (simplify using route-based approach) - Remove `domain-config-manager.ts`
- Replace `port-range-manager.ts` and `domain-config-manager.ts` - Remove legacy interfaces and adapter code
- Remove backward compatibility shims
### Backward Compatibility ### Transition Strategy
The backward compatibility layer will convert the legacy configuration to the new format: 1. **Breaking Change Approach**:
- This will be a major version update with breaking changes
- No backward compatibility will be maintained
- Clear migration documentation will guide users to the new API
```typescript 2. **Package Structure**:
function convertLegacyConfig(legacy: ILegacySmartProxyOptions): ISmartProxyOptions { - `@push.rocks/smartproxy` package will be updated to v14.0.0
const routes: IRouteConfig[] = []; - Legacy code fully removed, only route-based API exposed
- Support documentation provided for migration
// Convert main port configuration
if (legacy.fromPort) {
// Add main listener for fromPort
routes.push({
match: { ports: legacy.fromPort },
action: {
type: 'forward',
target: { host: legacy.targetIP || 'localhost', port: legacy.toPort },
tls: { mode: legacy.sniEnabled ? 'passthrough' : 'terminate' }
},
name: 'Main Listener (Legacy)'
});
// If ACME is enabled, add HTTP listener for challenges
if (legacy.acme?.enabled) {
routes.push({
match: { ports: 80 },
action: {
type: 'forward',
target: { host: 'localhost', port: 80 },
// Special flag for ACME handler
acmeEnabled: true
},
name: 'ACME Challenge Handler (Legacy)'
});
}
}
// Convert domain configs
if (legacy.domainConfigs) {
for (const domainConfig of legacy.domainConfigs) {
const { domains, forwarding } = domainConfig;
// Determine action based on forwarding type
let action: Partial<IRouteAction> = {
type: 'forward',
target: {
host: forwarding.target.host,
port: forwarding.target.port
}
};
// Set TLS mode based on forwarding type
switch (forwarding.type) {
case 'http-only':
// No TLS
break;
case 'https-passthrough':
action.tls = { mode: 'passthrough' };
break;
case 'https-terminate-to-http':
action.tls = {
mode: 'terminate',
certificate: forwarding.https?.customCert || 'auto'
};
break;
case 'https-terminate-to-https':
action.tls = {
mode: 'terminate-and-reencrypt',
certificate: forwarding.https?.customCert || 'auto'
};
break;
}
// Security settings
if (forwarding.security) {
action.security = forwarding.security;
}
// Add HTTP redirect if needed
if (forwarding.http?.redirectToHttps) {
routes.push({
match: { ports: 80, domains },
action: {
type: 'redirect',
redirect: { to: 'https://{domain}{path}', status: 301 }
},
name: `HTTP Redirect for ${domains.join(', ')} (Legacy)`
});
}
// Add main route
routes.push({
match: {
ports: forwarding.type.startsWith('https') ? 443 : 80,
domains
},
action: action as IRouteAction,
name: `Route for ${domains.join(', ')} (Legacy)`
});
// Add port ranges if specified
if (forwarding.advanced?.portRanges) {
for (const range of forwarding.advanced.portRanges) {
routes.push({
match: {
ports: { from: range.from, to: range.to },
domains
},
action: action as IRouteAction,
name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')} (Legacy)`
});
}
}
}
}
// Global port ranges
if (legacy.globalPortRanges) {
for (const range of legacy.globalPortRanges) {
routes.push({
match: { ports: { from: range.from, to: range.to } },
action: {
type: 'forward',
target: {
host: legacy.targetIP || 'localhost',
port: legacy.forwardAllGlobalRanges ? 0 : legacy.toPort,
preservePort: !!legacy.forwardAllGlobalRanges
},
tls: { mode: 'passthrough' }
},
name: `Global Port Range ${range.from}-${range.to} (Legacy)`
});
}
}
return {
routes,
defaults: {
target: {
host: legacy.targetIP || 'localhost',
port: legacy.toPort
}
},
acme: legacy.acme
};
}
```
## Success Criteria 3. **Migration Documentation**:
- Provide a migration guide with examples
- Show equivalent route configurations for common legacy patterns
- Offer code transformation helpers for complex setups
- All existing functionality works with the new route-based configuration ## Benefits of the Clean Approach
- Performance is equal or better than the current implementation
- Configuration is more intuitive and easier to understand 1. **Reduced Complexity**:
- New features can be added without breaking existing code - No overlapping or conflicting configuration systems
- Code is more maintainable with clear separation of concerns - No dual maintenance of backward compatibility code
- Migration from old configuration to new is straightforward - Simplified internal architecture
2. **Cleaner Code Base**:
- Removal of technical debt
- Better separation of concerns
- More maintainable codebase
3. **Better User Experience**:
- Consistent, predictable API
- No confusing overlapping options
- Clear documentation of one approach, not two
4. **Future-Proof Design**:
- Easier to extend with new features
- Better performance without legacy overhead
- Cleaner foundation for future enhancements
## Migration Support
While we're removing backward compatibility from the codebase, we'll provide extensive migration support:
1. **Migration Guide**:
- Detailed documentation on moving from legacy to route-based config
- Pattern-matching examples for all common use cases
- Troubleshooting guide for common migration issues
2. **Conversion Tool**:
- Provide a standalone one-time conversion tool
- Takes legacy configuration and outputs route-based equivalents
- Will not be included in the main package to avoid bloat
3. **Version Policy**:
- Maintain the legacy version (13.x) for security updates
- Make the route-based version a clear major version change (14.0.0)
- Clearly communicate the breaking changes
## Timeline and Versioning
1. **Development**:
- Develop route-based implementation in a separate branch
- Complete full test coverage of new implementation
- Ensure documentation is complete
2. **Release**:
- Release as version 14.0.0
- Clearly mark as breaking change
- Provide migration guide at release time
3. **Support**:
- Offer extended support for migration questions
- Consider maintaining security updates for v13.x for 6 months
- Focus active development on route-based version only

181
test/test.route-config.ts Normal file
View File

@ -0,0 +1,181 @@
/**
* Tests for the new route-based configuration system
*/
import { expect, tap } from '@push.rocks/tapbundle';
// Import from core modules
import {
SmartProxy,
createHttpRoute,
createHttpsRoute,
createPassthroughRoute,
createRedirectRoute,
createHttpToHttpsRedirect,
createHttpsServer,
createLoadBalancerRoute
} from '../ts/proxies/smart-proxy/index.js';
// Import test helpers
import { getCertificate } from './helpers/certificates.js';
tap.test('Routes: Should create basic HTTP route', async () => {
// Create a simple HTTP route
const httpRoute = createHttpRoute({
ports: 8080,
domains: 'example.com',
target: {
host: 'localhost',
port: 3000
},
name: 'Basic HTTP Route'
});
// Validate the route configuration
expect(httpRoute.match.ports).to.equal(8080);
expect(httpRoute.match.domains).to.equal('example.com');
expect(httpRoute.action.type).to.equal('forward');
expect(httpRoute.action.target?.host).to.equal('localhost');
expect(httpRoute.action.target?.port).to.equal(3000);
expect(httpRoute.name).to.equal('Basic HTTP Route');
});
tap.test('Routes: Should create HTTPS route with TLS termination', async () => {
// Create an HTTPS route with TLS termination
const httpsRoute = createHttpsRoute({
domains: 'secure.example.com',
target: {
host: 'localhost',
port: 8080
},
certificate: 'auto',
name: 'HTTPS Route'
});
// Validate the route configuration
expect(httpsRoute.match.ports).to.equal(443); // Default HTTPS port
expect(httpsRoute.match.domains).to.equal('secure.example.com');
expect(httpsRoute.action.type).to.equal('forward');
expect(httpsRoute.action.tls?.mode).to.equal('terminate');
expect(httpsRoute.action.tls?.certificate).to.equal('auto');
expect(httpsRoute.action.target?.host).to.equal('localhost');
expect(httpsRoute.action.target?.port).to.equal(8080);
expect(httpsRoute.name).to.equal('HTTPS Route');
});
tap.test('Routes: Should create HTTP to HTTPS redirect', async () => {
// Create an HTTP to HTTPS redirect
const redirectRoute = createHttpToHttpsRedirect({
domains: 'example.com',
statusCode: 301
});
// Validate the route configuration
expect(redirectRoute.match.ports).to.equal(80);
expect(redirectRoute.match.domains).to.equal('example.com');
expect(redirectRoute.action.type).to.equal('redirect');
expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}');
expect(redirectRoute.action.redirect?.status).to.equal(301);
});
tap.test('Routes: Should create complete HTTPS server with redirects', async () => {
// Create a complete HTTPS server setup
const routes = createHttpsServer({
domains: 'example.com',
target: {
host: 'localhost',
port: 8080
},
certificate: 'auto',
addHttpRedirect: true
});
// Validate that we got two routes (HTTPS route and HTTP redirect)
expect(routes.length).to.equal(2);
// Validate HTTPS route
const httpsRoute = routes[0];
expect(httpsRoute.match.ports).to.equal(443);
expect(httpsRoute.match.domains).to.equal('example.com');
expect(httpsRoute.action.type).to.equal('forward');
expect(httpsRoute.action.tls?.mode).to.equal('terminate');
// Validate HTTP redirect route
const redirectRoute = routes[1];
expect(redirectRoute.match.ports).to.equal(80);
expect(redirectRoute.action.type).to.equal('redirect');
expect(redirectRoute.action.redirect?.to).to.equal('https://{domain}{path}');
});
tap.test('Routes: Should create load balancer route', async () => {
// Create a load balancer route
const lbRoute = createLoadBalancerRoute({
domains: 'app.example.com',
targets: ['10.0.0.1', '10.0.0.2', '10.0.0.3'],
targetPort: 8080,
tlsMode: 'terminate',
certificate: 'auto',
name: 'Load Balanced Route'
});
// Validate the route configuration
expect(lbRoute.match.domains).to.equal('app.example.com');
expect(lbRoute.action.type).to.equal('forward');
expect(Array.isArray(lbRoute.action.target?.host)).to.equal(true);
expect((lbRoute.action.target?.host as string[]).length).to.equal(3);
expect((lbRoute.action.target?.host as string[])[0]).to.equal('10.0.0.1');
expect(lbRoute.action.target?.port).to.equal(8080);
expect(lbRoute.action.tls?.mode).to.equal('terminate');
});
tap.test('SmartProxy: Should create instance with route-based config', async () => {
// Create TLS certificates for testing
const cert = await getCertificate();
// Create a SmartProxy instance with route-based configuration
const proxy = new SmartProxy({
routes: [
createHttpRoute({
ports: 8080,
domains: 'example.com',
target: {
host: 'localhost',
port: 3000
},
name: 'HTTP Route'
}),
createHttpsRoute({
domains: 'secure.example.com',
target: {
host: 'localhost',
port: 8443
},
certificate: {
key: cert.key,
cert: cert.cert
},
name: 'HTTPS Route'
})
],
defaults: {
target: {
host: 'localhost',
port: 8080
},
security: {
allowedIPs: ['127.0.0.1', '192.168.0.*'],
maxConnections: 100
}
},
// Additional settings
initialDataTimeout: 10000,
inactivityTimeout: 300000,
enableDetailedLogging: true
});
// Simply verify the instance was created successfully
expect(proxy).to.be.an('object');
expect(proxy.start).to.be.a('function');
expect(proxy.stop).to.be.a('function');
});
export default tap.start();

View File

@ -66,13 +66,25 @@ function createTestClient(port: number, data: string): Promise<string> {
tap.test('setup port proxy test environment', async () => { tap.test('setup port proxy test environment', async () => {
testServer = await createTestServer(TEST_SERVER_PORT); testServer = await createTestServer(TEST_SERVER_PORT);
smartProxy = new SmartProxy({ smartProxy = new SmartProxy({
fromPort: PROXY_PORT, routes: [
toPort: TEST_SERVER_PORT, {
targetIP: 'localhost', match: {
domainConfigs: [], ports: PROXY_PORT
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1'], action: {
globalPortRanges: [] type: 'forward',
target: {
host: 'localhost',
port: TEST_SERVER_PORT
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1']
}
}
}); });
allProxies.push(smartProxy); // Track this proxy allProxies.push(smartProxy); // Track this proxy
}); });
@ -92,13 +104,25 @@ tap.test('should forward TCP connections and data to localhost', async () => {
// Test proxy with a custom target host. // Test proxy with a custom target host.
tap.test('should forward TCP connections to custom host', async () => { tap.test('should forward TCP connections to custom host', async () => {
const customHostProxy = new SmartProxy({ const customHostProxy = new SmartProxy({
fromPort: PROXY_PORT + 1, routes: [
toPort: TEST_SERVER_PORT, {
targetIP: '127.0.0.1', match: {
domainConfigs: [], ports: PROXY_PORT + 1
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1'], action: {
globalPortRanges: [] type: 'forward',
target: {
host: '127.0.0.1',
port: TEST_SERVER_PORT
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1']
}
}
}); });
allProxies.push(customHostProxy); // Track this proxy allProxies.push(customHostProxy); // Track this proxy
@ -125,14 +149,25 @@ tap.test('should forward connections to custom IP', async () => {
// We're simulating routing to a different IP by using a different port // We're simulating routing to a different IP by using a different port
// This tests the core functionality without requiring multiple IPs // This tests the core functionality without requiring multiple IPs
const domainProxy = new SmartProxy({ const domainProxy = new SmartProxy({
fromPort: forcedProxyPort, // 4003 - Listen on this port routes: [
toPort: targetServerPort, // 4200 - Forward to this port {
targetIP: '127.0.0.1', // Always use localhost (works in Docker) match: {
domainConfigs: [], // No domain configs to confuse things ports: forcedProxyPort
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], // Allow localhost action: {
// We'll test the functionality WITHOUT port ranges this time type: 'forward',
globalPortRanges: [] target: {
host: '127.0.0.1',
port: targetServerPort
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
}
}
}); });
allProxies.push(domainProxy); // Track this proxy allProxies.push(domainProxy); // Track this proxy
@ -208,22 +243,46 @@ tap.test('should stop port proxy', async () => {
tap.test('should support optional source IP preservation in chained proxies', async () => { tap.test('should support optional source IP preservation in chained proxies', async () => {
// Chained proxies without IP preservation. // Chained proxies without IP preservation.
const firstProxyDefault = new SmartProxy({ const firstProxyDefault = new SmartProxy({
fromPort: PROXY_PORT + 4, routes: [
toPort: PROXY_PORT + 5, {
targetIP: 'localhost', match: {
domainConfigs: [], ports: PROXY_PORT + 4
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], action: {
globalPortRanges: [] type: 'forward',
target: {
host: 'localhost',
port: PROXY_PORT + 5
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
}
}
}); });
const secondProxyDefault = new SmartProxy({ const secondProxyDefault = new SmartProxy({
fromPort: PROXY_PORT + 5, routes: [
toPort: TEST_SERVER_PORT, {
targetIP: 'localhost', match: {
domainConfigs: [], ports: PROXY_PORT + 5
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1'], action: {
globalPortRanges: [] type: 'forward',
target: {
host: 'localhost',
port: TEST_SERVER_PORT
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
}
}
}); });
allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies allProxies.push(firstProxyDefault, secondProxyDefault); // Track these proxies
@ -243,24 +302,50 @@ tap.test('should support optional source IP preservation in chained proxies', as
// Chained proxies with IP preservation. // Chained proxies with IP preservation.
const firstProxyPreserved = new SmartProxy({ const firstProxyPreserved = new SmartProxy({
fromPort: PROXY_PORT + 6, routes: [
toPort: PROXY_PORT + 7, {
targetIP: 'localhost', match: {
domainConfigs: [], ports: PROXY_PORT + 6
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1'], action: {
preserveSourceIP: true, type: 'forward',
globalPortRanges: [] target: {
host: 'localhost',
port: PROXY_PORT + 7
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1']
},
preserveSourceIP: true
},
preserveSourceIP: true
}); });
const secondProxyPreserved = new SmartProxy({ const secondProxyPreserved = new SmartProxy({
fromPort: PROXY_PORT + 7, routes: [
toPort: TEST_SERVER_PORT, {
targetIP: 'localhost', match: {
domainConfigs: [], ports: PROXY_PORT + 7
sniEnabled: false, },
defaultAllowedIPs: ['127.0.0.1'], action: {
preserveSourceIP: true, type: 'forward',
globalPortRanges: [] target: {
host: 'localhost',
port: TEST_SERVER_PORT
}
}
}
],
defaults: {
security: {
allowedIPs: ['127.0.0.1']
},
preserveSourceIP: true
},
preserveSourceIP: true
}); });
allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies allProxies.push(firstProxyPreserved, secondProxyPreserved); // Track these proxies

View File

@ -1,5 +1,7 @@
/** /**
* SmartProxy implementation * SmartProxy implementation
*
* Version 14.0.0: Unified Route-Based Configuration API
*/ */
// Re-export models // Re-export models
export * from './models/index.js'; export * from './models/index.js';
@ -7,12 +9,26 @@ export * from './models/index.js';
// Export the main SmartProxy class // Export the main SmartProxy class
export { SmartProxy } from './smart-proxy.js'; export { SmartProxy } from './smart-proxy.js';
// Export supporting classes // Export core supporting classes
export { ConnectionManager } from './connection-manager.js'; export { ConnectionManager } from './connection-manager.js';
export { SecurityManager } from './security-manager.js'; export { SecurityManager } from './security-manager.js';
export { DomainConfigManager } from './domain-config-manager.js';
export { TimeoutManager } from './timeout-manager.js'; export { TimeoutManager } from './timeout-manager.js';
export { TlsManager } from './tls-manager.js'; export { TlsManager } from './tls-manager.js';
export { NetworkProxyBridge } from './network-proxy-bridge.js'; export { NetworkProxyBridge } from './network-proxy-bridge.js';
export { PortRangeManager } from './port-range-manager.js';
export { ConnectionHandler } from './connection-handler.js'; // Export route-based components
export { RouteManager } from './route-manager.js';
export { RouteConnectionHandler } from './route-connection-handler.js';
// Export route helpers for configuration
export {
createRoute,
createHttpRoute,
createHttpsRoute,
createPassthroughRoute,
createRedirectRoute,
createHttpToHttpsRedirect,
createBlockRoute,
createLoadBalancerRoute,
createHttpsServer
} from './route-helpers.js';

View File

@ -2,3 +2,7 @@
* SmartProxy models * SmartProxy models
*/ */
export * from './interfaces.js'; export * from './interfaces.js';
export * from './route-types.js';
// Re-export IRoutedSmartProxyOptions explicitly to avoid ambiguity
export type { ISmartProxyOptions as IRoutedSmartProxyOptions } from './interfaces.js';

View File

@ -1,5 +1,7 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../../plugins.js';
import type { IForwardConfig } from '../../../forwarding/config/forwarding-types.js'; import type { IAcmeOptions } from '../../../certificate/models/certificate-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
@ -7,27 +9,102 @@ import type { IForwardConfig } from '../../../forwarding/config/forwarding-types
export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
/** /**
* Domain configuration with forwarding configuration * Alias for backward compatibility with code that uses IRoutedSmartProxyOptions
*/
export type IRoutedSmartProxyOptions = ISmartProxyOptions;
/**
* Legacy domain configuration interface for backward compatibility
*/ */
export interface IDomainConfig { export interface IDomainConfig {
domains: string[]; // Glob patterns for domain(s) domains: string[];
forwarding: IForwardConfig; // Unified forwarding configuration forwarding: {
type: TForwardingType;
target: {
host: string | string[];
port: number;
};
acme?: {
enabled?: boolean;
maintenance?: boolean;
production?: boolean;
forwardChallenges?: {
host: string;
port: number;
useTls?: boolean;
};
};
http?: {
enabled?: boolean;
redirectToHttps?: boolean;
headers?: Record<string, string>;
};
https?: {
customCert?: {
key: string;
cert: string;
};
forwardSni?: boolean;
};
security?: {
allowedIps?: string[];
blockedIps?: string[];
maxConnections?: number;
};
advanced?: {
portRanges?: Array<{ from: number; to: number }>;
networkProxyPort?: number;
keepAlive?: boolean;
timeout?: number;
headers?: Record<string, string>;
};
};
} }
/** /**
* Configuration options for the SmartProxy * Helper functions for type checking - now always assume route-based config
*/
export function isLegacyOptions(options: any): boolean {
return false; // No longer supporting legacy options
}
export function isRoutedOptions(options: any): boolean {
return true; // Always assume routed options
}
/**
* SmartProxy configuration options
*/ */
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
export interface ISmartProxyOptions { export interface ISmartProxyOptions {
fromPort: number; // The unified configuration array (required)
toPort: number; routes: IRouteConfig[];
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
domainConfigs: IDomainConfig[]; // Legacy options for backward compatibility
fromPort?: number;
toPort?: number;
sniEnabled?: boolean; sniEnabled?: boolean;
domainConfigs?: IDomainConfig[];
targetIP?: string;
defaultAllowedIPs?: string[]; defaultAllowedIPs?: string[];
defaultBlockedIPs?: string[]; defaultBlockedIPs?: string[];
globalPortRanges?: Array<{ from: number; to: number }>;
forwardAllGlobalRanges?: boolean;
preserveSourceIP?: boolean; preserveSourceIP?: boolean;
// Global/default settings
defaults?: {
target?: {
host: string; // Default host to use when not specified in routes
port: number; // Default port to use when not specified in routes
};
security?: {
allowedIPs?: string[]; // Default allowed IPs
blockedIPs?: string[]; // Default blocked IPs
maxConnections?: number; // Default max connections
};
preserveSourceIP?: boolean; // Default source IP preservation
};
// TLS options // TLS options
pfx?: Buffer; pfx?: Buffer;
key?: string | Buffer | Array<Buffer | string>; key?: string | Buffer | Array<Buffer | string>;
@ -50,8 +127,6 @@ export interface ISmartProxyOptions {
inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h) inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
// Socket optimization settings // Socket optimization settings
noDelay?: boolean; // Disable Nagle's algorithm (default: true) noDelay?: boolean; // Disable Nagle's algorithm (default: true)
@ -108,6 +183,9 @@ export interface IConnectionRecord {
pendingData: Buffer[]; // Buffer to hold data during connection setup pendingData: Buffer[]; // Buffer to hold data during connection setup
pendingDataSize: number; // Track total size of pending data pendingDataSize: number; // Track total size of pending data
// Legacy property for backward compatibility
domainConfig?: IDomainConfig;
// Enhanced tracking fields // Enhanced tracking fields
bytesReceived: number; // Total bytes received bytesReceived: number; // Total bytes received
bytesSent: number; // Total bytes sent bytesSent: number; // Total bytes sent
@ -116,7 +194,7 @@ export interface IConnectionRecord {
isTLS: boolean; // Whether this connection is a TLS connection isTLS: boolean; // Whether this connection is a TLS connection
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
hasReceivedInitialData: boolean; // Whether initial data has been received hasReceivedInitialData: boolean; // Whether initial data has been received
domainConfig?: IDomainConfig; // Associated domain config for this connection routeConfig?: IRouteConfig; // Associated route config for this connection
// Keep-alive tracking // Keep-alive tracking
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection

View File

@ -0,0 +1,184 @@
import * as plugins from '../../../plugins.js';
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
/**
* Supported action types for route configurations
*/
export type TRouteActionType = 'forward' | 'redirect' | 'block';
/**
* TLS handling modes for route configurations
*/
export type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
/**
* Port range specification format
*/
export type TPortRange = number | number[] | Array<{ from: number; to: number }>;
/**
* Route match criteria for incoming requests
*/
export interface IRouteMatch {
// Listen on these ports (required)
ports: TPortRange;
// Optional domain patterns to match (default: all domains)
domains?: string | string[];
// Advanced matching criteria
path?: string; // Match specific paths
clientIp?: string[]; // Match specific client IPs
tlsVersion?: string[]; // Match specific TLS versions
}
/**
* Target configuration for forwarding
*/
export interface IRouteTarget {
host: string | string[]; // Support single host or round-robin
port: number;
preservePort?: boolean; // Use incoming port as target port
}
/**
* TLS configuration for route actions
*/
export interface IRouteTls {
mode: TTlsMode;
certificate?: 'auto' | { // Auto = use ACME
key: string;
cert: string;
};
}
/**
* Redirect configuration for route actions
*/
export interface IRouteRedirect {
to: string; // URL or template with {domain}, {port}, etc.
status: 301 | 302 | 307 | 308;
}
/**
* Security options for route actions
*/
export interface IRouteSecurity {
allowedIps?: string[];
blockedIps?: string[];
maxConnections?: number;
authentication?: {
type: 'basic' | 'digest' | 'oauth';
// Auth-specific options would go here
};
}
/**
* Advanced options for route actions
*/
export interface IRouteAdvanced {
timeout?: number;
headers?: Record<string, string>;
keepAlive?: boolean;
// Additional advanced options would go here
}
/**
* Action configuration for route handling
*/
export interface IRouteAction {
// Basic routing
type: TRouteActionType;
// Target for forwarding
target?: IRouteTarget;
// TLS handling
tls?: IRouteTls;
// For redirects
redirect?: IRouteRedirect;
// Security options
security?: IRouteSecurity;
// Advanced options
advanced?: IRouteAdvanced;
}
/**
* The core unified configuration interface
*/
export interface IRouteConfig {
// What to match
match: IRouteMatch;
// What to do with matched traffic
action: IRouteAction;
// Optional metadata
name?: string; // Human-readable name for this route
description?: string; // Description of the route's purpose
priority?: number; // Controls matching order (higher = matched first)
tags?: string[]; // Arbitrary tags for categorization
}
/**
* Unified SmartProxy options with routes-based configuration
*/
export interface IRoutedSmartProxyOptions {
// The unified configuration array (required)
routes: IRouteConfig[];
// Global/default settings
defaults?: {
target?: {
host: string;
port: number;
};
security?: IRouteSecurity;
tls?: IRouteTls;
// ...other defaults
};
// Other global settings remain (acme, etc.)
acme?: IAcmeOptions;
// Connection timeouts and other global settings
initialDataTimeout?: number;
socketTimeout?: number;
inactivityCheckInterval?: number;
maxConnectionLifetime?: number;
inactivityTimeout?: number;
gracefulShutdownTimeout?: number;
// Socket optimization settings
noDelay?: boolean;
keepAlive?: boolean;
keepAliveInitialDelay?: number;
maxPendingDataSize?: number;
// Enhanced features
disableInactivityCheck?: boolean;
enableKeepAliveProbes?: boolean;
enableDetailedLogging?: boolean;
enableTlsDebugLogging?: boolean;
enableRandomizedTimeouts?: boolean;
allowSessionTicket?: boolean;
// Rate limiting and security
maxConnectionsPerIP?: number;
connectionRateLimitPerMinute?: number;
// Enhanced keep-alive settings
keepAliveTreatment?: 'standard' | 'extended' | 'immortal';
keepAliveInactivityMultiplier?: number;
extendedKeepAliveLifetime?: number;
/**
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
* or a static certificate object for immediate provisioning.
*/
certProvisionFunction?: (domain: string) => Promise<any>;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
import type {
IRouteConfig,
IRouteMatch,
IRouteAction,
IRouteTarget,
IRouteTls,
IRouteRedirect,
IRouteSecurity,
IRouteAdvanced
} 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;
}

View File

@ -0,0 +1,587 @@
import * as plugins from '../../plugins.js';
import type {
IRouteConfig,
IRouteMatch,
IRouteAction,
TPortRange
} from './models/route-types.js';
import type {
ISmartProxyOptions,
IRoutedSmartProxyOptions,
IDomainConfig
} from './models/interfaces.js';
import {
isRoutedOptions,
isLegacyOptions
} from './models/interfaces.js';
/**
* Result of route matching
*/
export interface IRouteMatchResult {
route: IRouteConfig;
// Additional match parameters (path, query, etc.)
params?: Record<string, string>;
}
/**
* The RouteManager handles all routing decisions based on connections and attributes
*/
export class RouteManager extends plugins.EventEmitter {
private routes: IRouteConfig[] = [];
private portMap: Map<number, IRouteConfig[]> = new Map();
private options: IRoutedSmartProxyOptions;
constructor(options: ISmartProxyOptions) {
super();
// We no longer support legacy options, always use provided options
this.options = options;
// Initialize routes from either source
this.updateRoutes(this.options.routes);
}
/**
* Update routes with new configuration
*/
public updateRoutes(routes: IRouteConfig[] = []): void {
// Sort routes by priority (higher first)
this.routes = [...(routes || [])].sort((a, b) => {
const priorityA = a.priority ?? 0;
const priorityB = b.priority ?? 0;
return priorityB - priorityA;
});
// Rebuild port mapping for fast lookups
this.rebuildPortMap();
}
/**
* Rebuild the port mapping for fast lookups
*/
private rebuildPortMap(): void {
this.portMap.clear();
for (const route of this.routes) {
const ports = this.expandPortRange(route.match.ports);
for (const port of ports) {
if (!this.portMap.has(port)) {
this.portMap.set(port, []);
}
this.portMap.get(port)!.push(route);
}
}
}
/**
* Expand a port range specification into an array of individual ports
*/
private expandPortRange(portRange: TPortRange): number[] {
if (typeof portRange === 'number') {
return [portRange];
}
if (Array.isArray(portRange)) {
// Handle array of port objects or numbers
return portRange.flatMap(item => {
if (typeof item === 'number') {
return [item];
} else if (typeof item === 'object' && 'from' in item && 'to' in item) {
// Handle port range object
const ports: number[] = [];
for (let p = item.from; p <= item.to; p++) {
ports.push(p);
}
return ports;
}
return [];
});
}
return [];
}
/**
* Get all ports that should be listened on
*/
public getListeningPorts(): number[] {
return Array.from(this.portMap.keys());
}
/**
* Get all routes for a given port
*/
public getRoutesForPort(port: number): IRouteConfig[] {
return this.portMap.get(port) || [];
}
/**
* Test if a pattern matches a domain using glob matching
*/
private matchDomain(pattern: string, domain: string): boolean {
// Convert glob pattern to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*'); // Convert * to .*
const regex = new RegExp(`^${regexPattern}$`, 'i');
return regex.test(domain);
}
/**
* Match a domain against all patterns in a route
*/
private matchRouteDomain(route: IRouteConfig, domain: string): boolean {
if (!route.match.domains) {
// If no domains specified, match all domains
return true;
}
const patterns = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
return patterns.some(pattern => this.matchDomain(pattern, domain));
}
/**
* Check if a client IP is allowed by a route's security settings
*/
private isClientIpAllowed(route: IRouteConfig, clientIp: string): boolean {
const security = route.action.security;
if (!security) {
return true; // No security settings means allowed
}
// Check blocked IPs first
if (security.blockedIps && security.blockedIps.length > 0) {
for (const pattern of security.blockedIps) {
if (this.matchIpPattern(pattern, clientIp)) {
return false; // IP is blocked
}
}
}
// If there are allowed IPs, check them
if (security.allowedIps && security.allowedIps.length > 0) {
for (const pattern of security.allowedIps) {
if (this.matchIpPattern(pattern, clientIp)) {
return true; // IP is allowed
}
}
return false; // IP not in allowed list
}
// No allowed IPs specified, so IP is allowed
return true;
}
/**
* Match an IP against a pattern
*/
private matchIpPattern(pattern: string, ip: string): boolean {
// Handle exact match
if (pattern === ip) {
return true;
}
// Handle CIDR notation (e.g., 192.168.1.0/24)
if (pattern.includes('/')) {
return this.matchIpCidr(pattern, ip);
}
// Handle glob pattern (e.g., 192.168.1.*)
if (pattern.includes('*')) {
const regexPattern = pattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(ip);
}
return false;
}
/**
* Match an IP against a CIDR pattern
*/
private matchIpCidr(cidr: string, ip: string): boolean {
try {
// In a real implementation, you'd use a proper IP library
// This is a simplified implementation
const [subnet, bits] = cidr.split('/');
const mask = parseInt(bits, 10);
// Convert IP addresses to numeric values
const ipNum = this.ipToNumber(ip);
const subnetNum = this.ipToNumber(subnet);
// Calculate subnet mask
const maskNum = ~(2 ** (32 - mask) - 1);
// Check if IP is in subnet
return (ipNum & maskNum) === (subnetNum & maskNum);
} catch (e) {
console.error(`Error matching IP ${ip} against CIDR ${cidr}:`, e);
return false;
}
}
/**
* Convert an IP address to a numeric value
*/
private ipToNumber(ip: string): number {
const parts = ip.split('.').map(part => parseInt(part, 10));
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
}
/**
* Find the matching route for a connection
*/
public findMatchingRoute(options: {
port: number;
domain?: string;
clientIp: string;
path?: string;
tlsVersion?: string;
}): IRouteMatchResult | null {
const { port, domain, clientIp, path, tlsVersion } = options;
// Get all routes for this port
const routesForPort = this.getRoutesForPort(port);
// Find the first matching route based on priority order
for (const route of routesForPort) {
// Check domain match if specified
if (domain && !this.matchRouteDomain(route, domain)) {
continue;
}
// Check path match if specified in both route and request
if (path && route.match.path) {
if (!this.matchPath(route.match.path, path)) {
continue;
}
}
// Check client IP match
if (route.match.clientIp && !route.match.clientIp.some(pattern =>
this.matchIpPattern(pattern, clientIp))) {
continue;
}
// Check TLS version match
if (tlsVersion && route.match.tlsVersion &&
!route.match.tlsVersion.includes(tlsVersion)) {
continue;
}
// Check security settings
if (!this.isClientIpAllowed(route, clientIp)) {
continue;
}
// All checks passed, this route matches
return { route };
}
return null;
}
/**
* Match a path against a pattern
*/
private matchPath(pattern: string, path: string): boolean {
// Convert the glob pattern to a regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\//g, '\\/'); // Escape slashes
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
/**
* Convert a domain config to routes
* (For backward compatibility with code that still uses domainConfigs)
*/
public domainConfigToRoutes(domainConfig: IDomainConfig): IRouteConfig[] {
const routes: IRouteConfig[] = [];
const { domains, forwarding } = domainConfig;
// Determine the action based on forwarding type
let action: IRouteAction = {
type: 'forward',
target: {
host: forwarding.target.host,
port: forwarding.target.port
}
};
// Set TLS mode based on forwarding type
switch (forwarding.type) {
case 'http-only':
// No TLS settings needed
break;
case 'https-passthrough':
action.tls = { mode: 'passthrough' };
break;
case 'https-terminate-to-http':
action.tls = {
mode: 'terminate',
certificate: forwarding.https?.customCert ? {
key: forwarding.https.customCert.key,
cert: forwarding.https.customCert.cert
} : 'auto'
};
break;
case 'https-terminate-to-https':
action.tls = {
mode: 'terminate-and-reencrypt',
certificate: forwarding.https?.customCert ? {
key: forwarding.https.customCert.key,
cert: forwarding.https.customCert.cert
} : 'auto'
};
break;
}
// Add security settings if present
if (forwarding.security) {
action.security = {
allowedIps: forwarding.security.allowedIps,
blockedIps: forwarding.security.blockedIps,
maxConnections: forwarding.security.maxConnections
};
}
// Add advanced settings if present
if (forwarding.advanced) {
action.advanced = {
timeout: forwarding.advanced.timeout,
headers: forwarding.advanced.headers,
keepAlive: forwarding.advanced.keepAlive
};
}
// Determine which port to use based on forwarding type
const defaultPort = forwarding.type.startsWith('https') ? 443 : 80;
// Add the main route
routes.push({
match: {
ports: defaultPort,
domains
},
action,
name: `Route for ${domains.join(', ')}`
});
// Add HTTP redirect if needed
if (forwarding.http?.redirectToHttps) {
routes.push({
match: {
ports: 80,
domains
},
action: {
type: 'redirect',
redirect: {
to: 'https://{domain}{path}',
status: 301
}
},
name: `HTTP Redirect for ${domains.join(', ')}`,
priority: 100 // Higher priority for redirects
});
}
// Add port ranges if specified
if (forwarding.advanced?.portRanges) {
for (const range of forwarding.advanced.portRanges) {
routes.push({
match: {
ports: [{ from: range.from, to: range.to }],
domains
},
action,
name: `Port Range ${range.from}-${range.to} for ${domains.join(', ')}`
});
}
}
return routes;
}
/**
* Update routes based on domain configs
* (For backward compatibility with code that still uses domainConfigs)
*/
public updateFromDomainConfigs(domainConfigs: IDomainConfig[]): void {
const routes: IRouteConfig[] = [];
// Convert each domain config to routes
for (const config of domainConfigs) {
routes.push(...this.domainConfigToRoutes(config));
}
// Merge with existing routes that aren't derived from domain configs
const nonDomainRoutes = this.routes.filter(r =>
!r.name || !r.name.includes('for '));
this.updateRoutes([...nonDomainRoutes, ...routes]);
}
/**
* Validate the route configuration and return any warnings
*/
public validateConfiguration(): string[] {
const warnings: string[] = [];
const duplicatePorts = new Map<number, number>();
// Check for routes with the same exact match criteria
for (let i = 0; i < this.routes.length; i++) {
for (let j = i + 1; j < this.routes.length; j++) {
const route1 = this.routes[i];
const route2 = this.routes[j];
// Check if route match criteria are the same
if (this.areMatchesSimilar(route1.match, route2.match)) {
warnings.push(
`Routes "${route1.name || i}" and "${route2.name || j}" have similar match criteria. ` +
`The route with higher priority (${Math.max(route1.priority || 0, route2.priority || 0)}) will be used.`
);
}
}
}
// Check for routes that may never be matched due to priority
for (let i = 0; i < this.routes.length; i++) {
const route = this.routes[i];
const higherPriorityRoutes = this.routes.filter(r =>
(r.priority || 0) > (route.priority || 0));
for (const higherRoute of higherPriorityRoutes) {
if (this.isRouteShadowed(route, higherRoute)) {
warnings.push(
`Route "${route.name || i}" may never be matched because it is shadowed by ` +
`higher priority route "${higherRoute.name || 'unnamed'}"`
);
break;
}
}
}
return warnings;
}
/**
* Check if two route matches are similar (potential conflict)
*/
private areMatchesSimilar(match1: IRouteMatch, match2: IRouteMatch): boolean {
// Check port overlap
const ports1 = new Set(this.expandPortRange(match1.ports));
const ports2 = new Set(this.expandPortRange(match2.ports));
let havePortOverlap = false;
for (const port of ports1) {
if (ports2.has(port)) {
havePortOverlap = true;
break;
}
}
if (!havePortOverlap) {
return false;
}
// Check domain overlap
if (match1.domains && match2.domains) {
const domains1 = Array.isArray(match1.domains) ? match1.domains : [match1.domains];
const domains2 = Array.isArray(match2.domains) ? match2.domains : [match2.domains];
// Check if any domain pattern from match1 could match any from match2
let haveDomainOverlap = false;
for (const domain1 of domains1) {
for (const domain2 of domains2) {
if (domain1 === domain2 ||
(domain1.includes('*') || domain2.includes('*'))) {
haveDomainOverlap = true;
break;
}
}
if (haveDomainOverlap) break;
}
if (!haveDomainOverlap) {
return false;
}
} else if (match1.domains || match2.domains) {
// One has domains, the other doesn't - they could overlap
// The one with domains is more specific, so it's not exactly a conflict
return false;
}
// Check path overlap
if (match1.path && match2.path) {
// This is a simplified check - in a real implementation,
// you'd need to check if the path patterns could match the same paths
return match1.path === match2.path ||
match1.path.includes('*') ||
match2.path.includes('*');
} else if (match1.path || match2.path) {
// One has a path, the other doesn't
return false;
}
// If we get here, the matches have significant overlap
return true;
}
/**
* Check if a route is completely shadowed by a higher priority route
*/
private isRouteShadowed(route: IRouteConfig, higherPriorityRoute: IRouteConfig): boolean {
// If they don't have similar match criteria, no shadowing occurs
if (!this.areMatchesSimilar(route.match, higherPriorityRoute.match)) {
return false;
}
// If higher priority route has more specific criteria, no shadowing
if (this.isRouteMoreSpecific(higherPriorityRoute.match, route.match)) {
return false;
}
// If higher priority route is equally or less specific but has higher priority,
// it shadows the lower priority route
return true;
}
/**
* Check if route1 is more specific than route2
*/
private isRouteMoreSpecific(match1: IRouteMatch, match2: IRouteMatch): boolean {
// Check if match1 has more specific criteria
let match1Points = 0;
let match2Points = 0;
// Path is the most specific
if (match1.path) match1Points += 3;
if (match2.path) match2Points += 3;
// Domain is next most specific
if (match1.domains) match1Points += 2;
if (match2.domains) match2Points += 2;
// Client IP and TLS version are least specific
if (match1.clientIp) match1Points += 1;
if (match2.clientIp) match2Points += 1;
if (match1.tlsVersion) match1Points += 1;
if (match2.tlsVersion) match2Points += 1;
return match1Points > match2Points;
}
}

View File

@ -1,6 +1,6 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
// Importing from the new structure // Importing required components
import { ConnectionManager } from './connection-manager.js'; import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js'; import { SecurityManager } from './security-manager.js';
import { DomainConfigManager } from './domain-config-manager.js'; import { DomainConfigManager } from './domain-config-manager.js';
@ -8,23 +8,27 @@ import { TlsManager } from './tls-manager.js';
import { NetworkProxyBridge } from './network-proxy-bridge.js'; import { NetworkProxyBridge } from './network-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js'; import { TimeoutManager } from './timeout-manager.js';
import { PortRangeManager } from './port-range-manager.js'; import { PortRangeManager } from './port-range-manager.js';
import { ConnectionHandler } from './connection-handler.js'; import { RouteManager } from './route-manager.js';
import { RouteConnectionHandler } from './route-connection-handler.js';
// External dependencies from migrated modules // External dependencies
import { Port80Handler } from '../../http/port80/port80-handler.js'; import { Port80Handler } from '../../http/port80/port80-handler.js';
import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js';
import type { ICertificateData } from '../../certificate/models/certificate-types.js'; import type { ICertificateData } from '../../certificate/models/certificate-types.js';
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
import type { TForwardingType } from '../../forwarding/config/forwarding-types.js';
import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
// Import types from models // Import types and utilities
import type { ISmartProxyOptions, IDomainConfig } from './models/interfaces.js'; import type {
// Provide backward compatibility types ISmartProxyOptions,
export type { ISmartProxyOptions as IPortProxySettings, IDomainConfig }; IRoutedSmartProxyOptions,
IDomainConfig
} from './models/interfaces.js';
import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js';
/** /**
* SmartProxy - Main class that coordinates all components * SmartProxy - Unified route-based API
*/ */
export class SmartProxy extends plugins.EventEmitter { export class SmartProxy extends plugins.EventEmitter {
private netServers: plugins.net.Server[] = []; private netServers: plugins.net.Server[] = [];
@ -34,24 +38,28 @@ export class SmartProxy extends plugins.EventEmitter {
// Component managers // Component managers
private connectionManager: ConnectionManager; private connectionManager: ConnectionManager;
private securityManager: SecurityManager; private securityManager: SecurityManager;
public domainConfigManager: DomainConfigManager; private domainConfigManager: DomainConfigManager;
private tlsManager: TlsManager; private tlsManager: TlsManager;
private networkProxyBridge: NetworkProxyBridge; private networkProxyBridge: NetworkProxyBridge;
private timeoutManager: TimeoutManager; private timeoutManager: TimeoutManager;
private portRangeManager: PortRangeManager; private portRangeManager: PortRangeManager;
private connectionHandler: ConnectionHandler; private routeManager: RouteManager;
private routeConnectionHandler: RouteConnectionHandler;
// Port80Handler for ACME certificate management // Port80Handler for ACME certificate management
private port80Handler: Port80Handler | null = null; private port80Handler: Port80Handler | null = null;
// CertProvisioner for unified certificate workflows // CertProvisioner for unified certificate workflows
private certProvisioner?: CertProvisioner; private certProvisioner?: CertProvisioner;
/**
* Constructor that supports both legacy and route-based configuration
*/
constructor(settingsArg: ISmartProxyOptions) { constructor(settingsArg: ISmartProxyOptions) {
super(); super();
// Set reasonable defaults for all settings // Set reasonable defaults for all settings
this.settings = { this.settings = {
...settingsArg, ...settingsArg,
targetIP: settingsArg.targetIP || 'localhost',
initialDataTimeout: settingsArg.initialDataTimeout || 120000, initialDataTimeout: settingsArg.initialDataTimeout || 120000,
socketTimeout: settingsArg.socketTimeout || 3600000, socketTimeout: settingsArg.socketTimeout || 3600000,
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000,
@ -76,12 +84,11 @@ export class SmartProxy extends plugins.EventEmitter {
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000,
networkProxyPort: settingsArg.networkProxyPort || 8443, networkProxyPort: settingsArg.networkProxyPort || 8443,
acme: settingsArg.acme || {},
globalPortRanges: settingsArg.globalPortRanges || [],
}; };
// Set default ACME options if not provided // Set default ACME options if not provided
if (!this.settings.acme || Object.keys(this.settings.acme).length === 0) { this.settings.acme = this.settings.acme || {};
if (Object.keys(this.settings.acme).length === 0) {
this.settings.acme = { this.settings.acme = {
enabled: false, enabled: false,
port: 80, port: 80,
@ -91,7 +98,7 @@ export class SmartProxy extends plugins.EventEmitter {
autoRenew: true, autoRenew: true,
certificateStore: './certs', certificateStore: './certs',
skipConfiguredCerts: false, skipConfiguredCerts: false,
httpsRedirectPort: this.settings.fromPort, httpsRedirectPort: this.settings.fromPort || 443,
renewCheckIntervalHours: 24, renewCheckIntervalHours: 24,
domainForwards: [] domainForwards: []
}; };
@ -105,13 +112,20 @@ export class SmartProxy extends plugins.EventEmitter {
this.securityManager, this.securityManager,
this.timeoutManager this.timeoutManager
); );
// Create domain config manager and port range manager (for backward compatibility)
this.domainConfigManager = new DomainConfigManager(this.settings); this.domainConfigManager = new DomainConfigManager(this.settings);
this.tlsManager = new TlsManager(this.settings);
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
this.portRangeManager = new PortRangeManager(this.settings); this.portRangeManager = new PortRangeManager(this.settings);
// Initialize connection handler // Create the new route manager
this.connectionHandler = new ConnectionHandler( this.routeManager = new RouteManager(this.settings);
// Create other required components
this.tlsManager = new TlsManager(this.settings);
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
// Initialize connection handler with route support
this.routeConnectionHandler = new RouteConnectionHandler(
this.settings, this.settings,
this.connectionManager, this.connectionManager,
this.securityManager, this.securityManager,
@ -119,12 +133,12 @@ export class SmartProxy extends plugins.EventEmitter {
this.tlsManager, this.tlsManager,
this.networkProxyBridge, this.networkProxyBridge,
this.timeoutManager, this.timeoutManager,
this.portRangeManager this.routeManager
); );
} }
/** /**
* The settings for the port proxy * The settings for the SmartProxy
*/ */
public settings: ISmartProxyOptions; public settings: ISmartProxyOptions;
@ -142,8 +156,9 @@ export class SmartProxy extends plugins.EventEmitter {
// Build and start the Port80Handler // Build and start the Port80Handler
this.port80Handler = buildPort80Handler({ this.port80Handler = buildPort80Handler({
...config, ...config,
httpsRedirectPort: config.httpsRedirectPort || this.settings.fromPort httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443)
}); });
// Share Port80Handler with NetworkProxyBridge before start // Share Port80Handler with NetworkProxyBridge before start
this.networkProxyBridge.setPort80Handler(this.port80Handler); this.networkProxyBridge.setPort80Handler(this.port80Handler);
await this.port80Handler.start(); await this.port80Handler.start();
@ -154,7 +169,7 @@ export class SmartProxy extends plugins.EventEmitter {
} }
/** /**
* Start the proxy server * Start the proxy server with support for both configuration types
*/ */
public async start() { public async start() {
// Don't start if already shutting down // Don't start if already shutting down
@ -163,11 +178,11 @@ export class SmartProxy extends plugins.EventEmitter {
return; return;
} }
// Process domain configs // If using legacy format, make sure domainConfigs are initialized
// Note: ensureForwardingConfig is no longer needed since forwarding is now required if (isLegacyOptions(this.settings)) {
// Initialize domain config manager with the processed configs
// Initialize domain config manager with the processed configs this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); }
// Initialize Port80Handler if enabled // Initialize Port80Handler if enabled
await this.initializePort80Handler(); await this.initializePort80Handler();
@ -176,20 +191,39 @@ export class SmartProxy extends plugins.EventEmitter {
if (this.port80Handler) { if (this.port80Handler) {
const acme = this.settings.acme!; const acme = this.settings.acme!;
// Convert domain forwards to use the new forwarding system if possible // Setup domain forwards based on configuration type
const domainForwards = acme.domainForwards?.map(f => { const domainForwards = acme.domainForwards?.map(f => {
// If the domain has a forwarding config in domainConfigs, use that if (isLegacyOptions(this.settings)) {
const domainConfig = this.settings.domainConfigs.find( // If using legacy mode, check if domain config exists
dc => dc.domains.some(d => d === f.domain) const domainConfig = this.settings.domainConfigs.find(
); dc => dc.domains.some(d => d === f.domain)
);
if (domainConfig?.forwarding) { if (domainConfig?.forwarding) {
return { return {
domain: f.domain,
forwardConfig: f.forwardConfig,
acmeForwardConfig: f.acmeForwardConfig,
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
};
}
} else {
// In route mode, look for matching route
const route = this.routeManager.findMatchingRoute({
port: 443,
domain: f.domain, domain: f.domain,
forwardConfig: f.forwardConfig, clientIp: '127.0.0.1' // Dummy IP for finding routes
acmeForwardConfig: f.acmeForwardConfig, })?.route;
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
}; if (route && route.action.type === 'forward' && route.action.tls) {
// If we found a matching route with TLS settings
return {
domain: f.domain,
forwardConfig: f.forwardConfig,
acmeForwardConfig: f.acmeForwardConfig,
sslRedirect: f.sslRedirect || false
};
}
} }
// Otherwise use the existing configuration // Otherwise use the existing configuration
@ -201,17 +235,38 @@ export class SmartProxy extends plugins.EventEmitter {
}; };
}) || []; }) || [];
this.certProvisioner = new CertProvisioner( // Create CertProvisioner with appropriate parameters
this.settings.domainConfigs, if (isLegacyOptions(this.settings)) {
this.port80Handler, this.certProvisioner = new CertProvisioner(
this.networkProxyBridge, this.settings.domainConfigs,
this.settings.certProvisionFunction, this.port80Handler,
acme.renewThresholdDays!, this.networkProxyBridge,
acme.renewCheckIntervalHours!, this.settings.certProvisionFunction,
acme.autoRenew!, acme.renewThresholdDays!,
domainForwards acme.renewCheckIntervalHours!,
); acme.autoRenew!,
domainForwards
);
} else {
// For route-based configuration, we need to adapt the interface
// Convert routes to domain configs for CertProvisioner
const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes(
(this.settings as IRoutedSmartProxyOptions).routes
);
this.certProvisioner = new CertProvisioner(
domainConfigs,
this.port80Handler,
this.networkProxyBridge,
this.settings.certProvisionFunction,
acme.renewThresholdDays!,
acme.renewCheckIntervalHours!,
acme.autoRenew!,
domainForwards
);
}
// Register certificate event handler
this.certProvisioner.on('certificate', (certData) => { this.certProvisioner.on('certificate', (certData) => {
this.emit('certificate', { this.emit('certificate', {
domain: certData.domain, domain: certData.domain,
@ -228,25 +283,22 @@ export class SmartProxy extends plugins.EventEmitter {
} }
// Initialize and start NetworkProxy if needed // Initialize and start NetworkProxy if needed
if ( if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
this.settings.useNetworkProxy &&
this.settings.useNetworkProxy.length > 0
) {
await this.networkProxyBridge.initialize(); await this.networkProxyBridge.initialize();
await this.networkProxyBridge.start(); await this.networkProxyBridge.start();
} }
// Validate port configuration // Validate the route configuration
const configWarnings = this.portRangeManager.validateConfiguration(); const configWarnings = this.routeManager.validateConfiguration();
if (configWarnings.length > 0) { if (configWarnings.length > 0) {
console.log("Port configuration warnings:"); console.log("Route configuration warnings:");
for (const warning of configWarnings) { for (const warning of configWarnings) {
console.log(` - ${warning}`); console.log(` - ${warning}`);
} }
} }
// Get listening ports from PortRangeManager // Get listening ports from RouteManager
const listeningPorts = this.portRangeManager.getListeningPorts(); const listeningPorts = this.routeManager.getListeningPorts();
// Create servers for each port // Create servers for each port
for (const port of listeningPorts) { for (const port of listeningPorts) {
@ -258,8 +310,8 @@ export class SmartProxy extends plugins.EventEmitter {
return; return;
} }
// Delegate to connection handler // Delegate to route connection handler
this.connectionHandler.handleConnection(socket); this.routeConnectionHandler.handleConnection(socket);
}).on('error', (err: Error) => { }).on('error', (err: Error) => {
console.log(`Server Error on port ${port}: ${err.message}`); console.log(`Server Error on port ${port}: ${err.message}`);
}); });
@ -268,7 +320,9 @@ export class SmartProxy extends plugins.EventEmitter {
const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
console.log( console.log(
`SmartProxy -> OK: Now listening on port ${port}${ `SmartProxy -> OK: Now listening on port ${port}${
this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' isLegacyOptions(this.settings) && this.settings.sniEnabled && !isNetworkProxyPort ?
' (SNI passthrough enabled)' :
''
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
); );
}); });
@ -348,12 +402,70 @@ export class SmartProxy extends plugins.EventEmitter {
} }
} }
/**
* Extract domain configurations from routes for certificate provisioning
*/
private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] {
const domainConfigs: IDomainConfig[] = [];
for (const route of routes) {
// Skip routes without domain specs
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Only process routes that need TLS termination (those with certificates)
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
!route.action.target) continue;
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Determine forwarding type based on TLS mode
const forwardingType = route.action.tls.mode === 'terminate'
? 'https-terminate-to-http'
: 'https-terminate-to-https';
// Create a forwarding config
const forwarding = {
type: forwardingType as any,
target: {
host: Array.isArray(route.action.target.host)
? route.action.target.host[0]
: route.action.target.host,
port: route.action.target.port
},
// Add TLS settings
https: {
customCert: route.action.tls.certificate !== 'auto'
? route.action.tls.certificate
: undefined
},
// Add security settings if present
security: route.action.security,
// Add advanced settings if present
advanced: route.action.advanced
};
domainConfigs.push({
domains,
forwarding
});
}
return domainConfigs;
}
/** /**
* Stop the proxy server * Stop the proxy server
*/ */
public async stop() { public async stop() {
console.log('SmartProxy shutting down...'); console.log('SmartProxy shutting down...');
this.isShuttingDown = true; this.isShuttingDown = true;
// Stop CertProvisioner if active // Stop CertProvisioner if active
if (this.certProvisioner) { if (this.certProvisioner) {
await this.certProvisioner.stop(); await this.certProvisioner.stop();
@ -411,14 +523,17 @@ export class SmartProxy extends plugins.EventEmitter {
} }
/** /**
* Updates the domain configurations for the proxy * Updates the domain configurations for the proxy (legacy support)
*/ */
public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> { public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
// Update domain configs in DomainConfigManager // Update domain configs in DomainConfigManager (legacy)
this.domainConfigManager.updateDomainConfigs(newDomainConfigs); this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
// Also update the RouteManager with these domain configs
this.routeManager.updateFromDomainConfigs(newDomainConfigs);
// If NetworkProxy is initialized, resync the configurations // If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) { if (this.networkProxyBridge.getNetworkProxy()) {
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
@ -428,7 +543,7 @@ export class SmartProxy extends plugins.EventEmitter {
if (this.port80Handler && this.settings.acme?.enabled) { if (this.port80Handler && this.settings.acme?.enabled) {
for (const domainConfig of newDomainConfigs) { for (const domainConfig of newDomainConfigs) {
// Skip certificate provisioning for http-only or passthrough configs that don't need certs // Skip certificate provisioning for http-only or passthrough configs that don't need certs
const forwardingType = domainConfig.forwarding.type; const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
const needsCertificate = const needsCertificate =
forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-http' ||
forwardingType === 'https-terminate-to-https'; forwardingType === 'https-terminate-to-https';
@ -490,6 +605,95 @@ export class SmartProxy extends plugins.EventEmitter {
} }
} }
/**
* Update routes with new configuration (new API)
*/
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
console.log(`Updating routes (${newRoutes.length} routes)`);
// Update routes in RouteManager
this.routeManager.updateRoutes(newRoutes);
// If NetworkProxy is initialized, resync the configurations
if (this.networkProxyBridge.getNetworkProxy()) {
// Create equivalent domain configs for NetworkProxy
const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes);
// Update domain configs in DomainConfigManager for sync
this.domainConfigManager.updateDomainConfigs(domainConfigs);
// Sync with NetworkProxy
await this.networkProxyBridge.syncDomainConfigsToNetworkProxy();
}
// If Port80Handler is running, provision certificates based on routes
if (this.port80Handler && this.settings.acme?.enabled) {
for (const route of newRoutes) {
// Skip routes without domains
if (!route.match.domains) continue;
// Skip non-forward routes
if (route.action.type !== 'forward') continue;
// Skip routes without TLS termination
if (!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
!route.action.target) continue;
// Skip certificate provisioning if certificate is not auto
if (route.action.tls.certificate !== 'auto') continue;
const domains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
for (const domain of domains) {
const isWildcard = domain.includes('*');
let provision: string | plugins.tsclass.network.ICert = 'http01';
if (this.settings.certProvisionFunction) {
try {
provision = await this.settings.certProvisionFunction(domain);
} catch (err) {
console.log(`certProvider error for ${domain}: ${err}`);
}
} else if (isWildcard) {
console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`);
continue;
}
if (provision === 'http01') {
if (isWildcard) {
console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`);
continue;
}
// Register domain with Port80Handler
this.port80Handler.addDomain({
domainName: domain,
sslRedirect: true,
acmeMaintenance: true
});
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
} else {
// Handle static certificate (e.g., DNS-01 provisioned)
const certObj = provision as plugins.tsclass.network.ICert;
const certData: ICertificateData = {
domain: certObj.domainName,
certificate: certObj.publicKey,
privateKey: certObj.privateKey,
expiryDate: new Date(certObj.validUntil)
};
this.networkProxyBridge.applyExternalCertificate(certData);
console.log(`Applied static certificate for ${domain} from certProvider`);
}
}
}
console.log('Provisioned certificates for new routes');
}
}
/** /**
* Request a certificate for a specific domain * Request a certificate for a specific domain
@ -583,7 +787,8 @@ export class SmartProxy extends plugins.EventEmitter {
networkProxyConnections, networkProxyConnections,
terminationStats, terminationStats,
acmeEnabled: !!this.port80Handler, acmeEnabled: !!this.port80Handler,
port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
routes: this.routeManager.getListeningPorts().length
}; };
} }
@ -591,18 +796,44 @@ export class SmartProxy extends plugins.EventEmitter {
* Get a list of eligible domains for ACME certificates * Get a list of eligible domains for ACME certificates
*/ */
public getEligibleDomainsForCertificates(): string[] { public getEligibleDomainsForCertificates(): string[] {
// Collect all non-wildcard domains from domain configs
const domains: string[] = []; const domains: string[] = [];
for (const config of this.settings.domainConfigs) { // Get domains from routes
const routes = isRoutedOptions(this.settings) ? this.settings.routes : [];
for (const route of routes) {
if (!route.match.domains) continue;
// Skip routes without TLS termination or auto certificates
if (route.action.type !== 'forward' ||
!route.action.tls ||
route.action.tls.mode === 'passthrough' ||
route.action.tls.certificate !== 'auto') continue;
const routeDomains = Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
// Skip domains that can't be used with ACME // Skip domains that can't be used with ACME
const eligibleDomains = config.domains.filter(domain => const eligibleDomains = routeDomains.filter(domain =>
!domain.includes('*') && this.isValidDomain(domain) !domain.includes('*') && this.isValidDomain(domain)
); );
domains.push(...eligibleDomains); domains.push(...eligibleDomains);
} }
// For legacy mode, also get domains from domain configs
if (isLegacyOptions(this.settings)) {
for (const config of this.settings.domainConfigs) {
// Skip domains that can't be used with ACME
const eligibleDomains = config.domains.filter(domain =>
!domain.includes('*') && this.isValidDomain(domain)
);
domains.push(...eligibleDomains);
}
}
return domains; return domains;
} }