Compare commits

...

8 Commits

Author SHA1 Message Date
18f03c1acf 15.0.1
Some checks failed
Default (tags) / security (push) Successful in 42s
Default (tags) / test (push) Failing after 2m13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-10 00:26:04 +00:00
200635e4bd fix 2025-05-10 00:26:03 +00:00
95c5c1b90d 15.0.0
Some checks failed
Default (tags) / security (push) Successful in 46s
Default (tags) / test (push) Failing after 1m45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-10 00:06:53 +00:00
bb66b98f1d BREAKING CHANGE(documentation): Update readme documentation to comprehensively describe the new unified route-based configuration system in v14.0.0 2025-05-10 00:06:53 +00:00
28022ebe87 change to route based approach 2025-05-10 00:01:02 +00:00
552f4c246b new plan 2025-05-09 23:13:48 +00:00
09fc71f051 13.1.3
Some checks failed
Default (tags) / security (push) Successful in 34s
Default (tags) / test (push) Failing after 1m32s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-05-09 22:58:42 +00:00
e508078ecf fix(documentation): Update readme.md to provide a unified and comprehensive overview of SmartProxy, with reorganized sections, enhanced diagrams, and detailed usage examples for various proxy scenarios. 2025-05-09 22:58:42 +00:00
16 changed files with 4294 additions and 793 deletions

View File

@ -1,5 +1,22 @@
# Changelog # Changelog
## 2025-05-10 - 15.0.0 - BREAKING CHANGE(documentation)
Update readme documentation to comprehensively describe the new unified route-based configuration system in v14.0.0
- Added detailed description of IRouteConfig, IRouteMatch, and IRouteAction interfaces
- Improved explanation of port, domain, path, client IP, and TLS version matching features
- Included examples of helper functions (createHttpRoute, createHttpsRoute, etc.) with usage of template variables
- Enhanced migration guide from legacy configurations to the new match/action pattern
- Updated examples and tests to reflect the new documentation structure
## 2025-05-09 - 13.1.3 - fix(documentation)
Update readme.md to provide a unified and comprehensive overview of SmartProxy, with reorganized sections, enhanced diagrams, and detailed usage examples for various proxy scenarios.
- Reorganized key sections to clearly list Primary API, Helper Functions, Specialized Components, and Core Utilities.
- Added detailed Quick Start examples covering API Gateway, automatic HTTPS, load balancing, wildcard subdomain support, and comprehensive proxy server setups.
- Included updated architecture flow diagrams and explanations of Unified Forwarding System and ACME integration.
- Improved clarity and consistency across documentation, with revised formatting and expanded descriptions.
## 2025-05-09 - 13.1.2 - fix(docs) ## 2025-05-09 - 13.1.2 - fix(docs)
Update readme to reflect updated interface and type naming conventions Update readme to reflect updated interface and type naming conventions

View File

@ -1,8 +1,8 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "13.1.2", "version": "15.0.1",
"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",

1294
readme.md

File diff suppressed because it is too large Load Diff

View File

