Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
18f03c1acf | |||
200635e4bd | |||
95c5c1b90d | |||
bb66b98f1d | |||
28022ebe87 | |||
552f4c246b |
@ -1,5 +1,14 @@
|
|||||||
# 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)
|
## 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.
|
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.
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "13.1.3",
|
"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",
|
||||||
|
467
readme.plan.md
467
readme.plan.md
@ -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 }>;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
// What to do with matched traffic
|
||||||
|
action: {
|
||||||
|
// Basic routing
|
||||||
|
type: 'forward' | 'redirect' | 'block';
|
||||||
|
|
||||||
|
// Target for forwarding
|
||||||
|
target?: {
|
||||||
|
host: string | string[]; // Support single host or round-robin
|
||||||
|
port: number;
|
||||||
|
preservePort?: boolean; // Use incoming port as target port
|
||||||
|
};
|
||||||
|
|
||||||
|
// TLS handling
|
||||||
|
tls?: {
|
||||||
|
mode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
certificate?: 'auto' | { // Auto = use ACME
|
||||||
|
key: string;
|
||||||
|
cert: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// For redirects
|
||||||
|
redirect?: {
|
||||||
|
to: string; // URL or template with {domain}, {port}, etc.
|
||||||
|
status: 301 | 302 | 307 | 308;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Security options
|
||||||
|
security?: {
|
||||||
|
allowedIps?: string[];
|
||||||
|
blockedIps?: string[];
|
||||||
|
maxConnections?: number;
|
||||||
|
authentication?: {
|
||||||
|
type: 'basic' | 'digest' | 'oauth';
|
||||||
|
// Auth-specific options
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Advanced options
|
||||||
|
advanced?: {
|
||||||
|
timeout?: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
keepAlive?: boolean;
|
||||||
|
// etc.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 forwarding configuration interfaces
|
// Main SmartProxy options
|
||||||
- [ ] Rename interfaces in `ts/forwarding/config/forwarding-types.ts`
|
interface ISmartProxyOptions {
|
||||||
- [ ] `TargetConfig` → `ITargetConfig`
|
// The unified configuration array (required)
|
||||||
- [ ] `HttpOptions` → `IHttpOptions`
|
routes: IRouteConfig[];
|
||||||
- [ ] `HttpsOptions` → `IHttpsOptions`
|
|
||||||
- [ ] `AcmeForwardingOptions` → `IAcmeForwardingOptions`
|
// Global/default settings
|
||||||
- [ ] `SecurityOptions` → `ISecurityOptions`
|
defaults?: {
|
||||||
- [ ] `AdvancedOptions` → `IAdvancedOptions`
|
target?: {
|
||||||
- [ ] `ForwardConfig` → `IForwardConfig`
|
host: string;
|
||||||
- [ ] Rename type definitions
|
port: number;
|
||||||
- [ ] `ForwardingType` → `TForwardingType`
|
};
|
||||||
- [ ] Update domain configuration interfaces
|
security?: {
|
||||||
|
// Global security defaults
|
||||||
|
};
|
||||||
|
tls?: {
|
||||||
|
// Global TLS defaults
|
||||||
|
};
|
||||||
|
// ...other defaults
|
||||||
|
};
|
||||||
|
|
||||||
|
// Other global settings remain (acme, etc.)
|
||||||
|
acme?: IAcmeOptions;
|
||||||
|
|
||||||
|
// Advanced settings remain as well
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] Standardize handler interfaces
|
## Revised Implementation Plan
|
||||||
- [ ] Update base handler interfaces
|
|
||||||
- [ ] Rename handler-specific interfaces
|
|
||||||
- [ ] Update factory interfaces
|
|
||||||
|
|
||||||
- [ ] Verify forwarding system functionality
|
### Phase 1: Core Design & Interface Definition
|
||||||
- [ ] Test all forwarding types
|
|
||||||
- [ ] Verify configuration parsing
|
|
||||||
- [ ] Ensure backward compatibility
|
|
||||||
|
|
||||||
## Phase 5: Proxy Implementation Standardization
|
1. **Define New Core Interfaces**:
|
||||||
|
- Create `IRouteConfig` interface with `match` and `action` branches
|
||||||
|
- Define all sub-interfaces for matching and actions
|
||||||
|
- Create new `ISmartProxyOptions` to use `routes` array exclusively
|
||||||
|
- Define template variable system for dynamic values
|
||||||
|
|
||||||
- [ ] Update SmartProxy interfaces
|
2. **Create Helper Functions**:
|
||||||
- [ ] Rename interfaces in `ts/proxies/smart-proxy/models/interfaces.ts`
|
- `createRoute()` - Basic route creation with reasonable defaults
|
||||||
- [ ] Update domain configuration interfaces
|
- `createHttpRoute()`, `createHttpsRoute()`, `createRedirect()` - Common scenarios
|
||||||
- [ ] Standardize manager interfaces
|
- `createLoadBalancer()` - For multi-target setups
|
||||||
|
- `mergeSecurity()`, `mergeDefaults()` - For combining configs
|
||||||
|
|
||||||
- [ ] Update NetworkProxy interfaces
|
3. **Design Router**:
|
||||||
- [ ] Rename in `ts/proxies/network-proxy/models/types.ts`
|
- Decision tree for route matching algorithm
|
||||||
- [ ] `NetworkProxyOptions` → `INetworkProxyOptions`
|
- Priority system for route ordering
|
||||||
- [ ] `CertificateEntry` → `ICertificateEntry`
|
- Optimized lookup strategy for fast routing
|
||||||
- [ ] `ReverseProxyConfig` → `IReverseProxyConfig`
|
|
||||||
- [ ] `ConnectionEntry` → `IConnectionEntry`
|
|
||||||
- [ ] `WebSocketWithHeartbeat` → `IWebSocketWithHeartbeat`
|
|
||||||
- [ ] `Logger` → `ILogger`
|
|
||||||
- [ ] Update request handler interfaces
|
|
||||||
- [ ] Standardize connection interfaces
|
|
||||||
|
|
||||||
- [ ] Update NfTablesProxy interfaces
|
### Phase 2: Core Implementation
|
||||||
- [ ] Rename interfaces in `ts/proxies/nftables-proxy/models/interfaces.ts`
|
|
||||||
- [ ] Update configuration interfaces
|
|
||||||
- [ ] Standardize firewall rule interfaces
|
|
||||||
|
|
||||||
- [ ] Test proxy implementations
|
1. **Create RouteManager**:
|
||||||
- [ ] Verify SmartProxy functionality
|
- Build a new RouteManager to replace both PortRangeManager and DomainConfigManager
|
||||||
- [ ] Test NetworkProxy with renamed interfaces
|
- Implement port and domain matching in one unified system
|
||||||
- [ ] Validate NfTablesProxy operations
|
- Create efficient route lookup algorithms
|
||||||
|
|
||||||
## Phase 6: HTTP & TLS Module Standardization
|
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
|
||||||
|
|
||||||
- [ ] Update HTTP interfaces
|
3. **Implement New SmartProxy Core**:
|
||||||
- [ ] Rename in `ts/http/port80/acme-interfaces.ts`
|
- Create new SmartProxy implementation using routes exclusively
|
||||||
- [ ] `SmartAcmeCert` → `ISmartAcmeCert`
|
- Build network servers based on port specifications
|
||||||
- [ ] `SmartAcmeOptions` → `ISmartAcmeOptions`
|
- Manage TLS contexts and certificates
|
||||||
- [ ] `Http01Challenge` → `IHttp01Challenge`
|
|
||||||
- [ ] `SmartAcme` → `ISmartAcme`
|
|
||||||
- [ ] Standardize router interfaces
|
|
||||||
- [ ] Update port80 handler interfaces
|
|
||||||
- [ ] Update redirect interfaces
|
|
||||||
|
|
||||||
- [ ] Update TLS/SNI interfaces
|
### Phase 3: Legacy Code Removal
|
||||||
- [ ] Standardize SNI handler interfaces
|
|
||||||
- [ ] Update client hello parser types
|
|
||||||
- [ ] Rename TLS alert interfaces
|
|
||||||
|
|
||||||
- [ ] Test HTTP & TLS functionality
|
1. **Identify Legacy Components**:
|
||||||
- [ ] Verify router operation
|
- Create an inventory of all files and components to be removed
|
||||||
- [ ] Test SNI extraction
|
- Document dependencies between legacy components
|
||||||
- [ ] Validate redirect functionality
|
- Create a removal plan that minimizes disruption
|
||||||
|
|
||||||
## Phase 7: Backward Compatibility Layer
|
2. **Remove Legacy Components**:
|
||||||
|
- Remove PortRangeManager and related code
|
||||||
|
- Remove DomainConfigManager and related code
|
||||||
|
- Remove old ConnectionHandler implementation
|
||||||
|
- Remove other legacy components
|
||||||
|
|
||||||
- [ ] Implement comprehensive type aliases
|
3. **Clean Interface Adaptations**:
|
||||||
- [ ] Create aliases for all renamed interfaces
|
- Remove all legacy interfaces and types
|
||||||
- [ ] Add deprecation notices via JSDoc
|
- Update type exports to only expose route-based interfaces
|
||||||
- [ ] Ensure all exports include both named versions
|
- Remove any adapter or backward compatibility code
|
||||||
|
|
||||||
- [ ] Update main entry point
|
### Phase 4: Updated Documentation & Examples
|
||||||
- [ ] Update `ts/index.ts` with all exports
|
|
||||||
- [ ] Include both prefixed and non-prefixed names
|
|
||||||
- [ ] Organize exports by module
|
|
||||||
|
|
||||||
- [ ] Add compatibility documentation
|
1. **Update Core Documentation**:
|
||||||
- [ ] Document renaming strategy
|
- Rewrite README.md with a focus on route-based configuration exclusively
|
||||||
- [ ] Provide migration examples
|
- Create interface reference documentation
|
||||||
- [ ] Create deprecation timeline
|
- Document all template variables
|
||||||
|
|
||||||
## Phase 8: Documentation & Examples
|
2. **Create Example Library**:
|
||||||
|
- Common configuration patterns using the new API
|
||||||
|
- Complex use cases for advanced features
|
||||||
|
- Infrastructure-as-code examples
|
||||||
|
|
||||||
- [ ] Update README and API documentation
|
3. **Add Validation Tools**:
|
||||||
- [ ] Update interface references in README.md
|
- Configuration validator to check for issues
|
||||||
- [ ] Document naming convention in README.md
|
- Schema definitions for IDE autocomplete
|
||||||
- [ ] Update API reference documentation
|
- Runtime validation helpers
|
||||||
|
|
||||||
- [ ] Update examples
|
### Phase 5: Testing
|
||||||
- [ ] Modify example code to use new interface names
|
|
||||||
- [ ] Add compatibility notes
|
|
||||||
- [ ] Create migration examples
|
|
||||||
|
|
||||||
- [ ] Add contributor guidelines
|
1. **Unit Tests**:
|
||||||
- [ ] Document naming conventions
|
- Test route matching logic
|
||||||
- [ ] Add interface/type style guide
|
- Validate priority handling
|
||||||
- [ ] Update PR templates
|
- Test template processing
|
||||||
|
|
||||||
## Phase 9: Testing & Validation
|
2. **Integration Tests**:
|
||||||
|
- Verify full proxy flows with the new system
|
||||||
|
- Test complex routing scenarios
|
||||||
|
- Ensure all features work as expected
|
||||||
|
|
||||||
- [ ] Run comprehensive test suite
|
3. **Performance Testing**:
|
||||||
- [ ] Run all unit tests
|
- Benchmark routing performance
|
||||||
- [ ] Execute integration tests
|
- Evaluate memory usage
|
||||||
- [ ] Verify example code
|
- Test with large numbers of routes
|
||||||
|
|
||||||
- [ ] Build type declarations
|
|
||||||
- [ ] Generate TypeScript declaration files
|
|
||||||
- [ ] Verify exported types
|
|
||||||
- [ ] Validate documentation generation
|
|
||||||
|
|
||||||
- [ ] Final compatibility check
|
|
||||||
- [ ] Verify import compatibility
|
|
||||||
- [ ] Test with existing dependent projects
|
|
||||||
- [ ] Validate backward compatibility claims
|
|
||||||
|
|
||||||
## 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
181
test/test.route-config.ts
Normal 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();
|
@ -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
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '13.1.3',
|
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.'
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
this.settings.domainConfigs = newDomainConfigs;
|
// If we're using domainConfigs property, update it
|
||||||
|
if (this.settings.domainConfigs) {
|
||||||
|
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();
|
||||||
const portRanges = domain.forwarding?.advanced?.portRanges;
|
|
||||||
return portRanges &&
|
// Check if any domain config has a matching port range
|
||||||
portRanges.length > 0 &&
|
for (const domain of domainConfigs) {
|
||||||
this.isPortInRanges(port, portRanges);
|
const portRanges = domain.forwarding?.advanced?.portRanges;
|
||||||
|
if (portRanges && portRanges.length > 0 && this.isPortInRanges(port, portRanges)) {
|
||||||
|
return domain;
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
184
ts/proxies/smart-proxy/models/route-types.ts
Normal file
184
ts/proxies/smart-proxy/models/route-types.ts
Normal 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>;
|
||||||
|
}
|
1117
ts/proxies/smart-proxy/route-connection-handler.ts
Normal file
1117
ts/proxies/smart-proxy/route-connection-handler.ts
Normal file
File diff suppressed because it is too large
Load Diff
344
ts/proxies/smart-proxy/route-helpers.ts
Normal file
344
ts/proxies/smart-proxy/route-helpers.ts
Normal 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;
|
||||||
|
}
|
587
ts/proxies/smart-proxy/route-manager.ts
Normal file
587
ts/proxies/smart-proxy/route-manager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,20 +203,39 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
if (this.port80Handler) {
|
if (this.port80Handler) {
|
||||||
const acme = this.settings.acme!;
|
const acme = this.settings.acme!;
|
||||||
|
|
||||||
// Convert domain forwards to use the new forwarding system if possible
|
// Setup domain forwards based on configuration type
|
||||||
const domainForwards = acme.domainForwards?.map(f => {
|
const domainForwards = acme.domainForwards?.map(f => {
|
||||||
// If the domain has a forwarding config in domainConfigs, use that
|
if (isLegacyOptions(this.settings)) {
|
||||||
const domainConfig = this.settings.domainConfigs.find(
|
// If using legacy mode, check if domain config exists
|
||||||
dc => dc.domains.some(d => d === f.domain)
|
const domainConfig = this.settings.domainConfigs.find(
|
||||||
);
|
dc => dc.domains.some(d => d === f.domain)
|
||||||
|
);
|
||||||
|
|
||||||
if (domainConfig?.forwarding) {
|
if (domainConfig?.forwarding) {
|
||||||
return {
|
return {
|
||||||
|
domain: f.domain,
|
||||||
|
forwardConfig: f.forwardConfig,
|
||||||
|
acmeForwardConfig: f.acmeForwardConfig,
|
||||||
|
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In route mode, look for matching route
|
||||||
|
const route = this.routeManager.findMatchingRoute({
|
||||||
|
port: 443,
|
||||||
domain: f.domain,
|
domain: f.domain,
|
||||||
forwardConfig: f.forwardConfig,
|
clientIp: '127.0.0.1' // Dummy IP for finding routes
|
||||||
acmeForwardConfig: f.acmeForwardConfig,
|
})?.route;
|
||||||
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
|
|
||||||
};
|
if (route && route.action.type === 'forward' && route.action.tls) {
|
||||||
|
// If we found a matching route with TLS settings
|
||||||
|
return {
|
||||||
|
domain: f.domain,
|
||||||
|
forwardConfig: f.forwardConfig,
|
||||||
|
acmeForwardConfig: f.acmeForwardConfig,
|
||||||
|
sslRedirect: f.sslRedirect || false
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise use the existing configuration
|
// Otherwise use the existing configuration
|
||||||
@ -201,17 +247,38 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
this.certProvisioner = new CertProvisioner(
|
// Create CertProvisioner with appropriate parameters
|
||||||
this.settings.domainConfigs,
|
if (isLegacyOptions(this.settings)) {
|
||||||
this.port80Handler,
|
this.certProvisioner = new CertProvisioner(
|
||||||
this.networkProxyBridge,
|
this.settings.domainConfigs,
|
||||||
this.settings.certProvisionFunction,
|
this.port80Handler,
|
||||||
acme.renewThresholdDays!,
|
this.networkProxyBridge,
|
||||||
acme.renewCheckIntervalHours!,
|
this.settings.certProvisionFunction,
|
||||||
acme.autoRenew!,
|
acme.renewThresholdDays!,
|
||||||
domainForwards
|
acme.renewCheckIntervalHours!,
|
||||||
);
|
acme.autoRenew!,
|
||||||
|
domainForwards
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// For route-based configuration, we need to adapt the interface
|
||||||
|
// Convert routes to domain configs for CertProvisioner
|
||||||
|
const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes(
|
||||||
|
(this.settings as IRoutedSmartProxyOptions).routes
|
||||||
|
);
|
||||||
|
|
||||||
|
this.certProvisioner = new CertProvisioner(
|
||||||
|
domainConfigs,
|
||||||
|
this.port80Handler,
|
||||||
|
this.networkProxyBridge,
|
||||||
|
this.settings.certProvisionFunction,
|
||||||
|
acme.renewThresholdDays!,
|
||||||
|
acme.renewCheckIntervalHours!,
|
||||||
|
acme.autoRenew!,
|
||||||
|
domainForwards
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register certificate event handler
|
||||||
this.certProvisioner.on('certificate', (certData) => {
|
this.certProvisioner.on('certificate', (certData) => {
|
||||||
this.emit('certificate', {
|
this.emit('certificate', {
|
||||||
domain: certData.domain,
|
domain: certData.domain,
|
||||||
@ -228,25 +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,18 +808,44 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
* Get a list of eligible domains for ACME certificates
|
* Get a list of eligible domains for ACME certificates
|
||||||
*/
|
*/
|
||||||
public getEligibleDomainsForCertificates(): string[] {
|
public getEligibleDomainsForCertificates(): string[] {
|
||||||
// Collect all non-wildcard domains from domain configs
|
|
||||||
const domains: string[] = [];
|
const domains: string[] = [];
|
||||||
|
|
||||||
for (const config of this.settings.domainConfigs) {
|
// Get domains from routes
|
||||||
|
const routes = isRoutedOptions(this.settings) ? this.settings.routes : [];
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
if (!route.match.domains) continue;
|
||||||
|
|
||||||
|
// Skip routes without TLS termination or auto certificates
|
||||||
|
if (route.action.type !== 'forward' ||
|
||||||
|
!route.action.tls ||
|
||||||
|
route.action.tls.mode === 'passthrough' ||
|
||||||
|
route.action.tls.certificate !== 'auto') continue;
|
||||||
|
|
||||||
|
const routeDomains = Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains
|
||||||
|
: [route.match.domains];
|
||||||
|
|
||||||
// Skip domains that can't be used with ACME
|
// Skip domains that can't be used with ACME
|
||||||
const eligibleDomains = config.domains.filter(domain =>
|
const eligibleDomains = routeDomains.filter(domain =>
|
||||||
!domain.includes('*') && this.isValidDomain(domain)
|
!domain.includes('*') && this.isValidDomain(domain)
|
||||||
);
|
);
|
||||||
|
|
||||||
domains.push(...eligibleDomains);
|
domains.push(...eligibleDomains);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For legacy mode, also get domains from domain configs
|
||||||
|
if (isLegacyOptions(this.settings)) {
|
||||||
|
for (const config of this.settings.domainConfigs) {
|
||||||
|
// Skip domains that can't be used with ACME
|
||||||
|
const eligibleDomains = config.domains.filter(domain =>
|
||||||
|
!domain.includes('*') && this.isValidDomain(domain)
|
||||||
|
);
|
||||||
|
|
||||||
|
domains.push(...eligibleDomains);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user