@ -1,255 +1,316 @@
# SmartProxy Interface & Type Naming Standardization Plan # SmartProxy Fully Unified Configuration Plan (Updated)
## Project Goal ## Project Goal
Standardize interface and type naming throughout the SmartProxy codebase to improve maintainability, readability, and developer experience by: Redesign SmartProxy's configuration for a more elegant, unified, and comprehensible approach by:
1. Ensuring all interfaces are prefixed with "I" 1. Creating a single, unified configuration model that covers both "where to listen" and "how to forward"
2. Ensuring all type aliases are prefixed with "T" 2. Eliminating the confusion between domain configs and port forwarding
3. Maintaining backward compatibility through type aliases 3. Providing a clear, declarative API that makes the intent obvious
4. Updating documentation to reflect naming conventions 4. Enhancing maintainability and extensibility for future features
5. Completely removing legacy code to eliminate technical debt
## Phase 2: Core Module Standardization ## Current Issues
- [ ] Update core module interfaces and types The current approach has several issues:
- [ ] Rename interfaces in `ts/core/models/common-types.ts`
- [ ] `AcmeOptions``IAcmeOptions`
- [ ] `DomainOptions``IDomainOptions`
- [ ] Other common interfaces
- [ ] Add backward compatibility aliases
- [ ] Update imports throughout core module
- [ ] Update core utility type definitions 1. **Dual Configuration Systems**:
- [ ] Update `ts/core/utils/validation-utils.ts` - Port configuration (`fromPort`, `toPort`, `globalPortRanges`) for "where to listen"
- [ ] Update `ts/core/utils/ip-utils.ts` - Domain configuration (`domainConfigs`) for "how to forward"
- [ ] Standardize event type definitions - Unclear relationship between these two systems
- [ ] Test core module changes 2. **Mixed Concerns**:
- [ ] Run unit tests for core modules - Port management is mixed with forwarding logic
- [ ] Verify type compatibility - Domain routing is separated from port listening
- [ ] Ensure backward compatibility - Security settings defined in multiple places
## Phase 3: Certificate Module Standardization 3. **Complex Logic**:
- PortRangeManager must coordinate with domain configuration
- Implicit rules for handling connections based on port and domain
- [ ] Update certificate interfaces 4. **Difficult to Understand and Configure**:
- [ ] Rename interfaces in `ts/certificate/models/certificate-types.ts` - Two separate configuration hierarchies that must work together
- [ ] `CertificateData``ICertificateData` - Unclear which settings take precedence
- [ ] `Certificates``ICertificates`
- [ ] `CertificateFailure``ICertificateFailure`
- [ ] `CertificateExpiring``ICertificateExpiring`
- [ ] `ForwardConfig``IForwardConfig`
- [ ] `DomainForwardConfig``IDomainForwardConfig`
- [ ] Update ACME challenge interfaces
- [ ] Standardize storage provider interfaces
- [ ] Ensure certificate provider compatibility ## Proposed Solution: Fully Unified Routing Configuration
- [ ] Update provider implementations
- [ ] Rename internal interfaces
- [ ] Maintain public API compatibility
- [ ] Test certificate module Replace both port and domain configuration with a single, unified configuration:
- [ ] Verify ACME functionality
- [ ] Test certificate provisioning
- [ ] Validate challenge handling
## Phase 4: Forwarding System Standardization ```typescript
// The core unified configuration interface
interface IRouteConfig {
// What to match
match: {
// Listen on these ports (required)
ports: number | number[] | Array<{ from: number, to: number }>;
- [ ] Update forwarding configuration interfaces // Optional domain patterns to match (default: all domains)
- [ ] Rename interfaces in `ts/forwarding/config/forwarding-types.ts` domains?: string | string[];
- [ ] `TargetConfig``ITargetConfig`
- [ ] `HttpOptions``IHttpOptions`
- [ ] `HttpsOptions``IHttpsOptions`
- [ ] `AcmeForwardingOptions``IAcmeForwardingOptions`
- [ ] `SecurityOptions``ISecurityOptions`
- [ ] `AdvancedOptions``IAdvancedOptions`
- [ ] `ForwardConfig``IForwardConfig`
- [ ] Rename type definitions
- [ ] `ForwardingType``TForwardingType`
- [ ] Update domain configuration interfaces
- [ ] Standardize handler interfaces // Advanced matching criteria
- [ ] Update base handler interfaces path?: string; // Match specific paths
- [ ] Rename handler-specific interfaces clientIp?: string[]; // Match specific client IPs
- [ ] Update factory interfaces tlsVersion?: string[]; // Match specific TLS versions
};
- [ ] Verify forwarding system functionality // What to do with matched traffic
- [ ] Test all forwarding types action: {
- [ ] Verify configuration parsing // Basic routing
- [ ] Ensure backward compatibility type: 'forward' | 'redirect' | 'block';
## Phase 5: Proxy Implementation Standardization // Target for forwarding
target?: {
host: string | string[]; // Support single host or round-robin
port: number;
preservePort?: boolean; // Use incoming port as target port
};
- [ ] Update SmartProxy interfaces // TLS handling
- [ ] Rename interfaces in `ts/proxies/smart-proxy/models/interfaces.ts` tls?: {
- [ ] Update domain configuration interfaces mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
- [ ] Standardize manager interfaces certificate?: 'auto' | { // Auto = use ACME
key: string;
cert: string;
};
};
- [ ] Update NetworkProxy interfaces // For redirects
- [ ] Rename in `ts/proxies/network-proxy/models/types.ts` redirect?: {
- [ ] `NetworkProxyOptions``INetworkProxyOptions` to: string; // URL or template with {domain}, {port}, etc.
- [ ] `CertificateEntry``ICertificateEntry` status: 301 | 302 | 307 | 308;
- [ ] `ReverseProxyConfig``IReverseProxyConfig` };
- [ ] `ConnectionEntry``IConnectionEntry`
- [ ] `WebSocketWithHeartbeat``IWebSocketWithHeartbeat`
- [ ] `Logger``ILogger`
- [ ] Update request handler interfaces
- [ ] Standardize connection interfaces
- [ ] Update NfTablesProxy interfaces // Security options
- [ ] Rename interfaces in `ts/proxies/nftables-proxy/models/interfaces.ts` security?: {
- [ ] Update configuration interfaces allowedIps?: string[];
- [ ] Standardize firewall rule interfaces blockedIps?: string[];
maxConnections?: number;
authentication?: {
type: 'basic' | 'digest' | 'oauth';
// Auth-specific options
};
};
- [ ] Test proxy implementations // Advanced options
- [ ] Verify SmartProxy functionality advanced?: {
- [ ] Test NetworkProxy with renamed interfaces timeout?: number;
- [ ] Validate NfTablesProxy operations headers?: Record<string, string>;
keepAlive?: boolean;
// etc.
};
};
## Phase 6: HTTP & TLS Module Standardization // 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
}
- [ ] Update HTTP interfaces // Main SmartProxy options
- [ ] Rename in `ts/http/port80/acme-interfaces.ts` interface ISmartProxyOptions {
- [ ] `SmartAcmeCert``ISmartAcmeCert` // The unified configuration array (required)
- [ ] `SmartAcmeOptions``ISmartAcmeOptions` routes: IRouteConfig[];
- [ ] `Http01Challenge``IHttp01Challenge`
- [ ] `SmartAcme``ISmartAcme`
- [ ] Standardize router interfaces
- [ ] Update port80 handler interfaces
- [ ] Update redirect interfaces
- [ ] Update TLS/SNI interfaces // Global/default settings
- [ ] Standardize SNI handler interfaces defaults?: {
- [ ] Update client hello parser types target?: {
- [ ] Rename TLS alert interfaces host: string;
port: number;
};
security?: {
// Global security defaults
};
tls?: {
// Global TLS defaults
};
// ...other defaults
};
- [ ] Test HTTP & TLS functionality // Other global settings remain (acme, etc.)
- [ ] Verify router operation acme?: IAcmeOptions;
- [ ] Test SNI extraction
- [ ] Validate redirect functionality
## Phase 7: Backward Compatibility Layer // Advanced settings remain as well
// ...
}
```
- [ ] Implement comprehensive type aliases ## Revised Implementation Plan
- [ ] Create aliases for all renamed interfaces
- [ ] Add deprecation notices via JSDoc
- [ ] Ensure all exports include both named versions
- [ ] Update main entry point ### Phase 1: Core Design & Interface Definition
- [ ] Update `ts/index.ts` with all exports
- [ ] Include both prefixed and non-prefixed names
- [ ] Organize exports by module
- [ ] Add compatibility documentation 1. **Define New Core Interfaces**:
- [ ] Document renaming strategy - Create `IRouteConfig` interface with `match` and `action` branches
- [ ] Provide migration examples - Define all sub-interfaces for matching and actions
- [ ] Create deprecation timeline - Create new `ISmartProxyOptions` to use `routes` array exclusively
- Define template variable system for dynamic values
## Phase 8: Documentation & Examples 2. **Create Helper Functions**:
- `createRoute()` - Basic route creation with reasonable defaults
- `createHttpRoute()`, `createHttpsRoute()`, `createRedirect()` - Common scenarios
- `createLoadBalancer()` - For multi-target setups
- `mergeSecurity()`, `mergeDefaults()` - For combining configs
- [ ] Update README and API documentation 3. **Design Router**:
- [ ] Update interface references in README.md - Decision tree for route matching algorithm
- [ ] Document naming convention in README.md - Priority system for route ordering
- [ ] Update API reference documentation - Optimized lookup strategy for fast routing
- [ ] Update examples ### Phase 2: Core Implementation
- [ ] Modify example code to use new interface names
- [ ] Add compatibility notes
- [ ] Create migration examples
- [ ] Add contributor guidelines 1. **Create RouteManager**:
- [ ] Document naming conventions - Build a new RouteManager to replace both PortRangeManager and DomainConfigManager
- [ ] Add interface/type style guide - Implement port and domain matching in one unified system
- [ ] Update PR templates - Create efficient route lookup algorithms
## Phase 9: Testing & Validation 2. **Implement New ConnectionHandler**:
- Create a new ConnectionHandler built from scratch for routes
- Implement the routing logic with the new match/action pattern
- Support template processing for headers and other dynamic values
- [ ] Run comprehensive test suite 3. **Implement New SmartProxy Core**:
- [ ] Run all unit tests - Create new SmartProxy implementation using routes exclusively
- [ ] Execute integration tests - Build network servers based on port specifications
- [ ] Verify example code - Manage TLS contexts and certificates
- [ ] Build type declarations ### Phase 3: Legacy Code Removal
- [ ] Generate TypeScript declaration files
- [ ] Verify exported types
- [ ] Validate documentation generation
- [ ] Final compatibility check 1. **Identify Legacy Components**:
- [ ] Verify import compatibility - Create an inventory of all files and components to be removed
- [ ] Test with existing dependent projects - Document dependencies between legacy components
- [ ] Validate backward compatibility claims - Create a removal plan that minimizes disruption
2. **Remove Legacy Components**:
- Remove PortRangeManager and related code
- Remove DomainConfigManager and related code
- Remove old ConnectionHandler implementation
- Remove other legacy components
3. **Clean Interface Adaptations**:
- Remove all legacy interfaces and types
- Update type exports to only expose route-based interfaces
- Remove any adapter or backward compatibility code
### Phase 4: Updated Documentation & Examples
1. **Update Core Documentation**:
- Rewrite README.md with a focus on route-based configuration exclusively
- Create interface reference documentation
- Document all template variables
2. **Create Example Library**:
- Common configuration patterns using the new API
- Complex use cases for advanced features
- Infrastructure-as-code examples
3. **Add Validation Tools**:
- Configuration validator to check for issues
- Schema definitions for IDE autocomplete
- Runtime validation helpers
### Phase 5: Testing
1. **Unit Tests**:
- Test route matching logic
- Validate priority handling
- Test template processing
2. **Integration Tests**:
- Verify full proxy flows with the new system
- Test complex routing scenarios
- Ensure all features work as expected
3. **Performance Testing**:
- Benchmark routing performance
- Evaluate memory usage
- Test with large numbers of routes
## Implementation Strategy ## Implementation Strategy
### Naming Pattern Rules ### Code Organization
1. **Interfaces**: 1. **New Files**:
- All interfaces should be prefixed with "I" - `route-config.ts` - Core route interfaces
- Example: `DomainConfig``IDomainConfig` - `route-manager.ts` - Route matching and management
- `route-connection-handler.ts` - Connection handling with routes
- `route-smart-proxy.ts` - Main SmartProxy implementation
- `template-engine.ts` - For variable substitution
2. **Type Aliases**: 2. **File Removal**:
- All type aliases should be prefixed with "T" - Remove `port-range-manager.ts`
- Example: `ForwardingType``TForwardingType` - Remove `domain-config-manager.ts`
- Remove legacy interfaces and adapter code
- Remove backward compatibility shims
3. **Enums**: ### Transition Strategy
- Enums should be named in PascalCase without prefix
- Example: `CertificateSource`
4. **Backward Compatibility**: 1. **Breaking Change Approach**:
- No Backward compatibility. Remove old names. - 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
### Module Implementation Order 2. **Package Structure**:
- `@push.rocks/smartproxy` package will be updated to v14.0.0
- Legacy code fully removed, only route-based API exposed
- Support documentation provided for migration
1. Core module 3. **Migration Documentation**:
2. Certificate module - Provide a migration guide with examples
3. Forwarding module - Show equivalent route configurations for common legacy patterns
4. Proxy implementations - Offer code transformation helpers for complex setups
5. HTTP & TLS modules
6. Main exports and entry points
### Testing Strategy ## Benefits of the Clean Approach
For each module: 1. **Reduced Complexity**:
1. Rename interfaces and types - No overlapping or conflicting configuration systems
2. Add backward compatibility aliases - No dual maintenance of backward compatibility code
3. Update imports throughout the module - Simplified internal architecture
4. Run tests to verify functionality
5. Commit changes module by module
## File-Specific Changes 2. **Cleaner Code Base**:
- Removal of technical debt
- Better separation of concerns
- More maintainable codebase
### Core Module Files 3. **Better User Experience**:
- `ts/core/models/common-types.ts` - Primary interfaces - Consistent, predictable API
- `ts/core/utils/validation-utils.ts` - Validation type definitions - No confusing overlapping options
- `ts/core/utils/ip-utils.ts` - IP utility type definitions - Clear documentation of one approach, not two
- `ts/core/utils/event-utils.ts` - Event type definitions
### Certificate Module Files 4. **Future-Proof Design**:
- `ts/certificate/models/certificate-types.ts` - Certificate interfaces - Easier to extend with new features
- `ts/certificate/acme/acme-factory.ts` - ACME factory types - Better performance without legacy overhead
- `ts/certificate/providers/cert-provisioner.ts` - Provider interfaces - Cleaner foundation for future enhancements
- `ts/certificate/storage/file-storage.ts` - Storage interfaces
### Forwarding Module Files ## Migration Support
- `ts/forwarding/config/forwarding-types.ts` - Forwarding interfaces and types
- `ts/forwarding/config/domain-config.ts` - Domain configuration
- `ts/forwarding/factory/forwarding-factory.ts` - Factory interfaces
- `ts/forwarding/handlers/*.ts` - Handler interfaces
### Proxy Module Files While we're removing backward compatibility from the codebase, we'll provide extensive migration support:
- `ts/proxies/network-proxy/models/types.ts` - NetworkProxy interfaces
- `ts/proxies/smart-proxy/models/interfaces.ts` - SmartProxy interfaces
- `ts/proxies/nftables-proxy/models/interfaces.ts` - NfTables interfaces
- `ts/proxies/smart-proxy/connection-manager.ts` - Connection types
### HTTP/TLS Module Files 1. **Migration Guide**:
- `ts/http/models/http-types.ts` - HTTP module interfaces - Detailed documentation on moving from legacy to route-based config
- `ts/http/port80/acme-interfaces.ts` - ACME interfaces - Pattern-matching examples for all common use cases
- `ts/tls/sni/client-hello-parser.ts` - TLS parser types - Troubleshooting guide for common migration issues
- `ts/tls/alerts/tls-alert.ts` - TLS alert interfaces
## Success Criteria 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
- All interfaces are prefixed with "I" 3. **Version Policy**:
- All type aliases are prefixed with "T" - Maintain the legacy version (13.x) for security updates
- All tests pass with new naming conventions - Make the route-based version a clear major version change (14.0.0)
- Documentation is updated with new naming conventions - Clearly communicate the breaking changes
- Backward compatibility is maintained through type aliases
- Declaration files correctly export both naming conventions ## 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 { loadTestCertificates } 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).toEqual(8080);
expect(httpRoute.match.domains).toEqual('example.com');
expect(httpRoute.action.type).toEqual('forward');
expect(httpRoute.action.target?.host).toEqual('localhost');
expect(httpRoute.action.target?.port).toEqual(3000);
expect(httpRoute.name).toEqual('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).toEqual(443); // Default HTTPS port
expect(httpsRoute.match.domains).toEqual('secure.example.com');
expect(httpsRoute.action.type).toEqual('forward');
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
expect(httpsRoute.action.tls?.certificate).toEqual('auto');
expect(httpsRoute.action.target?.host).toEqual('localhost');
expect(httpsRoute.action.target?.port).toEqual(8080);
expect(httpsRoute.name).toEqual('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).toEqual(80);
expect(redirectRoute.match.domains).toEqual('example.com');
expect(redirectRoute.action.type).toEqual('redirect');
expect(redirectRoute.action.redirect?.to).toEqual('https://{domain}{path}');
expect(redirectRoute.action.redirect?.status).toEqual(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).toEqual(2);
// Validate HTTPS route
const httpsRoute = routes[0];
expect(httpsRoute.match.ports).toEqual(443);
expect(httpsRoute.match.domains).toEqual('example.com');
expect(httpsRoute.action.type).toEqual('forward');
expect(httpsRoute.action.tls?.mode).toEqual('terminate');
// Validate HTTP redirect route
const redirectRoute = routes[1];
expect(redirectRoute.match.ports).toEqual(80);
expect(redirectRoute.action.type).toEqual('redirect');
expect(redirectRoute.action.redirect?.to).toEqual('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).toEqual('app.example.com');
expect(lbRoute.action.type).toEqual('forward');
expect(Array.isArray(lbRoute.action.target?.host)).toBeTrue();
expect((lbRoute.action.target?.host as string[]).length).toEqual(3);
expect((lbRoute.action.target?.host as string[])[0]).toEqual('10.0.0.1');
expect(lbRoute.action.target?.port).toEqual(8080);
expect(lbRoute.action.tls?.mode).toEqual('terminate');
});
tap.test('SmartProxy: Should create instance with route-based config', async () => {
// Create TLS certificates for testing
const certs = loadTestCertificates();
// 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: certs.privateKey,
cert: certs.publicKey
},
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(typeof proxy).toEqual('object');
expect(typeof proxy.start).toEqual('function');
expect(typeof proxy.stop).toEqual('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

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '13.1.2', version: '15.0.0',
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.'
} }

View File

@ -3,6 +3,8 @@ import type { IDomainConfig, ISmartProxyOptions } from './models/interfaces.js';
import type { TForwardingType, IForwardConfig } from '../../forwarding/config/forwarding-types.js'; import type { TForwardingType, IForwardConfig } from '../../forwarding/config/forwarding-types.js';
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js'; import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js'; import { ForwardingHandlerFactory } from '../../forwarding/factory/forwarding-factory.js';
import type { IRouteConfig } from './models/route-types.js';
import { RouteManager } from './route-manager.js';
/** /**
* Manages domain configurations and target selection * Manages domain configurations and target selection
@ -14,13 +16,112 @@ export class DomainConfigManager {
// Cache forwarding handlers for each domain config // Cache forwarding handlers for each domain config
private forwardingHandlers: Map<IDomainConfig, ForwardingHandler> = new Map(); private forwardingHandlers: Map<IDomainConfig, ForwardingHandler> = new Map();
constructor(private settings: ISmartProxyOptions) {} // Store derived domain configs from routes
private derivedDomainConfigs: IDomainConfig[] = [];
// Reference to RouteManager for route-based configuration
private routeManager?: RouteManager;
constructor(private settings: ISmartProxyOptions) {
// Initialize with derived domain configs if using route-based configuration
if (settings.routes && !settings.domainConfigs) {
this.generateDomainConfigsFromRoutes();
}
}
/**
* Set the route manager reference for route-based queries
*/
public setRouteManager(routeManager: RouteManager): void {
this.routeManager = routeManager;
// Regenerate domain configs from routes if needed
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
this.generateDomainConfigsFromRoutes();
}
}
/**
* Generate domain configs from routes
*/
public generateDomainConfigsFromRoutes(): void {
this.derivedDomainConfigs = [];
if (!this.settings.routes) return;
for (const route of this.settings.routes) {
if (route.action.type !== 'forward' || !route.match.domains) continue;
// Convert route to domain config
const domainConfig = this.routeToDomainConfig(route);
if (domainConfig) {
this.derivedDomainConfigs.push(domainConfig);
}
}
}
/**
* Convert a route to a domain config
*/
private routeToDomainConfig(route: IRouteConfig): IDomainConfig | null {
if (route.action.type !== 'forward' || !route.action.target) return null;
// Get domains from route
const domains = Array.isArray(route.match.domains) ?
route.match.domains :
(route.match.domains ? [route.match.domains] : []);
if (domains.length === 0) return null;
// Determine forwarding type based on TLS mode
let forwardingType: TForwardingType = 'http-only';
if (route.action.tls) {
switch (route.action.tls.mode) {
case 'passthrough':
forwardingType = 'https-passthrough';
break;
case 'terminate':
forwardingType = 'https-terminate-to-http';
break;
case 'terminate-and-reencrypt':
forwardingType = 'https-terminate-to-https';
break;
}
}
// Create domain config
return {
domains,
forwarding: {
type: forwardingType,
target: {
host: route.action.target.host,
port: route.action.target.port
},
security: route.action.security ? {
allowedIps: route.action.security.allowedIps,
blockedIps: route.action.security.blockedIps,
maxConnections: route.action.security.maxConnections
} : undefined,
https: route.action.tls && route.action.tls.certificate !== 'auto' ? {
customCert: route.action.tls.certificate
} : undefined,
advanced: route.action.advanced
}
};
}
/** /**
* Updates the domain configurations * Updates the domain configurations
*/ */
public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void { public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void {
// If we're using domainConfigs property, update it
if (this.settings.domainConfigs) {
this.settings.domainConfigs = newDomainConfigs; this.settings.domainConfigs = newDomainConfigs;
} else {
// Otherwise update our derived configs
this.derivedDomainConfigs = newDomainConfigs;
}
// Reset target indices for removed configs // Reset target indices for removed configs
const currentConfigSet = new Set(newDomainConfigs); const currentConfigSet = new Set(newDomainConfigs);
@ -60,7 +161,8 @@ export class DomainConfigManager {
* Get all domain configurations * Get all domain configurations
*/ */
public getDomainConfigs(): IDomainConfig[] { public getDomainConfigs(): IDomainConfig[] {
return this.settings.domainConfigs; // Use domainConfigs from settings if available, otherwise use derived configs
return this.settings.domainConfigs || this.derivedDomainConfigs;
} }
/** /**
@ -69,23 +171,64 @@ export class DomainConfigManager {
public findDomainConfig(serverName: string): IDomainConfig | undefined { public findDomainConfig(serverName: string): IDomainConfig | undefined {
if (!serverName) return undefined; if (!serverName) return undefined;
return this.settings.domainConfigs.find((config) => // Get domain configs from the appropriate source
config.domains.some((d) => plugins.minimatch(serverName, d)) const domainConfigs = this.getDomainConfigs();
);
// Check for direct match
for (const config of domainConfigs) {
if (config.domains.some(d => plugins.minimatch(serverName, d))) {
return config;
}
}
// No match found
return undefined;
} }
/** /**
* Find domain config for a specific port * Find domain config for a specific port
*/ */
public findDomainConfigForPort(port: number): IDomainConfig | undefined { public findDomainConfigForPort(port: number): IDomainConfig | undefined {
return this.settings.domainConfigs.find( // Get domain configs from the appropriate source
(domain) => { const domainConfigs = this.getDomainConfigs();
// Check if any domain config has a matching port range
for (const domain of domainConfigs) {
const portRanges = domain.forwarding?.advanced?.portRanges; const portRanges = domain.forwarding?.advanced?.portRanges;
return portRanges && if (portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges)) {
portRanges.length > 0 && return domain;
this.isPortInRanges(port, portRanges);
} }
); }
// If we're in route-based mode, also check routes for this port
if (this.settings.routes && (!this.settings.domainConfigs || this.settings.domainConfigs.length === 0)) {
const routesForPort = this.settings.routes.filter(route => {
// Check if this port is in the route's ports
if (typeof route.match.ports === 'number') {
return route.match.ports === port;
} else if (Array.isArray(route.match.ports)) {
return route.match.ports.some(p => {
if (typeof p === 'number') {
return p === port;
} else if (p.from && p.to) {
return port >= p.from && port <= p.to;
}
return false;
});
}
return false;
});
// If we found any routes for this port, convert the first one to a domain config
if (routesForPort.length > 0 && routesForPort[0].action.type === 'forward') {
const domainConfig = this.routeToDomainConfig(routesForPort[0]);
if (domainConfig) {
return domainConfig;
}
}
}
return undefined;
} }
/** /**

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,103 @@ 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 configuration types
*/
export function isLegacyOptions(options: any): boolean {
return !!(options.domainConfigs && options.domainConfigs.length > 0 &&
(!options.routes || options.routes.length === 0));
}
export function isRoutedOptions(options: any): boolean {
return !!(options.routes && options.routes.length > 0);
}
/**
* 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 +128,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 +184,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 +195,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,26 @@ export class SmartProxy extends plugins.EventEmitter {
this.securityManager, this.securityManager,
this.timeoutManager this.timeoutManager
); );
// Create the new route manager first
this.routeManager = new RouteManager(this.settings);
// Create domain config manager and port range manager
this.domainConfigManager = new DomainConfigManager(this.settings); this.domainConfigManager = new DomainConfigManager(this.settings);
this.tlsManager = new TlsManager(this.settings);
this.networkProxyBridge = new NetworkProxyBridge(this.settings); // Share the route manager with the domain config manager
if (typeof this.domainConfigManager.setRouteManager === 'function') {
this.domainConfigManager.setRouteManager(this.routeManager);
}
this.portRangeManager = new PortRangeManager(this.settings); this.portRangeManager = new PortRangeManager(this.settings);
// Initialize connection handler // Create other required components
this.connectionHandler = new ConnectionHandler( 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 +139,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 +162,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 +175,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 +184,17 @@ export class SmartProxy extends plugins.EventEmitter {
return; return;
} }
// Process domain configs // Initialize domain config based on configuration type
// Note: ensureForwardingConfig is no longer needed since forwarding is now required if (isLegacyOptions(this.settings)) {
// Initialize domain config manager with the legacy domain configs
// Initialize domain config manager with the processed configs this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs || []);
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs); } else if (isRoutedOptions(this.settings)) {
// For pure route-based configuration, the domain config is already initialized
// in the constructor, but we might need to regenerate it
if (typeof this.domainConfigManager.generateDomainConfigsFromRoutes === 'function') {
this.domainConfigManager.generateDomainConfigsFromRoutes();
}
}
// Initialize Port80Handler if enabled // Initialize Port80Handler if enabled
await this.initializePort80Handler(); await this.initializePort80Handler();
@ -176,9 +203,10 @@ 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)) {
// If using legacy mode, check if domain config exists
const domainConfig = this.settings.domainConfigs.find( const domainConfig = this.settings.domainConfigs.find(
dc => dc.domains.some(d => d === f.domain) dc => dc.domains.some(d => d === f.domain)
); );
@ -191,6 +219,24 @@ export class SmartProxy extends plugins.EventEmitter {
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false 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,
clientIp: '127.0.0.1' // Dummy IP for finding routes
})?.route;
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
return { return {
@ -201,6 +247,8 @@ export class SmartProxy extends plugins.EventEmitter {
}; };
}) || []; }) || [];
// Create CertProvisioner with appropriate parameters
if (isLegacyOptions(this.settings)) {
this.certProvisioner = new CertProvisioner( this.certProvisioner = new CertProvisioner(
this.settings.domainConfigs, this.settings.domainConfigs,
this.port80Handler, this.port80Handler,
@ -211,7 +259,26 @@ export class SmartProxy extends plugins.EventEmitter {
acme.autoRenew!, acme.autoRenew!,
domainForwards 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 +295,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 +322,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 +332,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 +414,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 +535,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 +555,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 +617,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 +799,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,9 +808,34 @@ 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[] = [];
// 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
const eligibleDomains = routeDomains.filter(domain =>
!domain.includes('*') && this.isValidDomain(domain)
);
domains.push(...eligibleDomains);
}
// For legacy mode, also get domains from domain configs
if (isLegacyOptions(this.settings)) {
for (const config of this.settings.domainConfigs) { for (const config of this.settings.domainConfigs) {
// 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 = config.domains.filter(domain =>
@ -602,6 +844,7 @@ export class SmartProxy extends plugins.EventEmitter {
domains.push(...eligibleDomains); domains.push(...eligibleDomains);
} }
}
return domains; return domains;
} }