Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
455858af0d | |||
b4a0e4be6b | |||
36bea96ac7 | |||
529857220d | |||
3596d35f45 | |||
8dd222443d | |||
18f03c1acf | |||
200635e4bd | |||
95c5c1b90d | |||
bb66b98f1d | |||
28022ebe87 | |||
552f4c246b | |||
09fc71f051 | |||
e508078ecf | |||
7f614584b8 | |||
e1a25b749c | |||
c34462b781 | |||
f8647516b5 |
39
changelog.md
39
changelog.md
@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-10 - 15.1.0 - feat(smartproxy)
|
||||||
|
Update documentation and route helper functions; add createPortRange, createSecurityConfig, createStaticFileRoute, and createTestRoute helpers to the readme and tests. Refactor test examples to use the new helper API and remove legacy connection handling files (including the old connection handler and PortRangeManager) to fully embrace the unified route‐based configuration.
|
||||||
|
|
||||||
|
- Added new helper functions (createPortRange, createSecurityConfig, createStaticFileRoute, createTestRoute) in readme and route helpers.
|
||||||
|
- Refactored tests (test.forwarding.examples.ts, test.forwarding.unit.ts, etc.) to update references to the new API.
|
||||||
|
- Removed legacy connection handler and PortRangeManager files to simplify code and align with route‐based configuration.
|
||||||
|
|
||||||
|
## 2025-05-10 - 15.0.0 - BREAKING CHANGE(documentation)
|
||||||
|
Update readme documentation to comprehensively describe the new unified route-based configuration system in v14.0.0
|
||||||
|
|
||||||
|
- Added detailed description of IRouteConfig, IRouteMatch, and IRouteAction interfaces
|
||||||
|
- Improved explanation of port, domain, path, client IP, and TLS version matching features
|
||||||
|
- Included examples of helper functions (createHttpRoute, createHttpsRoute, etc.) with usage of template variables
|
||||||
|
- Enhanced migration guide from legacy configurations to the new match/action pattern
|
||||||
|
- Updated examples and tests to reflect the new documentation structure
|
||||||
|
|
||||||
|
## 2025-05-09 - 13.1.3 - fix(documentation)
|
||||||
|
Update readme.md to provide a unified and comprehensive overview of SmartProxy, with reorganized sections, enhanced diagrams, and detailed usage examples for various proxy scenarios.
|
||||||
|
|
||||||
|
- Reorganized key sections to clearly list Primary API, Helper Functions, Specialized Components, and Core Utilities.
|
||||||
|
- Added detailed Quick Start examples covering API Gateway, automatic HTTPS, load balancing, wildcard subdomain support, and comprehensive proxy server setups.
|
||||||
|
- Included updated architecture flow diagrams and explanations of Unified Forwarding System and ACME integration.
|
||||||
|
- Improved clarity and consistency across documentation, with revised formatting and expanded descriptions.
|
||||||
|
|
||||||
|
## 2025-05-09 - 13.1.2 - fix(docs)
|
||||||
|
Update readme to reflect updated interface and type naming conventions
|
||||||
|
|
||||||
|
- Changed 'Interfaces' section to 'Interfaces and Types' with updated file references
|
||||||
|
- Replaced 'SmartProxyOptions', 'AcmeOptions', 'ForwardConfig' with their new names 'ISmartProxyOptions', 'IAcmeOptions', 'IForwardConfig', etc.
|
||||||
|
- Clarified API reference and project architecture documentation
|
||||||
|
|
||||||
|
## 2025-05-09 - 13.1.1 - fix(typescript)
|
||||||
|
Refactor types and interfaces to use consistent 'I' prefix and update related tests
|
||||||
|
|
||||||
|
- Replaced DomainConfig with IDomainConfig and SmartProxyOptions with ISmartProxyOptions in various modules
|
||||||
|
- Renamed SmartProxyCertProvisionObject to TSmartProxyCertProvisionObject for clarity
|
||||||
|
- Standardized type names (e.g. ForwardConfig → IForwardConfig, Logger → ILogger) across proxy, forwarding, and certificate modules
|
||||||
|
- Updated tests and helper functions to reflect new type names and ensure compatibility
|
||||||
|
|
||||||
## 2025-05-09 - 13.1.0 - feat(docs)
|
## 2025-05-09 - 13.1.0 - feat(docs)
|
||||||
Update README to reflect new modular architecture and expanded core utilities: add Project Architecture Overview, update export paths and API references, and mark plan tasks as completed
|
Update README to reflect new modular architecture and expanded core utilities: add Project Architecture Overview, update export paths and API references, and mark plan tasks as completed
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "13.1.0",
|
"version": "15.1.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
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
|
@ -1,9 +1,10 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import { CertProvisioner } from '../ts/certificate/providers/cert-provisioner.js';
|
import { CertProvisioner } from '../ts/certificate/providers/cert-provisioner.js';
|
||||||
import type { DomainConfig } from '../ts/forwarding/config/forwarding-types.js';
|
import type { IDomainConfig } from '../ts/forwarding/config/domain-config.js';
|
||||||
import type { SmartProxyCertProvisionObject } from '../ts/certificate/models/certificate-types.js';
|
import type { ICertificateData } from '../ts/certificate/models/certificate-types.js';
|
||||||
import type { CertificateData } from '../ts/certificate/models/certificate-types.js';
|
// Import SmartProxyCertProvisionObject type alias
|
||||||
|
import type { TSmartProxyCertProvisionObject } from '../ts/certificate/providers/cert-provisioner.js';
|
||||||
|
|
||||||
// Fake Port80Handler stub
|
// Fake Port80Handler stub
|
||||||
class FakePort80Handler extends plugins.EventEmitter {
|
class FakePort80Handler extends plugins.EventEmitter {
|
||||||
@ -19,15 +20,15 @@ class FakePort80Handler extends plugins.EventEmitter {
|
|||||||
|
|
||||||
// Fake NetworkProxyBridge stub
|
// Fake NetworkProxyBridge stub
|
||||||
class FakeNetworkProxyBridge {
|
class FakeNetworkProxyBridge {
|
||||||
public appliedCerts: CertificateData[] = [];
|
public appliedCerts: ICertificateData[] = [];
|
||||||
applyExternalCertificate(cert: CertificateData) {
|
applyExternalCertificate(cert: ICertificateData) {
|
||||||
this.appliedCerts.push(cert);
|
this.appliedCerts.push(cert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tap.test('CertProvisioner handles static provisioning', async () => {
|
tap.test('CertProvisioner handles static provisioning', async () => {
|
||||||
const domain = 'static.com';
|
const domain = 'static.com';
|
||||||
const domainConfigs: DomainConfig[] = [{
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
domains: [domain],
|
domains: [domain],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'https-terminate-to-https',
|
type: 'https-terminate-to-https',
|
||||||
@ -37,7 +38,7 @@ tap.test('CertProvisioner handles static provisioning', async () => {
|
|||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
// certProvider returns static certificate
|
// certProvider returns static certificate
|
||||||
const certProvider = async (d: string): Promise<SmartProxyCertProvisionObject> => {
|
const certProvider = async (d: string): Promise<TSmartProxyCertProvisionObject> => {
|
||||||
expect(d).toEqual(domain);
|
expect(d).toEqual(domain);
|
||||||
return {
|
return {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
@ -75,7 +76,7 @@ tap.test('CertProvisioner handles static provisioning', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner handles http01 provisioning', async () => {
|
tap.test('CertProvisioner handles http01 provisioning', async () => {
|
||||||
const domain = 'http01.com';
|
const domain = 'http01.com';
|
||||||
const domainConfigs: DomainConfig[] = [{
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
domains: [domain],
|
domains: [domain],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'https-terminate-to-http',
|
type: 'https-terminate-to-http',
|
||||||
@ -85,7 +86,7 @@ tap.test('CertProvisioner handles http01 provisioning', async () => {
|
|||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
// certProvider returns http01 directive
|
// certProvider returns http01 directive
|
||||||
const certProvider = async (): Promise<SmartProxyCertProvisionObject> => 'http01';
|
const certProvider = async (): Promise<TSmartProxyCertProvisionObject> => 'http01';
|
||||||
const prov = new CertProvisioner(
|
const prov = new CertProvisioner(
|
||||||
domainConfigs,
|
domainConfigs,
|
||||||
fakePort80 as any,
|
fakePort80 as any,
|
||||||
@ -106,7 +107,7 @@ tap.test('CertProvisioner handles http01 provisioning', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
||||||
const domain = 'renew.com';
|
const domain = 'renew.com';
|
||||||
const domainConfigs: DomainConfig[] = [{
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
domains: [domain],
|
domains: [domain],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'https-terminate-to-http',
|
type: 'https-terminate-to-http',
|
||||||
@ -115,7 +116,7 @@ tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
|||||||
}];
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
const certProvider = async (): Promise<SmartProxyCertProvisionObject> => 'http01';
|
const certProvider = async (): Promise<TSmartProxyCertProvisionObject> => 'http01';
|
||||||
const prov = new CertProvisioner(
|
const prov = new CertProvisioner(
|
||||||
domainConfigs,
|
domainConfigs,
|
||||||
fakePort80 as any,
|
fakePort80 as any,
|
||||||
@ -132,7 +133,7 @@ tap.test('CertProvisioner on-demand http01 renewal', async () => {
|
|||||||
|
|
||||||
tap.test('CertProvisioner on-demand static provisioning', async () => {
|
tap.test('CertProvisioner on-demand static provisioning', async () => {
|
||||||
const domain = 'ondemand.com';
|
const domain = 'ondemand.com';
|
||||||
const domainConfigs: DomainConfig[] = [{
|
const domainConfigs: IDomainConfig[] = [{
|
||||||
domains: [domain],
|
domains: [domain],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'https-terminate-to-https',
|
type: 'https-terminate-to-https',
|
||||||
@ -141,7 +142,7 @@ tap.test('CertProvisioner on-demand static provisioning', async () => {
|
|||||||
}];
|
}];
|
||||||
const fakePort80 = new FakePort80Handler();
|
const fakePort80 = new FakePort80Handler();
|
||||||
const fakeBridge = new FakeNetworkProxyBridge();
|
const fakeBridge = new FakeNetworkProxyBridge();
|
||||||
const certProvider = async (): Promise<SmartProxyCertProvisionObject> => ({
|
const certProvider = async (): Promise<TSmartProxyCertProvisionObject> => ({
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
publicKey: 'PKEY',
|
publicKey: 'PKEY',
|
||||||
privateKey: 'PRIV',
|
privateKey: 'PRIV',
|
||||||
|
@ -1,112 +1,197 @@
|
|||||||
import * as plugins from '../ts/plugins.js';
|
import * as path from 'path';
|
||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
|
|
||||||
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
|
||||||
import type { DomainConfig } from '../ts/forwarding/config/forwarding-types.js';
|
|
||||||
import type { ForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
|
||||||
import {
|
import {
|
||||||
httpOnly,
|
createHttpRoute,
|
||||||
httpsPassthrough,
|
createHttpsRoute,
|
||||||
tlsTerminateToHttp,
|
createPassthroughRoute,
|
||||||
tlsTerminateToHttps
|
createRedirectRoute,
|
||||||
} from '../ts/forwarding/config/forwarding-types.js';
|
createHttpToHttpsRedirect,
|
||||||
|
createBlockRoute,
|
||||||
|
createLoadBalancerRoute,
|
||||||
|
createHttpsServer,
|
||||||
|
createPortRange,
|
||||||
|
createSecurityConfig,
|
||||||
|
createStaticFileRoute,
|
||||||
|
createTestRoute
|
||||||
|
} from '../ts/proxies/smart-proxy/route-helpers/index.js';
|
||||||
|
import type { IRouteConfig } from '../ts/proxies/smart-proxy/models/route-types.js';
|
||||||
|
|
||||||
// Test to demonstrate various forwarding configurations
|
// Test to demonstrate various route configurations using the new helpers
|
||||||
tap.test('Forwarding configuration examples', async (tools) => {
|
tap.test('Route-based configuration examples', async (tools) => {
|
||||||
// Example 1: HTTP-only configuration
|
// Example 1: HTTP-only configuration
|
||||||
const httpOnlyConfig: DomainConfig = {
|
const httpOnlyRoute = createHttpRoute({
|
||||||
domains: ['http.example.com'],
|
domains: 'http.example.com',
|
||||||
forwarding: httpOnly({
|
target: {
|
||||||
target: {
|
host: 'localhost',
|
||||||
host: 'localhost',
|
port: 3000
|
||||||
port: 3000
|
},
|
||||||
},
|
security: {
|
||||||
security: {
|
allowedIps: ['*'] // Allow all
|
||||||
allowedIps: ['*'] // Allow all
|
},
|
||||||
}
|
name: 'Basic HTTP Route'
|
||||||
})
|
});
|
||||||
};
|
|
||||||
console.log(httpOnlyConfig.forwarding, 'HTTP-only configuration created successfully');
|
|
||||||
expect(httpOnlyConfig.forwarding.type).toEqual('http-only');
|
|
||||||
|
|
||||||
// Example 2: HTTPS Passthrough (SNI)
|
console.log('HTTP-only route created successfully:', httpOnlyRoute.name);
|
||||||
const httpsPassthroughConfig: DomainConfig = {
|
expect(httpOnlyRoute.action.type).toEqual('forward');
|
||||||
domains: ['pass.example.com'],
|
expect(httpOnlyRoute.match.domains).toEqual('http.example.com');
|
||||||
forwarding: httpsPassthrough({
|
|
||||||
target: {
|
// Example 2: HTTPS Passthrough (SNI) configuration
|
||||||
host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs
|
const httpsPassthroughRoute = createPassthroughRoute({
|
||||||
port: 443
|
domains: 'pass.example.com',
|
||||||
},
|
target: {
|
||||||
security: {
|
host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs
|
||||||
allowedIps: ['*'] // Allow all
|
port: 443
|
||||||
}
|
},
|
||||||
})
|
security: {
|
||||||
};
|
allowedIps: ['*'] // Allow all
|
||||||
expect(httpsPassthroughConfig.forwarding).toBeTruthy();
|
},
|
||||||
expect(httpsPassthroughConfig.forwarding.type).toEqual('https-passthrough');
|
name: 'HTTPS Passthrough Route'
|
||||||
expect(Array.isArray(httpsPassthroughConfig.forwarding.target.host)).toBeTrue();
|
});
|
||||||
|
|
||||||
|
expect(httpsPassthroughRoute).toBeTruthy();
|
||||||
|
expect(httpsPassthroughRoute.action.tls?.mode).toEqual('passthrough');
|
||||||
|
expect(Array.isArray(httpsPassthroughRoute.action.target?.host)).toBeTrue();
|
||||||
|
|
||||||
// Example 3: HTTPS Termination to HTTP Backend
|
// Example 3: HTTPS Termination to HTTP Backend
|
||||||
const terminateToHttpConfig: DomainConfig = {
|
const terminateToHttpRoute = createHttpsRoute({
|
||||||
domains: ['secure.example.com'],
|
domains: 'secure.example.com',
|
||||||
forwarding: tlsTerminateToHttp({
|
target: {
|
||||||
target: {
|
host: 'localhost',
|
||||||
host: 'localhost',
|
port: 8080
|
||||||
port: 8080
|
},
|
||||||
},
|
tlsMode: 'terminate',
|
||||||
http: {
|
certificate: 'auto',
|
||||||
redirectToHttps: true, // Redirect HTTP requests to HTTPS
|
headers: {
|
||||||
headers: {
|
'X-Forwarded-Proto': 'https'
|
||||||
'X-Forwarded-Proto': 'https'
|
},
|
||||||
}
|
security: {
|
||||||
},
|
allowedIps: ['*'] // Allow all
|
||||||
acme: {
|
},
|
||||||
enabled: true,
|
name: 'HTTPS Termination to HTTP Backend'
|
||||||
maintenance: true,
|
});
|
||||||
production: false // Use staging ACME server for testing
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
allowedIps: ['*'] // Allow all
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
expect(terminateToHttpConfig.forwarding).toBeTruthy();
|
|
||||||
expect(terminateToHttpConfig.forwarding.type).toEqual('https-terminate-to-http');
|
|
||||||
expect(terminateToHttpConfig.forwarding.http?.redirectToHttps).toBeTrue();
|
|
||||||
|
|
||||||
// Example 4: HTTPS Termination to HTTPS Backend
|
// Create the HTTP to HTTPS redirect for this domain
|
||||||
const terminateToHttpsConfig: DomainConfig = {
|
const httpToHttpsRedirect = createHttpToHttpsRedirect({
|
||||||
domains: ['proxy.example.com'],
|
domains: 'secure.example.com',
|
||||||
forwarding: tlsTerminateToHttps({
|
name: 'HTTP to HTTPS Redirect for secure.example.com'
|
||||||
target: {
|
});
|
||||||
host: 'internal-api.local',
|
|
||||||
port: 8443
|
|
||||||
},
|
|
||||||
https: {
|
|
||||||
forwardSni: true // Forward original SNI info
|
|
||||||
},
|
|
||||||
security: {
|
|
||||||
allowedIps: ['10.0.0.0/24', '192.168.1.0/24'],
|
|
||||||
maxConnections: 1000
|
|
||||||
},
|
|
||||||
advanced: {
|
|
||||||
timeout: 3600000, // 1 hour in ms
|
|
||||||
headers: {
|
|
||||||
'X-Original-Host': '{sni}'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
expect(terminateToHttpsConfig.forwarding).toBeTruthy();
|
|
||||||
expect(terminateToHttpsConfig.forwarding.type).toEqual('https-terminate-to-https');
|
|
||||||
expect(terminateToHttpsConfig.forwarding.https?.forwardSni).toBeTrue();
|
|
||||||
expect(terminateToHttpsConfig.forwarding.security?.allowedIps?.length).toEqual(2);
|
|
||||||
|
|
||||||
// Skip the SmartProxy integration test for now and just verify our configuration objects work
|
expect(terminateToHttpRoute).toBeTruthy();
|
||||||
console.log('All forwarding configurations were created successfully');
|
expect(terminateToHttpRoute.action.tls?.mode).toEqual('terminate');
|
||||||
|
expect(terminateToHttpRoute.action.advanced?.headers?.['X-Forwarded-Proto']).toEqual('https');
|
||||||
|
expect(httpToHttpsRedirect.action.type).toEqual('redirect');
|
||||||
|
|
||||||
// This is just to verify that our test passes
|
// Example 4: Load Balancer with HTTPS
|
||||||
expect(true).toBeTrue();
|
const loadBalancerRoute = createLoadBalancerRoute({
|
||||||
|
domains: 'proxy.example.com',
|
||||||
|
targets: ['internal-api-1.local', 'internal-api-2.local'],
|
||||||
|
targetPort: 8443,
|
||||||
|
tlsMode: 'terminate-and-reencrypt',
|
||||||
|
certificate: 'auto',
|
||||||
|
headers: {
|
||||||
|
'X-Original-Host': '{domain}'
|
||||||
|
},
|
||||||
|
security: {
|
||||||
|
allowedIps: ['10.0.0.0/24', '192.168.1.0/24'],
|
||||||
|
maxConnections: 1000
|
||||||
|
},
|
||||||
|
name: 'Load Balanced HTTPS Route'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(loadBalancerRoute).toBeTruthy();
|
||||||
|
expect(loadBalancerRoute.action.tls?.mode).toEqual('terminate-and-reencrypt');
|
||||||
|
expect(Array.isArray(loadBalancerRoute.action.target?.host)).toBeTrue();
|
||||||
|
expect(loadBalancerRoute.action.security?.allowedIps?.length).toEqual(2);
|
||||||
|
|
||||||
|
// Example 5: Block specific IPs
|
||||||
|
const blockRoute = createBlockRoute({
|
||||||
|
ports: [80, 443],
|
||||||
|
clientIp: ['192.168.5.0/24'],
|
||||||
|
name: 'Block Suspicious IPs',
|
||||||
|
priority: 1000 // High priority to ensure it's evaluated first
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(blockRoute.action.type).toEqual('block');
|
||||||
|
expect(blockRoute.match.clientIp?.length).toEqual(1);
|
||||||
|
expect(blockRoute.priority).toEqual(1000);
|
||||||
|
|
||||||
|
// Example 6: Complete HTTPS Server with HTTP Redirect
|
||||||
|
const httpsServerRoutes = createHttpsServer({
|
||||||
|
domains: 'complete.example.com',
|
||||||
|
target: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080
|
||||||
|
},
|
||||||
|
certificate: 'auto',
|
||||||
|
name: 'Complete HTTPS Server'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Array.isArray(httpsServerRoutes)).toBeTrue();
|
||||||
|
expect(httpsServerRoutes.length).toEqual(2); // HTTPS route and HTTP redirect
|
||||||
|
expect(httpsServerRoutes[0].action.tls?.mode).toEqual('terminate');
|
||||||
|
expect(httpsServerRoutes[1].action.type).toEqual('redirect');
|
||||||
|
|
||||||
|
// Example 7: Static File Server
|
||||||
|
const staticFileRoute = createStaticFileRoute({
|
||||||
|
domains: 'static.example.com',
|
||||||
|
targetDirectory: '/var/www/static',
|
||||||
|
tlsMode: 'terminate',
|
||||||
|
certificate: 'auto',
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'public, max-age=86400'
|
||||||
|
},
|
||||||
|
name: 'Static File Server'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(staticFileRoute.action.advanced?.staticFiles?.directory).toEqual('/var/www/static');
|
||||||
|
expect(staticFileRoute.action.advanced?.headers?.['Cache-Control']).toEqual('public, max-age=86400');
|
||||||
|
|
||||||
|
// Example 8: Test Route for Debugging
|
||||||
|
const testRoute = createTestRoute({
|
||||||
|
ports: 8000,
|
||||||
|
domains: 'test.example.com',
|
||||||
|
response: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ status: 'ok', message: 'API is working!' })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(testRoute.match.ports).toEqual(8000);
|
||||||
|
expect(testRoute.action.advanced?.testResponse?.status).toEqual(200);
|
||||||
|
|
||||||
|
// Create a SmartProxy instance with all routes
|
||||||
|
const allRoutes: IRouteConfig[] = [
|
||||||
|
httpOnlyRoute,
|
||||||
|
httpsPassthroughRoute,
|
||||||
|
terminateToHttpRoute,
|
||||||
|
httpToHttpsRedirect,
|
||||||
|
loadBalancerRoute,
|
||||||
|
blockRoute,
|
||||||
|
...httpsServerRoutes,
|
||||||
|
staticFileRoute,
|
||||||
|
testRoute
|
||||||
|
];
|
||||||
|
|
||||||
|
// We're not actually starting the SmartProxy in this test,
|
||||||
|
// just verifying that the configuration is valid
|
||||||
|
const smartProxy = new SmartProxy({
|
||||||
|
routes: allRoutes,
|
||||||
|
acme: {
|
||||||
|
email: 'admin@example.com',
|
||||||
|
termsOfServiceAgreed: true,
|
||||||
|
directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Smart Proxy configured with ${allRoutes.length} routes`);
|
||||||
|
|
||||||
|
// Verify our example proxy was created correctly
|
||||||
|
expect(smartProxy).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
@ -1,6 +1,6 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import type { ForwardConfig, ForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
import type { IForwardConfig, TForwardingType } from '../ts/forwarding/config/forwarding-types.js';
|
||||||
|
|
||||||
// First, import the components directly to avoid issues with compiled modules
|
// First, import the components directly to avoid issues with compiled modules
|
||||||
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
||||||
@ -17,7 +17,7 @@ const helpers = {
|
|||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
||||||
// HTTP-only defaults
|
// HTTP-only defaults
|
||||||
const httpConfig: ForwardConfig = {
|
const httpConfig: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -26,7 +26,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedHttpConfig.http?.enabled).toEqual(true);
|
expect(expandedHttpConfig.http?.enabled).toEqual(true);
|
||||||
|
|
||||||
// HTTPS-passthrough defaults
|
// HTTPS-passthrough defaults
|
||||||
const passthroughConfig: ForwardConfig = {
|
const passthroughConfig: IForwardConfig = {
|
||||||
type: 'https-passthrough',
|
type: 'https-passthrough',
|
||||||
target: { host: 'localhost', port: 443 }
|
target: { host: 'localhost', port: 443 }
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedPassthroughConfig.http?.enabled).toEqual(false);
|
expect(expandedPassthroughConfig.http?.enabled).toEqual(false);
|
||||||
|
|
||||||
// HTTPS-terminate-to-http defaults
|
// HTTPS-terminate-to-http defaults
|
||||||
const terminateToHttpConfig: ForwardConfig = {
|
const terminateToHttpConfig: IForwardConfig = {
|
||||||
type: 'https-terminate-to-http',
|
type: 'https-terminate-to-http',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -48,7 +48,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedTerminateToHttpConfig.acme?.maintenance).toEqual(true);
|
expect(expandedTerminateToHttpConfig.acme?.maintenance).toEqual(true);
|
||||||
|
|
||||||
// HTTPS-terminate-to-https defaults
|
// HTTPS-terminate-to-https defaults
|
||||||
const terminateToHttpsConfig: ForwardConfig = {
|
const terminateToHttpsConfig: IForwardConfig = {
|
||||||
type: 'https-terminate-to-https',
|
type: 'https-terminate-to-https',
|
||||||
target: { host: 'localhost', port: 8443 }
|
target: { host: 'localhost', port: 8443 }
|
||||||
};
|
};
|
||||||
@ -62,7 +62,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
||||||
// Valid configuration
|
// Valid configuration
|
||||||
const validConfig: ForwardConfig = {
|
const validConfig: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -77,7 +77,7 @@ tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
|||||||
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig1)).toThrow();
|
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig1)).toThrow();
|
||||||
|
|
||||||
// Invalid configuration - invalid port
|
// Invalid configuration - invalid port
|
||||||
const invalidConfig2: ForwardConfig = {
|
const invalidConfig2: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 0 }
|
target: { host: 'localhost', port: 0 }
|
||||||
};
|
};
|
||||||
@ -85,7 +85,7 @@ tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
|||||||
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig2)).toThrow();
|
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig2)).toThrow();
|
||||||
|
|
||||||
// Invalid configuration - HTTP disabled for HTTP-only
|
// Invalid configuration - HTTP disabled for HTTP-only
|
||||||
const invalidConfig3: ForwardConfig = {
|
const invalidConfig3: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 3000 },
|
target: { host: 'localhost', port: 3000 },
|
||||||
http: { enabled: false }
|
http: { enabled: false }
|
||||||
@ -94,7 +94,7 @@ tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
|||||||
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig3)).toThrow();
|
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig3)).toThrow();
|
||||||
|
|
||||||
// Invalid configuration - HTTP enabled for HTTPS passthrough
|
// Invalid configuration - HTTP enabled for HTTPS passthrough
|
||||||
const invalidConfig4: ForwardConfig = {
|
const invalidConfig4: IForwardConfig = {
|
||||||
type: 'https-passthrough',
|
type: 'https-passthrough',
|
||||||
target: { host: 'localhost', port: 443 },
|
target: { host: 'localhost', port: 443 },
|
||||||
http: { enabled: true }
|
http: { enabled: true }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { tap, expect } from '@push.rocks/tapbundle';
|
import { tap, expect } from '@push.rocks/tapbundle';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
import type { ForwardConfig } from '../ts/forwarding/config/forwarding-types.js';
|
import type { IForwardConfig } from '../ts/forwarding/config/forwarding-types.js';
|
||||||
|
|
||||||
// First, import the components directly to avoid issues with compiled modules
|
// First, import the components directly to avoid issues with compiled modules
|
||||||
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
import { ForwardingHandlerFactory } from '../ts/forwarding/factory/forwarding-factory.js';
|
||||||
@ -17,7 +17,7 @@ const helpers = {
|
|||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
|
||||||
// HTTP-only defaults
|
// HTTP-only defaults
|
||||||
const httpConfig: ForwardConfig = {
|
const httpConfig: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -26,7 +26,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedHttpConfig.http?.enabled).toEqual(true);
|
expect(expandedHttpConfig.http?.enabled).toEqual(true);
|
||||||
|
|
||||||
// HTTPS-passthrough defaults
|
// HTTPS-passthrough defaults
|
||||||
const passthroughConfig: ForwardConfig = {
|
const passthroughConfig: IForwardConfig = {
|
||||||
type: 'https-passthrough',
|
type: 'https-passthrough',
|
||||||
target: { host: 'localhost', port: 443 }
|
target: { host: 'localhost', port: 443 }
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedPassthroughConfig.http?.enabled).toEqual(false);
|
expect(expandedPassthroughConfig.http?.enabled).toEqual(false);
|
||||||
|
|
||||||
// HTTPS-terminate-to-http defaults
|
// HTTPS-terminate-to-http defaults
|
||||||
const terminateToHttpConfig: ForwardConfig = {
|
const terminateToHttpConfig: IForwardConfig = {
|
||||||
type: 'https-terminate-to-http',
|
type: 'https-terminate-to-http',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -48,7 +48,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
expect(expandedTerminateToHttpConfig.acme?.maintenance).toEqual(true);
|
expect(expandedTerminateToHttpConfig.acme?.maintenance).toEqual(true);
|
||||||
|
|
||||||
// HTTPS-terminate-to-https defaults
|
// HTTPS-terminate-to-https defaults
|
||||||
const terminateToHttpsConfig: ForwardConfig = {
|
const terminateToHttpsConfig: IForwardConfig = {
|
||||||
type: 'https-terminate-to-https',
|
type: 'https-terminate-to-https',
|
||||||
target: { host: 'localhost', port: 8443 }
|
target: { host: 'localhost', port: 8443 }
|
||||||
};
|
};
|
||||||
@ -62,7 +62,7 @@ tap.test('ForwardingHandlerFactory - apply defaults based on type', async () =>
|
|||||||
|
|
||||||
tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
||||||
// Valid configuration
|
// Valid configuration
|
||||||
const validConfig: ForwardConfig = {
|
const validConfig: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 3000 }
|
target: { host: 'localhost', port: 3000 }
|
||||||
};
|
};
|
||||||
@ -77,7 +77,7 @@ tap.test('ForwardingHandlerFactory - validate configuration', async () => {
|
|||||||
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig1)).toThrow();
|
expect(() => ForwardingHandlerFactory.validateConfig(invalidConfig1)).toThrow();
|
||||||
|
|
||||||
// Invalid configuration - invalid port
|
// Invalid configuration - invalid port
|
||||||
const invalidConfig2: ForwardConfig = {
|
const invalidConfig2: IForwardConfig = {
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: { host: 'localhost', port: 0 }
|
target: { host: 'localhost', port: 0 }
|
||||||
};
|
};
|
||||||
|
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
|
||||||
@ -282,10 +367,20 @@ tap.test('should support optional source IP preservation in chained proxies', as
|
|||||||
// Test round-robin behavior for multiple target hosts in a domain config.
|
// Test round-robin behavior for multiple target hosts in a domain config.
|
||||||
tap.test('should use round robin for multiple target hosts in domain config', async () => {
|
tap.test('should use round robin for multiple target hosts in domain config', async () => {
|
||||||
// Create a domain config with multiple hosts in the target
|
// Create a domain config with multiple hosts in the target
|
||||||
const domainConfig = {
|
const domainConfig: {
|
||||||
|
domains: string[];
|
||||||
|
forwarding: {
|
||||||
|
type: 'http-only';
|
||||||
|
target: {
|
||||||
|
host: string[];
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
http: { enabled: boolean };
|
||||||
|
}
|
||||||
|
} = {
|
||||||
domains: ['rr.test'],
|
domains: ['rr.test'],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'http-only',
|
type: 'http-only' as const,
|
||||||
target: {
|
target: {
|
||||||
host: ['hostA', 'hostB'], // Array of hosts for round-robin
|
host: ['hostA', 'hostB'], // Array of hosts for round-robin
|
||||||
port: 80
|
port: 80
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '13.1.0',
|
version: '15.1.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.'
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { AcmeOptions } from '../models/certificate-types.js';
|
import type { IAcmeOptions } from '../models/certificate-types.js';
|
||||||
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
||||||
// We'll need to update this import when we move the Port80Handler
|
// We'll need to update this import when we move the Port80Handler
|
||||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||||
@ -12,7 +12,7 @@ import { Port80Handler } from '../../http/port80/port80-handler.js';
|
|||||||
* @returns A new Port80Handler instance
|
* @returns A new Port80Handler instance
|
||||||
*/
|
*/
|
||||||
export function buildPort80Handler(
|
export function buildPort80Handler(
|
||||||
options: AcmeOptions
|
options: IAcmeOptions
|
||||||
): Port80Handler {
|
): Port80Handler {
|
||||||
if (options.certificateStore) {
|
if (options.certificateStore) {
|
||||||
ensureCertificateDirectory(options.certificateStore);
|
ensureCertificateDirectory(options.certificateStore);
|
||||||
@ -32,7 +32,7 @@ export function createDefaultAcmeOptions(
|
|||||||
email: string,
|
email: string,
|
||||||
certificateStore: string,
|
certificateStore: string,
|
||||||
useProduction: boolean = false
|
useProduction: boolean = false
|
||||||
): AcmeOptions {
|
): IAcmeOptions {
|
||||||
return {
|
return {
|
||||||
accountEmail: email,
|
accountEmail: email,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { AcmeOptions, CertificateData } from '../models/certificate-types.js';
|
import type { IAcmeOptions, ICertificateData } from '../models/certificate-types.js';
|
||||||
import { CertificateEvents } from '../events/certificate-events.js';
|
import { CertificateEvents } from '../events/certificate-events.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages ACME challenges and certificate validation
|
* Manages ACME challenges and certificate validation
|
||||||
*/
|
*/
|
||||||
export class AcmeChallengeHandler extends plugins.EventEmitter {
|
export class AcmeChallengeHandler extends plugins.EventEmitter {
|
||||||
private options: AcmeOptions;
|
private options: IAcmeOptions;
|
||||||
private client: any; // ACME client from plugins
|
private client: any; // ACME client from plugins
|
||||||
private pendingChallenges: Map<string, any>;
|
private pendingChallenges: Map<string, any>;
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export class AcmeChallengeHandler extends plugins.EventEmitter {
|
|||||||
* Creates a new ACME challenge handler
|
* Creates a new ACME challenge handler
|
||||||
* @param options ACME configuration options
|
* @param options ACME configuration options
|
||||||
*/
|
*/
|
||||||
constructor(options: AcmeOptions) {
|
constructor(options: IAcmeOptions) {
|
||||||
super();
|
super();
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.pendingChallenges = new Map();
|
this.pendingChallenges = new Map();
|
||||||
|
@ -25,8 +25,8 @@ export * from './storage/file-storage.js';
|
|||||||
// Convenience function to create a certificate provisioner with common settings
|
// Convenience function to create a certificate provisioner with common settings
|
||||||
import { CertProvisioner } from './providers/cert-provisioner.js';
|
import { CertProvisioner } from './providers/cert-provisioner.js';
|
||||||
import { buildPort80Handler } from './acme/acme-factory.js';
|
import { buildPort80Handler } from './acme/acme-factory.js';
|
||||||
import type { AcmeOptions, DomainForwardConfig } from './models/certificate-types.js';
|
import type { IAcmeOptions, IDomainForwardConfig } from './models/certificate-types.js';
|
||||||
import type { DomainConfig } from '../forwarding/config/domain-config.js';
|
import type { IDomainConfig } from '../forwarding/config/domain-config.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a complete certificate provisioning system with default settings
|
* Creates a complete certificate provisioning system with default settings
|
||||||
@ -37,8 +37,8 @@ import type { DomainConfig } from '../forwarding/config/domain-config.js';
|
|||||||
* @returns Configured CertProvisioner
|
* @returns Configured CertProvisioner
|
||||||
*/
|
*/
|
||||||
export function createCertificateProvisioner(
|
export function createCertificateProvisioner(
|
||||||
domainConfigs: DomainConfig[],
|
domainConfigs: IDomainConfig[],
|
||||||
acmeOptions: AcmeOptions,
|
acmeOptions: IAcmeOptions,
|
||||||
networkProxyBridge: any, // Placeholder until NetworkProxyBridge is migrated
|
networkProxyBridge: any, // Placeholder until NetworkProxyBridge is migrated
|
||||||
certProvider?: any // Placeholder until cert provider type is properly defined
|
certProvider?: any // Placeholder until cert provider type is properly defined
|
||||||
): CertProvisioner {
|
): CertProvisioner {
|
||||||
|
@ -84,4 +84,5 @@ export interface IAcmeOptions {
|
|||||||
certificateStore?: string; // Directory to store certificates
|
certificateStore?: string; // Directory to store certificates
|
||||||
skipConfiguredCerts?: boolean; // Skip domains with existing certificates
|
skipConfiguredCerts?: boolean; // Skip domains with existing certificates
|
||||||
domainForwards?: IDomainForwardConfig[]; // Domain-specific forwarding configs
|
domainForwards?: IDomainForwardConfig[]; // Domain-specific forwarding configs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,34 +1,34 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { DomainConfig } from '../../forwarding/config/domain-config.js';
|
import type { IDomainConfig } from '../../forwarding/config/domain-config.js';
|
||||||
import type { CertificateData, DomainForwardConfig, DomainOptions } from '../models/certificate-types.js';
|
import type { ICertificateData, IDomainForwardConfig, IDomainOptions } from '../models/certificate-types.js';
|
||||||
import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js';
|
import { Port80HandlerEvents, CertProvisionerEvents } from '../events/certificate-events.js';
|
||||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||||
// We need to define this interface until we migrate NetworkProxyBridge
|
// We need to define this interface until we migrate NetworkProxyBridge
|
||||||
interface NetworkProxyBridge {
|
interface INetworkProxyBridge {
|
||||||
applyExternalCertificate(certData: CertificateData): void;
|
applyExternalCertificate(certData: ICertificateData): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will be imported after NetworkProxyBridge is migrated
|
// This will be imported after NetworkProxyBridge is migrated
|
||||||
// import type { NetworkProxyBridge } from '../../proxies/smart-proxy/network-proxy-bridge.js';
|
// import type { NetworkProxyBridge } from '../../proxies/smart-proxy/network-proxy-bridge.js';
|
||||||
|
|
||||||
// For backward compatibility
|
// For backward compatibility
|
||||||
export type ISmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type for static certificate provisioning
|
* Type for static certificate provisioning
|
||||||
*/
|
*/
|
||||||
export type CertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01';
|
export type TCertProvisionObject = plugins.tsclass.network.ICert | 'http01' | 'dns01';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CertProvisioner manages certificate provisioning and renewal workflows,
|
* CertProvisioner manages certificate provisioning and renewal workflows,
|
||||||
* unifying static certificates and HTTP-01 challenges via Port80Handler.
|
* unifying static certificates and HTTP-01 challenges via Port80Handler.
|
||||||
*/
|
*/
|
||||||
export class CertProvisioner extends plugins.EventEmitter {
|
export class CertProvisioner extends plugins.EventEmitter {
|
||||||
private domainConfigs: DomainConfig[];
|
private domainConfigs: IDomainConfig[];
|
||||||
private port80Handler: Port80Handler;
|
private port80Handler: Port80Handler;
|
||||||
private networkProxyBridge: NetworkProxyBridge;
|
private networkProxyBridge: INetworkProxyBridge;
|
||||||
private certProvisionFunction?: (domain: string) => Promise<CertProvisionObject>;
|
private certProvisionFunction?: (domain: string) => Promise<TCertProvisionObject>;
|
||||||
private forwardConfigs: DomainForwardConfig[];
|
private forwardConfigs: IDomainForwardConfig[];
|
||||||
private renewThresholdDays: number;
|
private renewThresholdDays: number;
|
||||||
private renewCheckIntervalHours: number;
|
private renewCheckIntervalHours: number;
|
||||||
private autoRenew: boolean;
|
private autoRenew: boolean;
|
||||||
@ -47,14 +47,14 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
* @param forwardConfigs Domain forwarding configurations for ACME challenges
|
* @param forwardConfigs Domain forwarding configurations for ACME challenges
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
domainConfigs: DomainConfig[],
|
domainConfigs: IDomainConfig[],
|
||||||
port80Handler: Port80Handler,
|
port80Handler: Port80Handler,
|
||||||
networkProxyBridge: NetworkProxyBridge,
|
networkProxyBridge: INetworkProxyBridge,
|
||||||
certProvider?: (domain: string) => Promise<CertProvisionObject>,
|
certProvider?: (domain: string) => Promise<TCertProvisionObject>,
|
||||||
renewThresholdDays: number = 30,
|
renewThresholdDays: number = 30,
|
||||||
renewCheckIntervalHours: number = 24,
|
renewCheckIntervalHours: number = 24,
|
||||||
autoRenew: boolean = true,
|
autoRenew: boolean = true,
|
||||||
forwardConfigs: DomainForwardConfig[] = []
|
forwardConfigs: IDomainForwardConfig[] = []
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.domainConfigs = domainConfigs;
|
this.domainConfigs = domainConfigs;
|
||||||
@ -92,11 +92,11 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private setupEventSubscriptions(): void {
|
private setupEventSubscriptions(): void {
|
||||||
// We need to reimplement subscribeToPort80Handler here
|
// We need to reimplement subscribeToPort80Handler here
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: CertificateData) => {
|
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
|
||||||
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, { ...data, source: 'http01', isRenewal: false });
|
this.emit(CertProvisionerEvents.CERTIFICATE_ISSUED, { ...data, source: 'http01', isRenewal: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: CertificateData) => {
|
this.port80Handler.on(Port80HandlerEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
|
||||||
this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, { ...data, source: 'http01', isRenewal: true });
|
this.emit(CertProvisionerEvents.CERTIFICATE_RENEWED, { ...data, source: 'http01', isRenewal: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private setupForwardingConfigs(): void {
|
private setupForwardingConfigs(): void {
|
||||||
for (const config of this.forwardConfigs) {
|
for (const config of this.forwardConfigs) {
|
||||||
const domainOptions: DomainOptions = {
|
const domainOptions: IDomainOptions = {
|
||||||
domainName: config.domain,
|
domainName: config.domain,
|
||||||
sslRedirect: config.sslRedirect || false,
|
sslRedirect: config.sslRedirect || false,
|
||||||
acmeMaintenance: false,
|
acmeMaintenance: false,
|
||||||
@ -138,7 +138,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private async provisionDomain(domain: string): Promise<void> {
|
private async provisionDomain(domain: string): Promise<void> {
|
||||||
const isWildcard = domain.includes('*');
|
const isWildcard = domain.includes('*');
|
||||||
let provision: CertProvisionObject = 'http01';
|
let provision: TCertProvisionObject = 'http01';
|
||||||
|
|
||||||
// Try to get a certificate from the provision function
|
// Try to get a certificate from the provision function
|
||||||
if (this.certProvisionFunction) {
|
if (this.certProvisionFunction) {
|
||||||
@ -174,7 +174,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
// Static certificate (e.g., DNS-01 provisioned or user-provided)
|
// Static certificate (e.g., DNS-01 provisioned or user-provided)
|
||||||
this.provisionMap.set(domain, 'static');
|
this.provisionMap.set(domain, 'static');
|
||||||
const certObj = provision as plugins.tsclass.network.ICert;
|
const certObj = provision as plugins.tsclass.network.ICert;
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain: certObj.domainName,
|
domain: certObj.domainName,
|
||||||
certificate: certObj.publicKey,
|
certificate: certObj.publicKey,
|
||||||
privateKey: certObj.privateKey,
|
privateKey: certObj.privateKey,
|
||||||
@ -235,7 +235,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
|
|
||||||
if (provision !== 'http01' && provision !== 'dns01') {
|
if (provision !== 'http01' && provision !== 'dns01') {
|
||||||
const certObj = provision as plugins.tsclass.network.ICert;
|
const certObj = provision as plugins.tsclass.network.ICert;
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain: certObj.domainName,
|
domain: certObj.domainName,
|
||||||
certificate: certObj.publicKey,
|
certificate: certObj.publicKey,
|
||||||
privateKey: certObj.privateKey,
|
privateKey: certObj.privateKey,
|
||||||
@ -267,7 +267,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
const isWildcard = domain.includes('*');
|
const isWildcard = domain.includes('*');
|
||||||
|
|
||||||
// Determine provisioning method
|
// Determine provisioning method
|
||||||
let provision: CertProvisionObject = 'http01';
|
let provision: TCertProvisionObject = 'http01';
|
||||||
|
|
||||||
if (this.certProvisionFunction) {
|
if (this.certProvisionFunction) {
|
||||||
provision = await this.certProvisionFunction(domain);
|
provision = await this.certProvisionFunction(domain);
|
||||||
@ -288,7 +288,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
||||||
const certObj = provision as plugins.tsclass.network.ICert;
|
const certObj = provision as plugins.tsclass.network.ICert;
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain: certObj.domainName,
|
domain: certObj.domainName,
|
||||||
certificate: certObj.publicKey,
|
certificate: certObj.publicKey,
|
||||||
privateKey: certObj.privateKey,
|
privateKey: certObj.privateKey,
|
||||||
@ -311,7 +311,7 @@ export class CertProvisioner extends plugins.EventEmitter {
|
|||||||
sslRedirect?: boolean;
|
sslRedirect?: boolean;
|
||||||
acmeMaintenance?: boolean;
|
acmeMaintenance?: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const domainOptions: DomainOptions = {
|
const domainOptions: IDomainOptions = {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
sslRedirect: options?.sslRedirect || true,
|
sslRedirect: options?.sslRedirect || true,
|
||||||
acmeMaintenance: options?.acmeMaintenance || true
|
acmeMaintenance: options?.acmeMaintenance || true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { CertificateData, Certificates } from '../models/certificate-types.js';
|
import type { ICertificateData, ICertificates } from '../models/certificate-types.js';
|
||||||
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
import { ensureCertificateDirectory } from '../utils/certificate-helpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,10 +21,10 @@ export class FileStorage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a certificate to the file system
|
* Save a certificate to the file system
|
||||||
* @param domain Domain name
|
* @param domain Domain name
|
||||||
* @param certData Certificate data to save
|
* @param certData Certificate data to save
|
||||||
*/
|
*/
|
||||||
public async saveCertificate(domain: string, certData: CertificateData): Promise<void> {
|
public async saveCertificate(domain: string, certData: ICertificateData): Promise<void> {
|
||||||
const sanitizedDomain = this.sanitizeDomain(domain);
|
const sanitizedDomain = this.sanitizeDomain(domain);
|
||||||
const certDir = path.join(this.storageDir, sanitizedDomain);
|
const certDir = path.join(this.storageDir, sanitizedDomain);
|
||||||
ensureCertificateDirectory(certDir);
|
ensureCertificateDirectory(certDir);
|
||||||
@ -57,7 +57,7 @@ export class FileStorage {
|
|||||||
* @param domain Domain name
|
* @param domain Domain name
|
||||||
* @returns Certificate data if found, null otherwise
|
* @returns Certificate data if found, null otherwise
|
||||||
*/
|
*/
|
||||||
public async loadCertificate(domain: string): Promise<CertificateData | null> {
|
public async loadCertificate(domain: string): Promise<ICertificateData | null> {
|
||||||
const sanitizedDomain = this.sanitizeDomain(domain);
|
const sanitizedDomain = this.sanitizeDomain(domain);
|
||||||
const certDir = path.join(this.storageDir, sanitizedDomain);
|
const certDir = path.join(this.storageDir, sanitizedDomain);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import type { Certificates } from '../models/certificate-types.js';
|
import type { ICertificates } from '../models/certificate-types.js';
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
* Loads the default SSL certificates from the assets directory
|
* Loads the default SSL certificates from the assets directory
|
||||||
* @returns The certificate key pair
|
* @returns The certificate key pair
|
||||||
*/
|
*/
|
||||||
export function loadDefaultCertificates(): Certificates {
|
export function loadDefaultCertificates(): ICertificates {
|
||||||
try {
|
try {
|
||||||
// Need to adjust path from /ts/certificate/utils to /assets/certs
|
// Need to adjust path from /ts/certificate/utils to /assets/certs
|
||||||
const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs');
|
const certPath = path.join(__dirname, '..', '..', '..', 'assets', 'certs');
|
||||||
|
@ -6,7 +6,7 @@ import type {
|
|||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ForwardConfig as IForwardConfig
|
IForwardConfig
|
||||||
} from '../forwarding/config/forwarding-types.js';
|
} from '../forwarding/config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import type { ForwardConfig } from './forwarding-types.js';
|
import type { IForwardConfig } from './forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Domain configuration with unified forwarding configuration
|
* Domain configuration with unified forwarding configuration
|
||||||
*/
|
*/
|
||||||
export interface DomainConfig {
|
export interface IDomainConfig {
|
||||||
// Core properties - domain patterns
|
// Core properties - domain patterns
|
||||||
domains: string[];
|
domains: string[];
|
||||||
|
|
||||||
// Unified forwarding configuration
|
// Unified forwarding configuration
|
||||||
forwarding: ForwardConfig;
|
forwarding: IForwardConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,8 +16,8 @@ export interface DomainConfig {
|
|||||||
*/
|
*/
|
||||||
export function createDomainConfig(
|
export function createDomainConfig(
|
||||||
domains: string | string[],
|
domains: string | string[],
|
||||||
forwarding: ForwardConfig
|
forwarding: IForwardConfig
|
||||||
): DomainConfig {
|
): IDomainConfig {
|
||||||
// Normalize domains to an array
|
// Normalize domains to an array
|
||||||
const domainArray = Array.isArray(domains) ? domains : [domains];
|
const domainArray = Array.isArray(domains) ? domains : [domains];
|
||||||
|
|
||||||
@ -25,7 +25,4 @@ export function createDomainConfig(
|
|||||||
domains: domainArray,
|
domains: domainArray,
|
||||||
forwarding
|
forwarding
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backwards compatibility
|
|
||||||
export interface IDomainConfig extends DomainConfig {}
|
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { DomainConfig } from './domain-config.js';
|
import type { IDomainConfig } from './domain-config.js';
|
||||||
import { ForwardingHandler } from '../handlers/base-handler.js';
|
import { ForwardingHandler } from '../handlers/base-handler.js';
|
||||||
import { ForwardingHandlerEvents } from './forwarding-types.js';
|
import { ForwardingHandlerEvents } from './forwarding-types.js';
|
||||||
import { ForwardingHandlerFactory } from '../factory/forwarding-factory.js';
|
import { ForwardingHandlerFactory } from '../factory/forwarding-factory.js';
|
||||||
@ -21,14 +21,14 @@ export enum DomainManagerEvents {
|
|||||||
* Manages domains and their forwarding handlers
|
* Manages domains and their forwarding handlers
|
||||||
*/
|
*/
|
||||||
export class DomainManager extends plugins.EventEmitter {
|
export class DomainManager extends plugins.EventEmitter {
|
||||||
private domainConfigs: DomainConfig[] = [];
|
private domainConfigs: IDomainConfig[] = [];
|
||||||
private domainHandlers: Map<string, ForwardingHandler> = new Map();
|
private domainHandlers: Map<string, ForwardingHandler> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new DomainManager
|
* Create a new DomainManager
|
||||||
* @param initialDomains Optional initial domain configurations
|
* @param initialDomains Optional initial domain configurations
|
||||||
*/
|
*/
|
||||||
constructor(initialDomains?: DomainConfig[]) {
|
constructor(initialDomains?: IDomainConfig[]) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
if (initialDomains) {
|
if (initialDomains) {
|
||||||
@ -40,7 +40,7 @@ export class DomainManager extends plugins.EventEmitter {
|
|||||||
* Set or replace all domain configurations
|
* Set or replace all domain configurations
|
||||||
* @param configs Array of domain configurations
|
* @param configs Array of domain configurations
|
||||||
*/
|
*/
|
||||||
public async setDomainConfigs(configs: DomainConfig[]): Promise<void> {
|
public async setDomainConfigs(configs: IDomainConfig[]): Promise<void> {
|
||||||
// Clear existing handlers
|
// Clear existing handlers
|
||||||
this.domainHandlers.clear();
|
this.domainHandlers.clear();
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ export class DomainManager extends plugins.EventEmitter {
|
|||||||
* Add a new domain configuration
|
* Add a new domain configuration
|
||||||
* @param config The domain configuration to add
|
* @param config The domain configuration to add
|
||||||
*/
|
*/
|
||||||
public async addDomainConfig(config: DomainConfig): Promise<void> {
|
public async addDomainConfig(config: IDomainConfig): Promise<void> {
|
||||||
// Check if any of these domains already exist
|
// Check if any of these domains already exist
|
||||||
for (const domain of config.domains) {
|
for (const domain of config.domains) {
|
||||||
if (this.domainHandlers.has(domain)) {
|
if (this.domainHandlers.has(domain)) {
|
||||||
@ -193,7 +193,7 @@ export class DomainManager extends plugins.EventEmitter {
|
|||||||
* Create handlers for a domain configuration
|
* Create handlers for a domain configuration
|
||||||
* @param config The domain configuration
|
* @param config The domain configuration
|
||||||
*/
|
*/
|
||||||
private async createHandlersForDomain(config: DomainConfig): Promise<void> {
|
private async createHandlersForDomain(config: IDomainConfig): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Create a handler for this forwarding configuration
|
// Create a handler for this forwarding configuration
|
||||||
const handler = ForwardingHandlerFactory.createHandler(config.forwarding);
|
const handler = ForwardingHandlerFactory.createHandler(config.forwarding);
|
||||||
@ -221,7 +221,7 @@ export class DomainManager extends plugins.EventEmitter {
|
|||||||
* @param handler The handler
|
* @param handler The handler
|
||||||
* @param config The domain configuration for this handler
|
* @param config The domain configuration for this handler
|
||||||
*/
|
*/
|
||||||
private setupHandlerEvents(handler: ForwardingHandler, config: DomainConfig): void {
|
private setupHandlerEvents(handler: ForwardingHandler, config: IDomainConfig): void {
|
||||||
// Forward relevant events
|
// Forward relevant events
|
||||||
handler.on(ForwardingHandlerEvents.CERTIFICATE_NEEDED, (data) => {
|
handler.on(ForwardingHandlerEvents.CERTIFICATE_NEEDED, (data) => {
|
||||||
this.emit(DomainManagerEvents.CERTIFICATE_NEEDED, {
|
this.emit(DomainManagerEvents.CERTIFICATE_NEEDED, {
|
||||||
@ -277,7 +277,7 @@ export class DomainManager extends plugins.EventEmitter {
|
|||||||
* Get all domain configurations
|
* Get all domain configurations
|
||||||
* @returns Array of domain configurations
|
* @returns Array of domain configurations
|
||||||
*/
|
*/
|
||||||
public getDomainConfigs(): DomainConfig[] {
|
public getDomainConfigs(): IDomainConfig[] {
|
||||||
return [...this.domainConfigs];
|
return [...this.domainConfigs];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ import type * as plugins from '../../plugins.js';
|
|||||||
/**
|
/**
|
||||||
* The primary forwarding types supported by SmartProxy
|
* The primary forwarding types supported by SmartProxy
|
||||||
*/
|
*/
|
||||||
export type ForwardingType =
|
export type TForwardingType =
|
||||||
| 'http-only' // HTTP forwarding only (no HTTPS)
|
| 'http-only' // HTTP forwarding only (no HTTPS)
|
||||||
| 'https-passthrough' // Pass-through TLS traffic (SNI forwarding)
|
| 'https-passthrough' // Pass-through TLS traffic (SNI forwarding)
|
||||||
| 'https-terminate-to-http' // Terminate TLS and forward to HTTP backend
|
| 'https-terminate-to-http' // Terminate TLS and forward to HTTP backend
|
||||||
@ -12,7 +12,7 @@ export type ForwardingType =
|
|||||||
/**
|
/**
|
||||||
* Target configuration for forwarding
|
* Target configuration for forwarding
|
||||||
*/
|
*/
|
||||||
export interface TargetConfig {
|
export interface ITargetConfig {
|
||||||
host: string | string[]; // Support single host or round-robin
|
host: string | string[]; // Support single host or round-robin
|
||||||
port: number;
|
port: number;
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@ export interface TargetConfig {
|
|||||||
/**
|
/**
|
||||||
* HTTP-specific options for forwarding
|
* HTTP-specific options for forwarding
|
||||||
*/
|
*/
|
||||||
export interface HttpOptions {
|
export interface IHttpOptions {
|
||||||
enabled?: boolean; // Whether HTTP is enabled
|
enabled?: boolean; // Whether HTTP is enabled
|
||||||
redirectToHttps?: boolean; // Redirect HTTP to HTTPS
|
redirectToHttps?: boolean; // Redirect HTTP to HTTPS
|
||||||
headers?: Record<string, string>; // Custom headers for HTTP responses
|
headers?: Record<string, string>; // Custom headers for HTTP responses
|
||||||
@ -29,7 +29,7 @@ export interface HttpOptions {
|
|||||||
/**
|
/**
|
||||||
* HTTPS-specific options for forwarding
|
* HTTPS-specific options for forwarding
|
||||||
*/
|
*/
|
||||||
export interface HttpsOptions {
|
export interface IHttpsOptions {
|
||||||
customCert?: { // Use custom cert instead of auto-provisioned
|
customCert?: { // Use custom cert instead of auto-provisioned
|
||||||
key: string;
|
key: string;
|
||||||
cert: string;
|
cert: string;
|
||||||
@ -40,8 +40,8 @@ export interface HttpsOptions {
|
|||||||
/**
|
/**
|
||||||
* ACME certificate handling options
|
* ACME certificate handling options
|
||||||
*/
|
*/
|
||||||
export interface AcmeForwardingOptions {
|
export interface IAcmeForwardingOptions {
|
||||||
enabled?: boolean; // Enable ACME certificate provisioning
|
enabled?: boolean; // Enable ACME certificate provisioning
|
||||||
maintenance?: boolean; // Auto-renew certificates
|
maintenance?: boolean; // Auto-renew certificates
|
||||||
production?: boolean; // Use production ACME servers
|
production?: boolean; // Use production ACME servers
|
||||||
forwardChallenges?: { // Forward ACME challenges
|
forwardChallenges?: { // Forward ACME challenges
|
||||||
@ -54,7 +54,7 @@ export interface AcmeForwardingOptions {
|
|||||||
/**
|
/**
|
||||||
* Security options for forwarding
|
* Security options for forwarding
|
||||||
*/
|
*/
|
||||||
export interface SecurityOptions {
|
export interface ISecurityOptions {
|
||||||
allowedIps?: string[]; // IPs allowed to connect
|
allowedIps?: string[]; // IPs allowed to connect
|
||||||
blockedIps?: string[]; // IPs blocked from connecting
|
blockedIps?: string[]; // IPs blocked from connecting
|
||||||
maxConnections?: number; // Max simultaneous connections
|
maxConnections?: number; // Max simultaneous connections
|
||||||
@ -63,7 +63,7 @@ export interface SecurityOptions {
|
|||||||
/**
|
/**
|
||||||
* Advanced options for forwarding
|
* Advanced options for forwarding
|
||||||
*/
|
*/
|
||||||
export interface AdvancedOptions {
|
export interface IAdvancedOptions {
|
||||||
portRanges?: Array<{ from: number; to: number }>; // Allowed port ranges
|
portRanges?: Array<{ from: number; to: number }>; // Allowed port ranges
|
||||||
networkProxyPort?: number; // Custom NetworkProxy port if using terminate mode
|
networkProxyPort?: number; // Custom NetworkProxy port if using terminate mode
|
||||||
keepAlive?: boolean; // Enable TCP keepalive
|
keepAlive?: boolean; // Enable TCP keepalive
|
||||||
@ -74,21 +74,21 @@ export interface AdvancedOptions {
|
|||||||
/**
|
/**
|
||||||
* Unified forwarding configuration interface
|
* Unified forwarding configuration interface
|
||||||
*/
|
*/
|
||||||
export interface ForwardConfig {
|
export interface IForwardConfig {
|
||||||
// Define the primary forwarding type - use-case driven approach
|
// Define the primary forwarding type - use-case driven approach
|
||||||
type: ForwardingType;
|
type: TForwardingType;
|
||||||
|
|
||||||
// Target configuration
|
// Target configuration
|
||||||
target: TargetConfig;
|
target: ITargetConfig;
|
||||||
|
|
||||||
// Protocol options
|
// Protocol options
|
||||||
http?: HttpOptions;
|
http?: IHttpOptions;
|
||||||
https?: HttpsOptions;
|
https?: IHttpsOptions;
|
||||||
acme?: AcmeForwardingOptions;
|
acme?: IAcmeForwardingOptions;
|
||||||
|
|
||||||
// Security and advanced options
|
// Security and advanced options
|
||||||
security?: SecurityOptions;
|
security?: ISecurityOptions;
|
||||||
advanced?: AdvancedOptions;
|
advanced?: IAdvancedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -118,8 +118,8 @@ export interface IForwardingHandler extends plugins.EventEmitter {
|
|||||||
* Helper function types for common forwarding patterns
|
* Helper function types for common forwarding patterns
|
||||||
*/
|
*/
|
||||||
export const httpOnly = (
|
export const httpOnly = (
|
||||||
partialConfig: Partial<ForwardConfig> & Pick<ForwardConfig, 'target'>
|
partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'>
|
||||||
): ForwardConfig => ({
|
): IForwardConfig => ({
|
||||||
type: 'http-only',
|
type: 'http-only',
|
||||||
target: partialConfig.target,
|
target: partialConfig.target,
|
||||||
http: { enabled: true, ...(partialConfig.http || {}) },
|
http: { enabled: true, ...(partialConfig.http || {}) },
|
||||||
@ -128,8 +128,8 @@ export const httpOnly = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const tlsTerminateToHttp = (
|
export const tlsTerminateToHttp = (
|
||||||
partialConfig: Partial<ForwardConfig> & Pick<ForwardConfig, 'target'>
|
partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'>
|
||||||
): ForwardConfig => ({
|
): IForwardConfig => ({
|
||||||
type: 'https-terminate-to-http',
|
type: 'https-terminate-to-http',
|
||||||
target: partialConfig.target,
|
target: partialConfig.target,
|
||||||
https: { ...(partialConfig.https || {}) },
|
https: { ...(partialConfig.https || {}) },
|
||||||
@ -140,8 +140,8 @@ export const tlsTerminateToHttp = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const tlsTerminateToHttps = (
|
export const tlsTerminateToHttps = (
|
||||||
partialConfig: Partial<ForwardConfig> & Pick<ForwardConfig, 'target'>
|
partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'>
|
||||||
): ForwardConfig => ({
|
): IForwardConfig => ({
|
||||||
type: 'https-terminate-to-https',
|
type: 'https-terminate-to-https',
|
||||||
target: partialConfig.target,
|
target: partialConfig.target,
|
||||||
https: { ...(partialConfig.https || {}) },
|
https: { ...(partialConfig.https || {}) },
|
||||||
@ -152,20 +152,11 @@ export const tlsTerminateToHttps = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const httpsPassthrough = (
|
export const httpsPassthrough = (
|
||||||
partialConfig: Partial<ForwardConfig> & Pick<ForwardConfig, 'target'>
|
partialConfig: Partial<IForwardConfig> & Pick<IForwardConfig, 'target'>
|
||||||
): ForwardConfig => ({
|
): IForwardConfig => ({
|
||||||
type: 'https-passthrough',
|
type: 'https-passthrough',
|
||||||
target: partialConfig.target,
|
target: partialConfig.target,
|
||||||
https: { forwardSni: true, ...(partialConfig.https || {}) },
|
https: { forwardSni: true, ...(partialConfig.https || {}) },
|
||||||
...(partialConfig.security ? { security: partialConfig.security } : {}),
|
...(partialConfig.security ? { security: partialConfig.security } : {}),
|
||||||
...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {})
|
...(partialConfig.advanced ? { advanced: partialConfig.advanced } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
// Backwards compatibility interfaces with 'I' prefix
|
|
||||||
export interface ITargetConfig extends TargetConfig {}
|
|
||||||
export interface IHttpOptions extends HttpOptions {}
|
|
||||||
export interface IHttpsOptions extends HttpsOptions {}
|
|
||||||
export interface IAcmeForwardingOptions extends AcmeForwardingOptions {}
|
|
||||||
export interface ISecurityOptions extends SecurityOptions {}
|
|
||||||
export interface IAdvancedOptions extends AdvancedOptions {}
|
|
||||||
export interface IForwardConfig extends ForwardConfig {}
|
|
@ -1,5 +1,5 @@
|
|||||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import type { ForwardingHandler } from '../handlers/base-handler.js';
|
import { ForwardingHandler } from '../handlers/base-handler.js';
|
||||||
import { HttpForwardingHandler } from '../handlers/http-handler.js';
|
import { HttpForwardingHandler } from '../handlers/http-handler.js';
|
||||||
import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js';
|
import { HttpsPassthroughHandler } from '../handlers/https-passthrough-handler.js';
|
||||||
import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js';
|
import { HttpsTerminateToHttpHandler } from '../handlers/https-terminate-to-http-handler.js';
|
||||||
@ -14,35 +14,35 @@ export class ForwardingHandlerFactory {
|
|||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
* @returns The appropriate forwarding handler
|
* @returns The appropriate forwarding handler
|
||||||
*/
|
*/
|
||||||
public static createHandler(config: ForwardConfig): ForwardingHandler {
|
public static createHandler(config: IForwardConfig): ForwardingHandler {
|
||||||
// Create the appropriate handler based on the forwarding type
|
// Create the appropriate handler based on the forwarding type
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case 'http-only':
|
case 'http-only':
|
||||||
return new HttpForwardingHandler(config);
|
return new HttpForwardingHandler(config);
|
||||||
|
|
||||||
case 'https-passthrough':
|
case 'https-passthrough':
|
||||||
return new HttpsPassthroughHandler(config);
|
return new HttpsPassthroughHandler(config);
|
||||||
|
|
||||||
case 'https-terminate-to-http':
|
case 'https-terminate-to-http':
|
||||||
return new HttpsTerminateToHttpHandler(config);
|
return new HttpsTerminateToHttpHandler(config);
|
||||||
|
|
||||||
case 'https-terminate-to-https':
|
case 'https-terminate-to-https':
|
||||||
return new HttpsTerminateToHttpsHandler(config);
|
return new HttpsTerminateToHttpsHandler(config);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Type system should prevent this, but just in case:
|
// Type system should prevent this, but just in case:
|
||||||
throw new Error(`Unknown forwarding type: ${(config as any).type}`);
|
throw new Error(`Unknown forwarding type: ${(config as any).type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply default values to a forwarding configuration based on its type
|
* Apply default values to a forwarding configuration based on its type
|
||||||
* @param config The original forwarding configuration
|
* @param config The original forwarding configuration
|
||||||
* @returns A configuration with defaults applied
|
* @returns A configuration with defaults applied
|
||||||
*/
|
*/
|
||||||
public static applyDefaults(config: ForwardConfig): ForwardConfig {
|
public static applyDefaults(config: IForwardConfig): IForwardConfig {
|
||||||
// Create a deep copy of the configuration
|
// Create a deep copy of the configuration
|
||||||
const result: ForwardConfig = JSON.parse(JSON.stringify(config));
|
const result: IForwardConfig = JSON.parse(JSON.stringify(config));
|
||||||
|
|
||||||
// Apply defaults based on forwarding type
|
// Apply defaults based on forwarding type
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
@ -112,7 +112,7 @@ export class ForwardingHandlerFactory {
|
|||||||
* @param config The configuration to validate
|
* @param config The configuration to validate
|
||||||
* @throws Error if the configuration is invalid
|
* @throws Error if the configuration is invalid
|
||||||
*/
|
*/
|
||||||
public static validateConfig(config: ForwardConfig): void {
|
public static validateConfig(config: IForwardConfig): void {
|
||||||
// Validate common properties
|
// Validate common properties
|
||||||
if (!config.target) {
|
if (!config.target) {
|
||||||
throw new Error('Forwarding configuration must include a target');
|
throw new Error('Forwarding configuration must include a target');
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type {
|
import type {
|
||||||
ForwardConfig,
|
IForwardConfig,
|
||||||
IForwardingHandler
|
IForwardingHandler
|
||||||
} from '../config/forwarding-types.js';
|
} from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||||
@ -13,7 +13,7 @@ export abstract class ForwardingHandler extends plugins.EventEmitter implements
|
|||||||
* Create a new ForwardingHandler
|
* Create a new ForwardingHandler
|
||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
*/
|
*/
|
||||||
constructor(protected config: ForwardConfig) {
|
constructor(protected config: IForwardConfig) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,14 +11,23 @@ export class HttpForwardingHandler extends ForwardingHandler {
|
|||||||
* Create a new HTTP forwarding handler
|
* Create a new HTTP forwarding handler
|
||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
*/
|
*/
|
||||||
constructor(config: ForwardConfig) {
|
constructor(config: IForwardConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
// Validate that this is an HTTP-only configuration
|
// Validate that this is an HTTP-only configuration
|
||||||
if (config.type !== 'http-only') {
|
if (config.type !== 'http-only') {
|
||||||
throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
|
throw new Error(`Invalid configuration type for HttpForwardingHandler: ${config.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the handler
|
||||||
|
* HTTP handler doesn't need special initialization
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// Basic initialization from parent class
|
||||||
|
await super.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a raw socket connection
|
* Handle a raw socket connection
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -11,14 +11,23 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
|
|||||||
* Create a new HTTPS passthrough handler
|
* Create a new HTTPS passthrough handler
|
||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
*/
|
*/
|
||||||
constructor(config: ForwardConfig) {
|
constructor(config: IForwardConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
// Validate that this is an HTTPS passthrough configuration
|
// Validate that this is an HTTPS passthrough configuration
|
||||||
if (config.type !== 'https-passthrough') {
|
if (config.type !== 'https-passthrough') {
|
||||||
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
|
throw new Error(`Invalid configuration type for HttpsPassthroughHandler: ${config.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the handler
|
||||||
|
* HTTPS passthrough handler doesn't need special initialization
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
// Basic initialization from parent class
|
||||||
|
await super.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a TLS/SSL socket connection by forwarding it without termination
|
* Handle a TLS/SSL socket connection by forwarding it without termination
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +14,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
|
|||||||
* Create a new HTTPS termination with HTTP backend handler
|
* Create a new HTTPS termination with HTTP backend handler
|
||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
*/
|
*/
|
||||||
constructor(config: ForwardConfig) {
|
constructor(config: IForwardConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
// Validate that this is an HTTPS terminate to HTTP configuration
|
// Validate that this is an HTTPS terminate to HTTP configuration
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { ForwardingHandler } from './base-handler.js';
|
import { ForwardingHandler } from './base-handler.js';
|
||||||
import type { ForwardConfig } from '../config/forwarding-types.js';
|
import type { IForwardConfig } from '../config/forwarding-types.js';
|
||||||
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -13,7 +13,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
|
|||||||
* Create a new HTTPS termination with HTTPS backend handler
|
* Create a new HTTPS termination with HTTPS backend handler
|
||||||
* @param config The forwarding configuration
|
* @param config The forwarding configuration
|
||||||
*/
|
*/
|
||||||
constructor(config: ForwardConfig) {
|
constructor(config: IForwardConfig) {
|
||||||
super(config);
|
super(config);
|
||||||
|
|
||||||
// Validate that this is an HTTPS terminate to HTTPS configuration
|
// Validate that this is an HTTPS terminate to HTTPS configuration
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type {
|
import type {
|
||||||
ForwardConfig,
|
IForwardConfig,
|
||||||
DomainOptions,
|
IDomainOptions,
|
||||||
AcmeOptions
|
IAcmeOptions
|
||||||
} from '../../certificate/models/certificate-types.js';
|
} from '../../certificate/models/certificate-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,8 +35,8 @@ export enum HttpStatus {
|
|||||||
/**
|
/**
|
||||||
* Represents a domain configuration with certificate status information
|
* Represents a domain configuration with certificate status information
|
||||||
*/
|
*/
|
||||||
export interface DomainCertificate {
|
export interface IDomainCertificate {
|
||||||
options: DomainOptions;
|
options: IDomainOptions;
|
||||||
certObtained: boolean;
|
certObtained: boolean;
|
||||||
obtainingInProgress: boolean;
|
obtainingInProgress: boolean;
|
||||||
certificate?: string;
|
certificate?: string;
|
||||||
@ -82,7 +82,7 @@ export class ServerError extends HttpError {
|
|||||||
/**
|
/**
|
||||||
* Redirect configuration for HTTP requests
|
* Redirect configuration for HTTP requests
|
||||||
*/
|
*/
|
||||||
export interface RedirectConfig {
|
export interface IRedirectConfig {
|
||||||
source: string; // Source path or pattern
|
source: string; // Source path or pattern
|
||||||
destination: string; // Destination URL
|
destination: string; // Destination URL
|
||||||
type: HttpStatus; // Redirect status code
|
type: HttpStatus; // Redirect status code
|
||||||
@ -92,7 +92,7 @@ export interface RedirectConfig {
|
|||||||
/**
|
/**
|
||||||
* HTTP router configuration
|
* HTTP router configuration
|
||||||
*/
|
*/
|
||||||
export interface RouterConfig {
|
export interface IRouterConfig {
|
||||||
routes: Array<{
|
routes: Array<{
|
||||||
path: string;
|
path: string;
|
||||||
handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
|
handler: (req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse) => void;
|
||||||
@ -102,5 +102,4 @@ export interface RouterConfig {
|
|||||||
|
|
||||||
// Backward compatibility interfaces
|
// Backward compatibility interfaces
|
||||||
export { HttpError as Port80HandlerError };
|
export { HttpError as Port80HandlerError };
|
||||||
export { CertificateError as CertError };
|
export { CertificateError as CertError };
|
||||||
export type IDomainCertificate = DomainCertificate;
|
|
@ -7,7 +7,7 @@ import * as plugins from '../../plugins.js';
|
|||||||
/**
|
/**
|
||||||
* Structure for SmartAcme certificate result
|
* Structure for SmartAcme certificate result
|
||||||
*/
|
*/
|
||||||
export interface SmartAcmeCert {
|
export interface ISmartAcmeCert {
|
||||||
id?: string;
|
id?: string;
|
||||||
domainName: string;
|
domainName: string;
|
||||||
created?: number | Date | string;
|
created?: number | Date | string;
|
||||||
@ -20,7 +20,7 @@ export interface SmartAcmeCert {
|
|||||||
/**
|
/**
|
||||||
* Structure for SmartAcme options
|
* Structure for SmartAcme options
|
||||||
*/
|
*/
|
||||||
export interface SmartAcmeOptions {
|
export interface ISmartAcmeOptions {
|
||||||
accountEmail: string;
|
accountEmail: string;
|
||||||
certManager: ICertManager;
|
certManager: ICertManager;
|
||||||
environment: 'production' | 'integration';
|
environment: 'production' | 'integration';
|
||||||
@ -39,8 +39,8 @@ export interface SmartAcmeOptions {
|
|||||||
*/
|
*/
|
||||||
export interface ICertManager {
|
export interface ICertManager {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
get(domainName: string): Promise<SmartAcmeCert | null>;
|
get(domainName: string): Promise<ISmartAcmeCert | null>;
|
||||||
put(cert: SmartAcmeCert): Promise<SmartAcmeCert>;
|
put(cert: ISmartAcmeCert): Promise<ISmartAcmeCert>;
|
||||||
delete(domainName: string): Promise<void>;
|
delete(domainName: string): Promise<void>;
|
||||||
close?(): Promise<void>;
|
close?(): Promise<void>;
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ export interface IChallengeHandler<T> {
|
|||||||
/**
|
/**
|
||||||
* HTTP-01 challenge type
|
* HTTP-01 challenge type
|
||||||
*/
|
*/
|
||||||
export interface Http01Challenge {
|
export interface IHttp01Challenge {
|
||||||
type: string; // 'http-01'
|
type: string; // 'http-01'
|
||||||
token: string;
|
token: string;
|
||||||
keyAuthorization: string;
|
keyAuthorization: string;
|
||||||
@ -69,17 +69,17 @@ export interface Http01Challenge {
|
|||||||
/**
|
/**
|
||||||
* HTTP-01 Memory Handler Interface
|
* HTTP-01 Memory Handler Interface
|
||||||
*/
|
*/
|
||||||
export interface Http01MemoryHandler extends IChallengeHandler<Http01Challenge> {
|
export interface IHttp01MemoryHandler extends IChallengeHandler<IHttp01Challenge> {
|
||||||
handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void;
|
handleRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, next?: () => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SmartAcme main class interface
|
* SmartAcme main class interface
|
||||||
*/
|
*/
|
||||||
export interface SmartAcme {
|
export interface ISmartAcme {
|
||||||
start(): Promise<void>;
|
start(): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
getCertificateForDomain(domain: string): Promise<SmartAcmeCert>;
|
getCertificateForDomain(domain: string): Promise<ISmartAcmeCert>;
|
||||||
on?(event: string, listener: (data: any) => void): void;
|
on?(event: string, listener: (data: any) => void): void;
|
||||||
eventEmitter?: plugins.EventEmitter;
|
eventEmitter?: plugins.EventEmitter;
|
||||||
}
|
}
|
@ -4,15 +4,15 @@ import {
|
|||||||
CertificateEvents
|
CertificateEvents
|
||||||
} from '../../certificate/events/certificate-events.js';
|
} from '../../certificate/events/certificate-events.js';
|
||||||
import type {
|
import type {
|
||||||
CertificateData,
|
ICertificateData,
|
||||||
CertificateFailure,
|
ICertificateFailure,
|
||||||
CertificateExpiring
|
ICertificateExpiring
|
||||||
} from '../../certificate/models/certificate-types.js';
|
} from '../../certificate/models/certificate-types.js';
|
||||||
import type {
|
import type {
|
||||||
SmartAcme,
|
ISmartAcme,
|
||||||
SmartAcmeCert,
|
ISmartAcmeCert,
|
||||||
SmartAcmeOptions,
|
ISmartAcmeOptions,
|
||||||
Http01MemoryHandler
|
IHttp01MemoryHandler
|
||||||
} from './acme-interfaces.js';
|
} from './acme-interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,8 +20,8 @@ import type {
|
|||||||
* It acts as a bridge between the HTTP server and the ACME challenge verification process
|
* It acts as a bridge between the HTTP server and the ACME challenge verification process
|
||||||
*/
|
*/
|
||||||
export class ChallengeResponder extends plugins.EventEmitter {
|
export class ChallengeResponder extends plugins.EventEmitter {
|
||||||
private smartAcme: SmartAcme | null = null;
|
private smartAcme: ISmartAcme | null = null;
|
||||||
private http01Handler: Http01MemoryHandler | null = null;
|
private http01Handler: IHttp01MemoryHandler | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new challenge responder
|
* Creates a new challenge responder
|
||||||
@ -95,7 +95,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
emitter.on('certificate', (data: any) => {
|
emitter.on('certificate', (data: any) => {
|
||||||
const isRenewal = !!data.isRenewal;
|
const isRenewal = !!data.isRenewal;
|
||||||
|
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain: data.domainName || data.domain,
|
domain: data.domainName || data.domain,
|
||||||
certificate: data.publicKey || data.cert,
|
certificate: data.publicKey || data.cert,
|
||||||
privateKey: data.privateKey || data.key,
|
privateKey: data.privateKey || data.key,
|
||||||
@ -114,7 +114,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
// Forward error events
|
// Forward error events
|
||||||
emitter.on('error', (error: any) => {
|
emitter.on('error', (error: any) => {
|
||||||
const domain = error.domainName || error.domain || 'unknown';
|
const domain = error.domainName || error.domain || 'unknown';
|
||||||
const failureData: CertificateFailure = {
|
const failureData: ICertificateFailure = {
|
||||||
domain,
|
domain,
|
||||||
error: error.message || String(error),
|
error: error.message || String(error),
|
||||||
isRenewal: !!error.isRenewal
|
isRenewal: !!error.isRenewal
|
||||||
@ -171,7 +171,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
* @param domain Domain name to request a certificate for
|
* @param domain Domain name to request a certificate for
|
||||||
* @param isRenewal Whether this is a renewal request
|
* @param isRenewal Whether this is a renewal request
|
||||||
*/
|
*/
|
||||||
public async requestCertificate(domain: string, isRenewal: boolean = false): Promise<CertificateData> {
|
public async requestCertificate(domain: string, isRenewal: boolean = false): Promise<ICertificateData> {
|
||||||
if (!this.smartAcme) {
|
if (!this.smartAcme) {
|
||||||
throw new Error('ACME client not initialized');
|
throw new Error('ACME client not initialized');
|
||||||
}
|
}
|
||||||
@ -181,7 +181,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
const certObj = await this.smartAcme.getCertificateForDomain(domain);
|
||||||
|
|
||||||
// Convert the certificate object to our CertificateData format
|
// Convert the certificate object to our CertificateData format
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain,
|
domain,
|
||||||
certificate: certObj.publicKey,
|
certificate: certObj.publicKey,
|
||||||
privateKey: certObj.privateKey,
|
privateKey: certObj.privateKey,
|
||||||
@ -193,7 +193,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
return certData;
|
return certData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Create failure object
|
// Create failure object
|
||||||
const failure: CertificateFailure = {
|
const failure: ICertificateFailure = {
|
||||||
domain,
|
domain,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
isRenewal
|
isRenewal
|
||||||
@ -217,7 +217,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public checkCertificateExpiry(
|
public checkCertificateExpiry(
|
||||||
domain: string,
|
domain: string,
|
||||||
certificate: CertificateData,
|
certificate: ICertificateData,
|
||||||
thresholdDays: number = 30
|
thresholdDays: number = 30
|
||||||
): void {
|
): void {
|
||||||
if (!certificate.expiryDate) return;
|
if (!certificate.expiryDate) return;
|
||||||
@ -227,7 +227,7 @@ export class ChallengeResponder extends plugins.EventEmitter {
|
|||||||
const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
const daysDifference = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
if (daysDifference <= thresholdDays) {
|
if (daysDifference <= thresholdDays) {
|
||||||
const expiryInfo: CertificateExpiring = {
|
const expiryInfo: ICertificateExpiring = {
|
||||||
domain,
|
domain,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
daysRemaining: daysDifference
|
daysRemaining: daysDifference
|
||||||
|
@ -2,12 +2,12 @@ import * as plugins from '../../plugins.js';
|
|||||||
import { IncomingMessage, ServerResponse } from 'http';
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
|
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
|
||||||
import type {
|
import type {
|
||||||
ForwardConfig,
|
IForwardConfig,
|
||||||
DomainOptions,
|
IDomainOptions,
|
||||||
CertificateData,
|
ICertificateData,
|
||||||
CertificateFailure,
|
ICertificateFailure,
|
||||||
CertificateExpiring,
|
ICertificateExpiring,
|
||||||
AcmeOptions
|
IAcmeOptions
|
||||||
} from '../../certificate/models/certificate-types.js';
|
} from '../../certificate/models/certificate-types.js';
|
||||||
import {
|
import {
|
||||||
HttpEvents,
|
HttpEvents,
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
CertificateError,
|
CertificateError,
|
||||||
ServerError,
|
ServerError,
|
||||||
} from '../models/http-types.js';
|
} from '../models/http-types.js';
|
||||||
import type { DomainCertificate } from '../models/http-types.js';
|
import type { IDomainCertificate } from '../models/http-types.js';
|
||||||
import { ChallengeResponder } from './challenge-responder.js';
|
import { ChallengeResponder } from './challenge-responder.js';
|
||||||
|
|
||||||
// Re-export for backward compatibility
|
// Re-export for backward compatibility
|
||||||
@ -40,21 +40,21 @@ export const Port80HandlerEvents = CertificateEvents;
|
|||||||
* Now with glob pattern support for domain matching
|
* Now with glob pattern support for domain matching
|
||||||
*/
|
*/
|
||||||
export class Port80Handler extends plugins.EventEmitter {
|
export class Port80Handler extends plugins.EventEmitter {
|
||||||
private domainCertificates: Map<string, DomainCertificate>;
|
private domainCertificates: Map<string, IDomainCertificate>;
|
||||||
private challengeResponder: ChallengeResponder | null = null;
|
private challengeResponder: ChallengeResponder | null = null;
|
||||||
private server: plugins.http.Server | null = null;
|
private server: plugins.http.Server | null = null;
|
||||||
|
|
||||||
// Renewal scheduling is handled externally by SmartProxy
|
// Renewal scheduling is handled externally by SmartProxy
|
||||||
private isShuttingDown: boolean = false;
|
private isShuttingDown: boolean = false;
|
||||||
private options: Required<AcmeOptions>;
|
private options: Required<IAcmeOptions>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Port80Handler
|
* Creates a new Port80Handler
|
||||||
* @param options Configuration options
|
* @param options Configuration options
|
||||||
*/
|
*/
|
||||||
constructor(options: AcmeOptions = {}) {
|
constructor(options: IAcmeOptions = {}) {
|
||||||
super();
|
super();
|
||||||
this.domainCertificates = new Map<string, DomainCertificate>();
|
this.domainCertificates = new Map<string, IDomainCertificate>();
|
||||||
|
|
||||||
// Default options
|
// Default options
|
||||||
this.options = {
|
this.options = {
|
||||||
@ -80,19 +80,19 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Forward certificate events from the challenge responder
|
// Forward certificate events from the challenge responder
|
||||||
this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: CertificateData) => {
|
this.challengeResponder.on(CertificateEvents.CERTIFICATE_ISSUED, (data: ICertificateData) => {
|
||||||
this.emit(CertificateEvents.CERTIFICATE_ISSUED, data);
|
this.emit(CertificateEvents.CERTIFICATE_ISSUED, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: CertificateData) => {
|
this.challengeResponder.on(CertificateEvents.CERTIFICATE_RENEWED, (data: ICertificateData) => {
|
||||||
this.emit(CertificateEvents.CERTIFICATE_RENEWED, data);
|
this.emit(CertificateEvents.CERTIFICATE_RENEWED, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: CertificateFailure) => {
|
this.challengeResponder.on(CertificateEvents.CERTIFICATE_FAILED, (error: ICertificateFailure) => {
|
||||||
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
|
this.emit(CertificateEvents.CERTIFICATE_FAILED, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: CertificateExpiring) => {
|
this.challengeResponder.on(CertificateEvents.CERTIFICATE_EXPIRING, (expiry: ICertificateExpiring) => {
|
||||||
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry);
|
this.emit(CertificateEvents.CERTIFICATE_EXPIRING, expiry);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -198,7 +198,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
* Adds a domain with configuration options
|
* Adds a domain with configuration options
|
||||||
* @param options Domain configuration options
|
* @param options Domain configuration options
|
||||||
*/
|
*/
|
||||||
public addDomain(options: DomainOptions): void {
|
public addDomain(options: IDomainOptions): void {
|
||||||
if (!options.domainName || typeof options.domainName !== 'string') {
|
if (!options.domainName || typeof options.domainName !== 'string') {
|
||||||
throw new HttpError('Invalid domain name');
|
throw new HttpError('Invalid domain name');
|
||||||
}
|
}
|
||||||
@ -247,7 +247,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
* Gets the certificate for a domain if it exists
|
* Gets the certificate for a domain if it exists
|
||||||
* @param domain The domain to get the certificate for
|
* @param domain The domain to get the certificate for
|
||||||
*/
|
*/
|
||||||
public getCertificate(domain: string): CertificateData | null {
|
public getCertificate(domain: string): ICertificateData | null {
|
||||||
// Can't get certificates for glob patterns
|
// Can't get certificates for glob patterns
|
||||||
if (this.isGlobPattern(domain)) {
|
if (this.isGlobPattern(domain)) {
|
||||||
return null;
|
return null;
|
||||||
@ -283,7 +283,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
* @param requestDomain The actual domain from the request
|
* @param requestDomain The actual domain from the request
|
||||||
* @returns The domain info or null if not found
|
* @returns The domain info or null if not found
|
||||||
*/
|
*/
|
||||||
private getDomainInfoForRequest(requestDomain: string): { domainInfo: DomainCertificate, pattern: string } | null {
|
private getDomainInfoForRequest(requestDomain: string): { domainInfo: IDomainCertificate, pattern: string } | null {
|
||||||
// Try direct match first
|
// Try direct match first
|
||||||
if (this.domainCertificates.has(requestDomain)) {
|
if (this.domainCertificates.has(requestDomain)) {
|
||||||
return {
|
return {
|
||||||
@ -459,7 +459,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
private forwardRequest(
|
private forwardRequest(
|
||||||
req: plugins.http.IncomingMessage,
|
req: plugins.http.IncomingMessage,
|
||||||
res: plugins.http.ServerResponse,
|
res: plugins.http.ServerResponse,
|
||||||
target: ForwardConfig,
|
target: IForwardConfig,
|
||||||
requestType: string
|
requestType: string
|
||||||
): void {
|
): void {
|
||||||
const options = {
|
const options = {
|
||||||
@ -612,7 +612,7 @@ export class Port80Handler extends plugins.EventEmitter {
|
|||||||
* @param eventType The event type to emit
|
* @param eventType The event type to emit
|
||||||
* @param data The certificate data
|
* @param data The certificate data
|
||||||
*/
|
*/
|
||||||
private emitCertificateEvent(eventType: CertificateEvents, data: CertificateData): void {
|
private emitCertificateEvent(eventType: CertificateEvents, data: ICertificateData): void {
|
||||||
this.emit(eventType, data);
|
this.emit(eventType, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { ReverseProxyConfig } from '../../proxies/network-proxy/models/types.js';
|
import type { IReverseProxyConfig } from '../../proxies/network-proxy/models/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional path pattern configuration that can be added to proxy configs
|
* Optional path pattern configuration that can be added to proxy configs
|
||||||
@ -15,7 +15,7 @@ export type IPathPatternConfig = PathPatternConfig;
|
|||||||
* Interface for router result with additional metadata
|
* Interface for router result with additional metadata
|
||||||
*/
|
*/
|
||||||
export interface RouterResult {
|
export interface RouterResult {
|
||||||
config: ReverseProxyConfig;
|
config: IReverseProxyConfig;
|
||||||
pathMatch?: string;
|
pathMatch?: string;
|
||||||
pathParams?: Record<string, string>;
|
pathParams?: Record<string, string>;
|
||||||
pathRemainder?: string;
|
pathRemainder?: string;
|
||||||
@ -41,11 +41,11 @@ export type IRouterResult = RouterResult;
|
|||||||
*/
|
*/
|
||||||
export class ProxyRouter {
|
export class ProxyRouter {
|
||||||
// Store original configs for reference
|
// Store original configs for reference
|
||||||
private reverseProxyConfigs: ReverseProxyConfig[] = [];
|
private reverseProxyConfigs: IReverseProxyConfig[] = [];
|
||||||
// Default config to use when no match is found (optional)
|
// Default config to use when no match is found (optional)
|
||||||
private defaultConfig?: ReverseProxyConfig;
|
private defaultConfig?: IReverseProxyConfig;
|
||||||
// Store path patterns separately since they're not in the original interface
|
// Store path patterns separately since they're not in the original interface
|
||||||
private pathPatterns: Map<ReverseProxyConfig, string> = new Map();
|
private pathPatterns: Map<IReverseProxyConfig, string> = new Map();
|
||||||
// Logger interface
|
// Logger interface
|
||||||
private logger: {
|
private logger: {
|
||||||
error: (message: string, data?: any) => void;
|
error: (message: string, data?: any) => void;
|
||||||
@ -55,7 +55,7 @@ export class ProxyRouter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
configs?: ReverseProxyConfig[],
|
configs?: IReverseProxyConfig[],
|
||||||
logger?: {
|
logger?: {
|
||||||
error: (message: string, data?: any) => void;
|
error: (message: string, data?: any) => void;
|
||||||
warn: (message: string, data?: any) => void;
|
warn: (message: string, data?: any) => void;
|
||||||
@ -73,7 +73,7 @@ export class ProxyRouter {
|
|||||||
* Sets a new set of reverse configs to be routed to
|
* Sets a new set of reverse configs to be routed to
|
||||||
* @param reverseCandidatesArg Array of reverse proxy configurations
|
* @param reverseCandidatesArg Array of reverse proxy configurations
|
||||||
*/
|
*/
|
||||||
public setNewProxyConfigs(reverseCandidatesArg: ReverseProxyConfig[]): void {
|
public setNewProxyConfigs(reverseCandidatesArg: IReverseProxyConfig[]): void {
|
||||||
this.reverseProxyConfigs = [...reverseCandidatesArg];
|
this.reverseProxyConfigs = [...reverseCandidatesArg];
|
||||||
|
|
||||||
// Find default config if any (config with "*" as hostname)
|
// Find default config if any (config with "*" as hostname)
|
||||||
@ -87,7 +87,7 @@ export class ProxyRouter {
|
|||||||
* @param req The incoming HTTP request
|
* @param req The incoming HTTP request
|
||||||
* @returns The matching proxy config or undefined if no match found
|
* @returns The matching proxy config or undefined if no match found
|
||||||
*/
|
*/
|
||||||
public routeReq(req: plugins.http.IncomingMessage): ReverseProxyConfig {
|
public routeReq(req: plugins.http.IncomingMessage): IReverseProxyConfig {
|
||||||
const result = this.routeReqWithDetails(req);
|
const result = this.routeReqWithDetails(req);
|
||||||
return result ? result.config : undefined;
|
return result ? result.config : undefined;
|
||||||
}
|
}
|
||||||
@ -356,7 +356,7 @@ export class ProxyRouter {
|
|||||||
* Gets all currently active proxy configurations
|
* Gets all currently active proxy configurations
|
||||||
* @returns Array of all active configurations
|
* @returns Array of all active configurations
|
||||||
*/
|
*/
|
||||||
public getProxyConfigs(): ReverseProxyConfig[] {
|
public getProxyConfigs(): IReverseProxyConfig[] {
|
||||||
return [...this.reverseProxyConfigs];
|
return [...this.reverseProxyConfigs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,7 +380,7 @@ export class ProxyRouter {
|
|||||||
* @param pathPattern Optional path pattern for route matching
|
* @param pathPattern Optional path pattern for route matching
|
||||||
*/
|
*/
|
||||||
public addProxyConfig(
|
public addProxyConfig(
|
||||||
config: ReverseProxyConfig,
|
config: IReverseProxyConfig,
|
||||||
pathPattern?: string
|
pathPattern?: string
|
||||||
): void {
|
): void {
|
||||||
this.reverseProxyConfigs.push(config);
|
this.reverseProxyConfigs.push(config);
|
||||||
@ -398,7 +398,7 @@ export class ProxyRouter {
|
|||||||
* @returns Boolean indicating if the config was found and updated
|
* @returns Boolean indicating if the config was found and updated
|
||||||
*/
|
*/
|
||||||
public setPathPattern(
|
public setPathPattern(
|
||||||
config: ReverseProxyConfig,
|
config: IReverseProxyConfig,
|
||||||
pathPattern: string
|
pathPattern: string
|
||||||
): boolean {
|
): boolean {
|
||||||
const exists = this.reverseProxyConfigs.includes(config);
|
const exists = this.reverseProxyConfigs.includes(config);
|
||||||
|
@ -2,26 +2,26 @@ import * as plugins from '../../plugins.js';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { type NetworkProxyOptions, type CertificateEntry, type Logger, createLogger } from './models/types.js';
|
import { type INetworkProxyOptions, type ICertificateEntry, type ILogger, createLogger } from './models/types.js';
|
||||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||||
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
|
import { CertificateEvents } from '../../certificate/events/certificate-events.js';
|
||||||
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
|
import { buildPort80Handler } from '../../certificate/acme/acme-factory.js';
|
||||||
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
||||||
import type { DomainOptions } from '../../certificate/models/certificate-types.js';
|
import type { IDomainOptions } from '../../certificate/models/certificate-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages SSL certificates for NetworkProxy including ACME integration
|
* Manages SSL certificates for NetworkProxy including ACME integration
|
||||||
*/
|
*/
|
||||||
export class CertificateManager {
|
export class CertificateManager {
|
||||||
private defaultCertificates: { key: string; cert: string };
|
private defaultCertificates: { key: string; cert: string };
|
||||||
private certificateCache: Map<string, CertificateEntry> = new Map();
|
private certificateCache: Map<string, ICertificateEntry> = new Map();
|
||||||
private port80Handler: Port80Handler | null = null;
|
private port80Handler: Port80Handler | null = null;
|
||||||
private externalPort80Handler: boolean = false;
|
private externalPort80Handler: boolean = false;
|
||||||
private certificateStoreDir: string;
|
private certificateStoreDir: string;
|
||||||
private logger: Logger;
|
private logger: ILogger;
|
||||||
private httpsServer: plugins.https.Server | null = null;
|
private httpsServer: plugins.https.Server | null = null;
|
||||||
|
|
||||||
constructor(private options: NetworkProxyOptions) {
|
constructor(private options: INetworkProxyOptions) {
|
||||||
this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
|
this.certificateStoreDir = path.resolve(options.acme?.certificateStore || './certs');
|
||||||
this.logger = createLogger(options.logLevel || 'info');
|
this.logger = createLogger(options.logLevel || 'info');
|
||||||
|
|
||||||
@ -219,9 +219,9 @@ export class CertificateManager {
|
|||||||
|
|
||||||
if (!certData) {
|
if (!certData) {
|
||||||
this.logger.info(`No certificate found for ${domain}, registering for issuance`);
|
this.logger.info(`No certificate found for ${domain}, registering for issuance`);
|
||||||
|
|
||||||
// Register with new domain options format
|
// Register with new domain options format
|
||||||
const domainOptions: DomainOptions = {
|
const domainOptions: IDomainOptions = {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
sslRedirect: true,
|
sslRedirect: true,
|
||||||
acmeMaintenance: true
|
acmeMaintenance: true
|
||||||
@ -274,7 +274,7 @@ export class CertificateManager {
|
|||||||
/**
|
/**
|
||||||
* Gets a certificate for a domain
|
* Gets a certificate for a domain
|
||||||
*/
|
*/
|
||||||
public getCertificate(domain: string): CertificateEntry | undefined {
|
public getCertificate(domain: string): ICertificateEntry | undefined {
|
||||||
return this.certificateCache.get(domain);
|
return this.certificateCache.get(domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +300,7 @@ export class CertificateManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use the new domain options format
|
// Use the new domain options format
|
||||||
const domainOptions: DomainOptions = {
|
const domainOptions: IDomainOptions = {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
sslRedirect: true,
|
sslRedirect: true,
|
||||||
acmeMaintenance: true
|
acmeMaintenance: true
|
||||||
@ -341,7 +341,7 @@ export class CertificateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register the domain for certificate issuance with new domain options format
|
// Register the domain for certificate issuance with new domain options format
|
||||||
const domainOptions: DomainOptions = {
|
const domainOptions: IDomainOptions = {
|
||||||
domainName: domain,
|
domainName: domain,
|
||||||
sslRedirect: true,
|
sslRedirect: true,
|
||||||
acmeMaintenance: true
|
acmeMaintenance: true
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { type NetworkProxyOptions, type ConnectionEntry, type Logger, createLogger } from './models/types.js';
|
import { type INetworkProxyOptions, type IConnectionEntry, type ILogger, createLogger } from './models/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages a pool of backend connections for efficient reuse
|
* Manages a pool of backend connections for efficient reuse
|
||||||
*/
|
*/
|
||||||
export class ConnectionPool {
|
export class ConnectionPool {
|
||||||
private connectionPool: Map<string, Array<ConnectionEntry>> = new Map();
|
private connectionPool: Map<string, Array<IConnectionEntry>> = new Map();
|
||||||
private roundRobinPositions: Map<string, number> = new Map();
|
private roundRobinPositions: Map<string, number> = new Map();
|
||||||
private logger: Logger;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(private options: NetworkProxyOptions) {
|
constructor(private options: INetworkProxyOptions) {
|
||||||
this.logger = createLogger(options.logLevel || 'info');
|
this.logger = createLogger(options.logLevel || 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
import type { AcmeOptions } from '../../../certificate/models/certificate-types.js';
|
import type { IAcmeOptions } from '../../../certificate/models/certificate-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for NetworkProxy
|
* Configuration options for NetworkProxy
|
||||||
*/
|
*/
|
||||||
export interface NetworkProxyOptions {
|
export interface INetworkProxyOptions {
|
||||||
port: number;
|
port: number;
|
||||||
maxConnections?: number;
|
maxConnections?: number;
|
||||||
keepAliveTimeout?: number;
|
keepAliveTimeout?: number;
|
||||||
@ -16,22 +16,22 @@ export interface NetworkProxyOptions {
|
|||||||
allowHeaders?: string;
|
allowHeaders?: string;
|
||||||
maxAge?: number;
|
maxAge?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Settings for SmartProxy integration
|
// Settings for SmartProxy integration
|
||||||
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
connectionPoolSize?: number; // Maximum connections to maintain in the pool to each backend
|
||||||
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy
|
portProxyIntegration?: boolean; // Flag to indicate this proxy is used by SmartProxy
|
||||||
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
|
useExternalPort80Handler?: boolean; // Flag to indicate using external Port80Handler
|
||||||
// Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
|
// Protocol to use when proxying to backends: HTTP/1.x or HTTP/2
|
||||||
backendProtocol?: 'http1' | 'http2';
|
backendProtocol?: 'http1' | 'http2';
|
||||||
|
|
||||||
// ACME certificate management options
|
// ACME certificate management options
|
||||||
acme?: AcmeOptions;
|
acme?: IAcmeOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for a certificate entry in the cache
|
* Interface for a certificate entry in the cache
|
||||||
*/
|
*/
|
||||||
export interface CertificateEntry {
|
export interface ICertificateEntry {
|
||||||
key: string;
|
key: string;
|
||||||
cert: string;
|
cert: string;
|
||||||
expires?: Date;
|
expires?: Date;
|
||||||
@ -40,7 +40,7 @@ export interface CertificateEntry {
|
|||||||
/**
|
/**
|
||||||
* Interface for reverse proxy configuration
|
* Interface for reverse proxy configuration
|
||||||
*/
|
*/
|
||||||
export interface ReverseProxyConfig {
|
export interface IReverseProxyConfig {
|
||||||
destinationIps: string[];
|
destinationIps: string[];
|
||||||
destinationPorts: number[];
|
destinationPorts: number[];
|
||||||
hostName: string;
|
hostName: string;
|
||||||
@ -62,7 +62,7 @@ export interface ReverseProxyConfig {
|
|||||||
/**
|
/**
|
||||||
* Interface for connection tracking in the pool
|
* Interface for connection tracking in the pool
|
||||||
*/
|
*/
|
||||||
export interface ConnectionEntry {
|
export interface IConnectionEntry {
|
||||||
socket: plugins.net.Socket;
|
socket: plugins.net.Socket;
|
||||||
lastUsed: number;
|
lastUsed: number;
|
||||||
isIdle: boolean;
|
isIdle: boolean;
|
||||||
@ -71,7 +71,7 @@ export interface ConnectionEntry {
|
|||||||
/**
|
/**
|
||||||
* WebSocket with heartbeat interface
|
* WebSocket with heartbeat interface
|
||||||
*/
|
*/
|
||||||
export interface WebSocketWithHeartbeat extends plugins.wsDefault {
|
export interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
||||||
lastPong: number;
|
lastPong: number;
|
||||||
isAlive: boolean;
|
isAlive: boolean;
|
||||||
}
|
}
|
||||||
@ -79,7 +79,7 @@ export interface WebSocketWithHeartbeat extends plugins.wsDefault {
|
|||||||
/**
|
/**
|
||||||
* Logger interface for consistent logging across components
|
* Logger interface for consistent logging across components
|
||||||
*/
|
*/
|
||||||
export interface Logger {
|
export interface ILogger {
|
||||||
debug(message: string, data?: any): void;
|
debug(message: string, data?: any): void;
|
||||||
info(message: string, data?: any): void;
|
info(message: string, data?: any): void;
|
||||||
warn(message: string, data?: any): void;
|
warn(message: string, data?: any): void;
|
||||||
@ -89,14 +89,14 @@ export interface Logger {
|
|||||||
/**
|
/**
|
||||||
* Creates a logger based on the specified log level
|
* Creates a logger based on the specified log level
|
||||||
*/
|
*/
|
||||||
export function createLogger(logLevel: string = 'info'): Logger {
|
export function createLogger(logLevel: string = 'info'): ILogger {
|
||||||
const logLevels = {
|
const logLevels = {
|
||||||
error: 0,
|
error: 0,
|
||||||
warn: 1,
|
warn: 1,
|
||||||
info: 2,
|
info: 2,
|
||||||
debug: 3
|
debug: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
debug: (message: string, data?: any) => {
|
debug: (message: string, data?: any) => {
|
||||||
if (logLevels[logLevel] >= logLevels.debug) {
|
if (logLevels[logLevel] >= logLevels.debug) {
|
||||||
@ -119,12 +119,4 @@ export function createLogger(logLevel: string = 'info'): Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility interfaces
|
|
||||||
export interface INetworkProxyOptions extends NetworkProxyOptions {}
|
|
||||||
export interface ICertificateEntry extends CertificateEntry {}
|
|
||||||
export interface IReverseProxyConfig extends ReverseProxyConfig {}
|
|
||||||
export interface IConnectionEntry extends ConnectionEntry {}
|
|
||||||
export interface IWebSocketWithHeartbeat extends WebSocketWithHeartbeat {}
|
|
||||||
export interface ILogger extends Logger {}
|
|
@ -3,9 +3,9 @@ import {
|
|||||||
createLogger
|
createLogger
|
||||||
} from './models/types.js';
|
} from './models/types.js';
|
||||||
import type {
|
import type {
|
||||||
NetworkProxyOptions,
|
INetworkProxyOptions,
|
||||||
Logger,
|
ILogger,
|
||||||
ReverseProxyConfig
|
IReverseProxyConfig
|
||||||
} from './models/types.js';
|
} from './models/types.js';
|
||||||
import { CertificateManager } from './certificate-manager.js';
|
import { CertificateManager } from './certificate-manager.js';
|
||||||
import { ConnectionPool } from './connection-pool.js';
|
import { ConnectionPool } from './connection-pool.js';
|
||||||
@ -24,8 +24,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
// Configuration
|
// Configuration
|
||||||
public options: NetworkProxyOptions;
|
public options: INetworkProxyOptions;
|
||||||
public proxyConfigs: ReverseProxyConfig[] = [];
|
public proxyConfigs: IReverseProxyConfig[] = [];
|
||||||
|
|
||||||
// Server instances (HTTP/2 with HTTP/1 fallback)
|
// Server instances (HTTP/2 with HTTP/1 fallback)
|
||||||
public httpsServer: any;
|
public httpsServer: any;
|
||||||
@ -54,12 +54,12 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
private connectionPoolCleanupInterval: NodeJS.Timeout;
|
private connectionPoolCleanupInterval: NodeJS.Timeout;
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
private logger: Logger;
|
private logger: ILogger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new NetworkProxy instance
|
* Creates a new NetworkProxy instance
|
||||||
*/
|
*/
|
||||||
constructor(optionsArg: NetworkProxyOptions) {
|
constructor(optionsArg: INetworkProxyOptions) {
|
||||||
// Set default options
|
// Set default options
|
||||||
this.options = {
|
this.options = {
|
||||||
port: optionsArg.port,
|
port: optionsArg.port,
|
||||||
@ -328,7 +328,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
* Updates proxy configurations
|
* Updates proxy configurations
|
||||||
*/
|
*/
|
||||||
public async updateProxyConfigs(
|
public async updateProxyConfigs(
|
||||||
proxyConfigsArg: ReverseProxyConfig[]
|
proxyConfigsArg: IReverseProxyConfig[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`);
|
this.logger.info(`Updating proxy configurations (${proxyConfigsArg.length} configs)`);
|
||||||
|
|
||||||
@ -385,8 +385,8 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
allowedIPs?: string[];
|
allowedIPs?: string[];
|
||||||
}>,
|
}>,
|
||||||
sslKeyPair?: { key: string; cert: string }
|
sslKeyPair?: { key: string; cert: string }
|
||||||
): ReverseProxyConfig[] {
|
): IReverseProxyConfig[] {
|
||||||
const proxyConfigs: ReverseProxyConfig[] = [];
|
const proxyConfigs: IReverseProxyConfig[] = [];
|
||||||
|
|
||||||
// Use default certificates if not provided
|
// Use default certificates if not provided
|
||||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
||||||
@ -478,7 +478,7 @@ export class NetworkProxy implements IMetricsTracker {
|
|||||||
/**
|
/**
|
||||||
* Gets all proxy configurations currently in use
|
* Gets all proxy configurations currently in use
|
||||||
*/
|
*/
|
||||||
public getProxyConfigs(): ReverseProxyConfig[] {
|
public getProxyConfigs(): IReverseProxyConfig[] {
|
||||||
return [...this.proxyConfigs];
|
return [...this.proxyConfigs];
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { type NetworkProxyOptions, type Logger, createLogger, type ReverseProxyConfig } from './models/types.js';
|
import { type INetworkProxyOptions, type ILogger, createLogger, type IReverseProxyConfig } from './models/types.js';
|
||||||
import { ConnectionPool } from './connection-pool.js';
|
import { ConnectionPool } from './connection-pool.js';
|
||||||
import { ProxyRouter } from '../../http/router/index.js';
|
import { ProxyRouter } from '../../http/router/index.js';
|
||||||
|
|
||||||
@ -19,13 +19,13 @@ export type MetricsTracker = IMetricsTracker;
|
|||||||
*/
|
*/
|
||||||
export class RequestHandler {
|
export class RequestHandler {
|
||||||
private defaultHeaders: { [key: string]: string } = {};
|
private defaultHeaders: { [key: string]: string } = {};
|
||||||
private logger: Logger;
|
private logger: ILogger;
|
||||||
private metricsTracker: IMetricsTracker | null = null;
|
private metricsTracker: IMetricsTracker | null = null;
|
||||||
// HTTP/2 client sessions for backend proxying
|
// HTTP/2 client sessions for backend proxying
|
||||||
private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
|
private h2Sessions: Map<string, plugins.http2.ClientHttp2Session> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private options: NetworkProxyOptions,
|
private options: INetworkProxyOptions,
|
||||||
private connectionPool: ConnectionPool,
|
private connectionPool: ConnectionPool,
|
||||||
private router: ProxyRouter
|
private router: ProxyRouter
|
||||||
) {
|
) {
|
||||||
@ -137,7 +137,7 @@ export class RequestHandler {
|
|||||||
this.applyDefaultHeaders(res);
|
this.applyDefaultHeaders(res);
|
||||||
|
|
||||||
// Determine routing configuration
|
// Determine routing configuration
|
||||||
let proxyConfig: ReverseProxyConfig | undefined;
|
let proxyConfig: IReverseProxyConfig | undefined;
|
||||||
try {
|
try {
|
||||||
proxyConfig = this.router.routeReq(req);
|
proxyConfig = this.router.routeReq(req);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -235,7 +235,7 @@ export class RequestHandler {
|
|||||||
// Remove host header to avoid issues with virtual hosts on target server
|
// Remove host header to avoid issues with virtual hosts on target server
|
||||||
// The host header should match the target server's expected hostname
|
// The host header should match the target server's expected hostname
|
||||||
if (options.headers && options.headers.host) {
|
if (options.headers && options.headers.host) {
|
||||||
if ((proxyConfig as ReverseProxyConfig).rewriteHostHeader) {
|
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
||||||
options.headers.host = `${destination.host}:${destination.port}`;
|
options.headers.host = `${destination.host}:${destination.port}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,7 +426,7 @@ export class RequestHandler {
|
|||||||
outboundHeaders[key] = value;
|
outboundHeaders[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (outboundHeaders.host && (proxyConfig as any).rewriteHostHeader) {
|
if (outboundHeaders.host && (proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
||||||
outboundHeaders.host = `${destination.host}:${destination.port}`;
|
outboundHeaders.host = `${destination.host}:${destination.port}`;
|
||||||
}
|
}
|
||||||
// Create HTTP/1 proxy request
|
// Create HTTP/1 proxy request
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { type NetworkProxyOptions, type WebSocketWithHeartbeat, type Logger, createLogger, type ReverseProxyConfig } from './models/types.js';
|
import { type INetworkProxyOptions, type IWebSocketWithHeartbeat, type ILogger, createLogger, type IReverseProxyConfig } from './models/types.js';
|
||||||
import { ConnectionPool } from './connection-pool.js';
|
import { ConnectionPool } from './connection-pool.js';
|
||||||
import { ProxyRouter } from '../../http/router/index.js';
|
import { ProxyRouter } from '../../http/router/index.js';
|
||||||
|
|
||||||
@ -9,10 +9,10 @@ import { ProxyRouter } from '../../http/router/index.js';
|
|||||||
export class WebSocketHandler {
|
export class WebSocketHandler {
|
||||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
private wsServer: plugins.ws.WebSocketServer | null = null;
|
private wsServer: plugins.ws.WebSocketServer | null = null;
|
||||||
private logger: Logger;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private options: NetworkProxyOptions,
|
private options: INetworkProxyOptions,
|
||||||
private connectionPool: ConnectionPool,
|
private connectionPool: ConnectionPool,
|
||||||
private router: ProxyRouter
|
private router: ProxyRouter
|
||||||
) {
|
) {
|
||||||
@ -30,7 +30,7 @@ export class WebSocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle WebSocket connections
|
// Handle WebSocket connections
|
||||||
this.wsServer.on('connection', (wsIncoming: WebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => {
|
this.wsServer.on('connection', (wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage) => {
|
||||||
this.handleWebSocketConnection(wsIncoming, req);
|
this.handleWebSocketConnection(wsIncoming, req);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,9 +56,9 @@ export class WebSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
|
this.logger.debug(`WebSocket heartbeat check for ${this.wsServer.clients.size} clients`);
|
||||||
|
|
||||||
this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
this.wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
||||||
const wsWithHeartbeat = ws as WebSocketWithHeartbeat;
|
const wsWithHeartbeat = ws as IWebSocketWithHeartbeat;
|
||||||
|
|
||||||
if (wsWithHeartbeat.isAlive === false) {
|
if (wsWithHeartbeat.isAlive === false) {
|
||||||
this.logger.debug('Terminating inactive WebSocket connection');
|
this.logger.debug('Terminating inactive WebSocket connection');
|
||||||
@ -79,7 +79,7 @@ export class WebSocketHandler {
|
|||||||
/**
|
/**
|
||||||
* Handle a new WebSocket connection
|
* Handle a new WebSocket connection
|
||||||
*/
|
*/
|
||||||
private handleWebSocketConnection(wsIncoming: WebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void {
|
private handleWebSocketConnection(wsIncoming: IWebSocketWithHeartbeat, req: plugins.http.IncomingMessage): void {
|
||||||
try {
|
try {
|
||||||
// Initialize heartbeat tracking
|
// Initialize heartbeat tracking
|
||||||
wsIncoming.isAlive = true;
|
wsIncoming.isAlive = true;
|
||||||
@ -127,7 +127,7 @@ export class WebSocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Override host header if needed
|
// Override host header if needed
|
||||||
if ((proxyConfig as ReverseProxyConfig).rewriteHostHeader) {
|
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
||||||
headers['host'] = `${destination.host}:${destination.port}`;
|
headers['host'] = `${destination.host}:${destination.port}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type {
|
import type {
|
||||||
ConnectionRecord,
|
IConnectionRecord,
|
||||||
DomainConfig,
|
IDomainConfig,
|
||||||
SmartProxyOptions,
|
ISmartProxyOptions,
|
||||||
} from './models/interfaces.js';
|
} from './models/interfaces.js';
|
||||||
import { ConnectionManager } from './connection-manager.js';
|
import { ConnectionManager } from './connection-manager.js';
|
||||||
import { SecurityManager } from './security-manager.js';
|
import { SecurityManager } from './security-manager.js';
|
||||||
@ -12,14 +12,14 @@ 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 type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
|
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
|
||||||
import type { ForwardingType } from '../../forwarding/config/forwarding-types.js';
|
import type { TForwardingType } from '../../forwarding/config/forwarding-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles new connection processing and setup logic
|
* Handles new connection processing and setup logic
|
||||||
*/
|
*/
|
||||||
export class ConnectionHandler {
|
export class ConnectionHandler {
|
||||||
constructor(
|
constructor(
|
||||||
private settings: SmartProxyOptions,
|
private settings: ISmartProxyOptions,
|
||||||
private connectionManager: ConnectionManager,
|
private connectionManager: ConnectionManager,
|
||||||
private securityManager: SecurityManager,
|
private securityManager: SecurityManager,
|
||||||
private domainConfigManager: DomainConfigManager,
|
private domainConfigManager: DomainConfigManager,
|
||||||
@ -102,7 +102,7 @@ export class ConnectionHandler {
|
|||||||
*/
|
*/
|
||||||
private handleNetworkProxyConnection(
|
private handleNetworkProxyConnection(
|
||||||
socket: plugins.net.Socket,
|
socket: plugins.net.Socket,
|
||||||
record: ConnectionRecord
|
record: IConnectionRecord
|
||||||
): void {
|
): void {
|
||||||
const connectionId = record.id;
|
const connectionId = record.id;
|
||||||
let initialDataReceived = false;
|
let initialDataReceived = false;
|
||||||
@ -307,7 +307,7 @@ export class ConnectionHandler {
|
|||||||
/**
|
/**
|
||||||
* Handle a standard (non-NetworkProxy) connection
|
* Handle a standard (non-NetworkProxy) connection
|
||||||
*/
|
*/
|
||||||
private handleStandardConnection(socket: plugins.net.Socket, record: ConnectionRecord): void {
|
private handleStandardConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
|
||||||
const connectionId = record.id;
|
const connectionId = record.id;
|
||||||
const localPort = record.localPort;
|
const localPort = record.localPort;
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ export class ConnectionHandler {
|
|||||||
const setupConnection = (
|
const setupConnection = (
|
||||||
serverName: string,
|
serverName: string,
|
||||||
initialChunk?: Buffer,
|
initialChunk?: Buffer,
|
||||||
forcedDomain?: DomainConfig,
|
forcedDomain?: IDomainConfig,
|
||||||
overridePort?: number
|
overridePort?: number
|
||||||
) => {
|
) => {
|
||||||
// Clear the initial timeout since we've received data
|
// Clear the initial timeout since we've received data
|
||||||
@ -500,7 +500,7 @@ export class ConnectionHandler {
|
|||||||
const globalDomainConfig = {
|
const globalDomainConfig = {
|
||||||
domains: ['global'],
|
domains: ['global'],
|
||||||
forwarding: {
|
forwarding: {
|
||||||
type: 'http-only' as ForwardingType,
|
type: 'http-only' as TForwardingType,
|
||||||
target: {
|
target: {
|
||||||
host: this.settings.targetIP!,
|
host: this.settings.targetIP!,
|
||||||
port: this.settings.toPort
|
port: this.settings.toPort
|
||||||
@ -730,8 +730,8 @@ export class ConnectionHandler {
|
|||||||
*/
|
*/
|
||||||
private setupDirectConnection(
|
private setupDirectConnection(
|
||||||
socket: plugins.net.Socket,
|
socket: plugins.net.Socket,
|
||||||
record: ConnectionRecord,
|
record: IConnectionRecord,
|
||||||
domainConfig?: DomainConfig,
|
domainConfig?: IDomainConfig,
|
||||||
serverName?: string,
|
serverName?: string,
|
||||||
initialChunk?: Buffer,
|
initialChunk?: Buffer,
|
||||||
overridePort?: number
|
overridePort?: number
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { ConnectionRecord, SmartProxyOptions } from './models/interfaces.js';
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||||
import { SecurityManager } from './security-manager.js';
|
import { SecurityManager } from './security-manager.js';
|
||||||
import { TimeoutManager } from './timeout-manager.js';
|
import { TimeoutManager } from './timeout-manager.js';
|
||||||
|
|
||||||
@ -7,14 +7,14 @@ import { TimeoutManager } from './timeout-manager.js';
|
|||||||
* Manages connection lifecycle, tracking, and cleanup
|
* Manages connection lifecycle, tracking, and cleanup
|
||||||
*/
|
*/
|
||||||
export class ConnectionManager {
|
export class ConnectionManager {
|
||||||
private connectionRecords: Map<string, ConnectionRecord> = new Map();
|
private connectionRecords: Map<string, IConnectionRecord> = new Map();
|
||||||
private terminationStats: {
|
private terminationStats: {
|
||||||
incoming: Record<string, number>;
|
incoming: Record<string, number>;
|
||||||
outgoing: Record<string, number>;
|
outgoing: Record<string, number>;
|
||||||
} = { incoming: {}, outgoing: {} };
|
} = { incoming: {}, outgoing: {} };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private settings: SmartProxyOptions,
|
private settings: ISmartProxyOptions,
|
||||||
private securityManager: SecurityManager,
|
private securityManager: SecurityManager,
|
||||||
private timeoutManager: TimeoutManager
|
private timeoutManager: TimeoutManager
|
||||||
) {}
|
) {}
|
||||||
@ -30,12 +30,12 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Create and track a new connection
|
* Create and track a new connection
|
||||||
*/
|
*/
|
||||||
public createConnection(socket: plugins.net.Socket): ConnectionRecord {
|
public createConnection(socket: plugins.net.Socket): IConnectionRecord {
|
||||||
const connectionId = this.generateConnectionId();
|
const connectionId = this.generateConnectionId();
|
||||||
const remoteIP = socket.remoteAddress || '';
|
const remoteIP = socket.remoteAddress || '';
|
||||||
const localPort = socket.localPort || 0;
|
const localPort = socket.localPort || 0;
|
||||||
|
|
||||||
const record: ConnectionRecord = {
|
const record: IConnectionRecord = {
|
||||||
id: connectionId,
|
id: connectionId,
|
||||||
incoming: socket,
|
incoming: socket,
|
||||||
outgoing: null,
|
outgoing: null,
|
||||||
@ -66,7 +66,7 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Track an existing connection
|
* Track an existing connection
|
||||||
*/
|
*/
|
||||||
public trackConnection(connectionId: string, record: ConnectionRecord): void {
|
public trackConnection(connectionId: string, record: IConnectionRecord): void {
|
||||||
this.connectionRecords.set(connectionId, record);
|
this.connectionRecords.set(connectionId, record);
|
||||||
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
||||||
}
|
}
|
||||||
@ -74,14 +74,14 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Get a connection by ID
|
* Get a connection by ID
|
||||||
*/
|
*/
|
||||||
public getConnection(connectionId: string): ConnectionRecord | undefined {
|
public getConnection(connectionId: string): IConnectionRecord | undefined {
|
||||||
return this.connectionRecords.get(connectionId);
|
return this.connectionRecords.get(connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all active connections
|
* Get all active connections
|
||||||
*/
|
*/
|
||||||
public getConnections(): Map<string, ConnectionRecord> {
|
public getConnections(): Map<string, IConnectionRecord> {
|
||||||
return this.connectionRecords;
|
return this.connectionRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Initiates cleanup once for a connection
|
* Initiates cleanup once for a connection
|
||||||
*/
|
*/
|
||||||
public initiateCleanupOnce(record: ConnectionRecord, reason: string = 'normal'): void {
|
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
||||||
}
|
}
|
||||||
@ -110,11 +110,11 @@ export class ConnectionManager {
|
|||||||
|
|
||||||
this.cleanupConnection(record, reason);
|
this.cleanupConnection(record, reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up a connection record
|
* Clean up a connection record
|
||||||
*/
|
*/
|
||||||
public cleanupConnection(record: ConnectionRecord, reason: string = 'normal'): void {
|
public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
|
||||||
if (!record.connectionClosed) {
|
if (!record.connectionClosed) {
|
||||||
record.connectionClosed = true;
|
record.connectionClosed = true;
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Helper method to clean up a socket
|
* Helper method to clean up a socket
|
||||||
*/
|
*/
|
||||||
private cleanupSocket(record: ConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
|
private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
|
||||||
try {
|
try {
|
||||||
if (!socket.destroyed) {
|
if (!socket.destroyed) {
|
||||||
// Try graceful shutdown first, then force destroy after a short timeout
|
// Try graceful shutdown first, then force destroy after a short timeout
|
||||||
@ -213,7 +213,7 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Creates a generic error handler for incoming or outgoing sockets
|
* Creates a generic error handler for incoming or outgoing sockets
|
||||||
*/
|
*/
|
||||||
public handleError(side: 'incoming' | 'outgoing', record: ConnectionRecord) {
|
public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
||||||
return (err: Error) => {
|
return (err: Error) => {
|
||||||
const code = (err as any).code;
|
const code = (err as any).code;
|
||||||
let reason = 'error';
|
let reason = 'error';
|
||||||
@ -256,7 +256,7 @@ export class ConnectionManager {
|
|||||||
/**
|
/**
|
||||||
* Creates a generic close handler for incoming or outgoing sockets
|
* Creates a generic close handler for incoming or outgoing sockets
|
||||||
*/
|
*/
|
||||||
public handleClose(side: 'incoming' | 'outgoing', record: ConnectionRecord) {
|
public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
||||||
return () => {
|
return () => {
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (this.settings.enableDetailedLogging) {
|
||||||
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
|
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
|
||||||
|
@ -1,26 +1,127 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { DomainConfig, SmartProxyOptions } from './models/interfaces.js';
|
import type { IDomainConfig, ISmartProxyOptions } from './models/interfaces.js';
|
||||||
import type { ForwardingType, ForwardConfig } 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
|
||||||
*/
|
*/
|
||||||
export class DomainConfigManager {
|
export class DomainConfigManager {
|
||||||
// Track round-robin indices for domain configs
|
// Track round-robin indices for domain configs
|
||||||
private domainTargetIndices: Map<DomainConfig, number> = new Map();
|
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
||||||
|
|
||||||
// Cache forwarding handlers for each domain config
|
// Cache forwarding handlers for each domain config
|
||||||
private forwardingHandlers: Map<DomainConfig, ForwardingHandler> = new Map();
|
private forwardingHandlers: Map<IDomainConfig, ForwardingHandler> = new Map();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the domain configurations
|
* Updates the domain configurations
|
||||||
*/
|
*/
|
||||||
public updateDomainConfigs(newDomainConfigs: DomainConfig[]): 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);
|
||||||
@ -31,7 +132,7 @@ export class DomainConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clear handlers for removed configs and create handlers for new configs
|
// Clear handlers for removed configs and create handlers for new configs
|
||||||
const handlersToRemove: DomainConfig[] = [];
|
const handlersToRemove: IDomainConfig[] = [];
|
||||||
for (const [config] of this.forwardingHandlers) {
|
for (const [config] of this.forwardingHandlers) {
|
||||||
if (!currentConfigSet.has(config)) {
|
if (!currentConfigSet.has(config)) {
|
||||||
handlersToRemove.push(config);
|
handlersToRemove.push(config);
|
||||||
@ -55,37 +156,79 @@ export class DomainConfigManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all domain configurations
|
* Get all domain configurations
|
||||||
*/
|
*/
|
||||||
public getDomainConfigs(): DomainConfig[] {
|
public getDomainConfigs(): IDomainConfig[] {
|
||||||
return this.settings.domainConfigs;
|
// Use domainConfigs from settings if available, otherwise use derived configs
|
||||||
|
return this.settings.domainConfigs || this.derivedDomainConfigs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find domain config matching a server name
|
* Find domain config matching a server name
|
||||||
*/
|
*/
|
||||||
public findDomainConfig(serverName: string): DomainConfig | 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): DomainConfig | 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,7 +241,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Get target IP with round-robin support
|
* Get target IP with round-robin support
|
||||||
*/
|
*/
|
||||||
public getTargetIP(domainConfig: DomainConfig): string {
|
public getTargetIP(domainConfig: IDomainConfig): string {
|
||||||
const targetHosts = Array.isArray(domainConfig.forwarding.target.host)
|
const targetHosts = Array.isArray(domainConfig.forwarding.target.host)
|
||||||
? domainConfig.forwarding.target.host
|
? domainConfig.forwarding.target.host
|
||||||
: [domainConfig.forwarding.target.host];
|
: [domainConfig.forwarding.target.host];
|
||||||
@ -117,21 +260,21 @@ export class DomainConfigManager {
|
|||||||
* Get target host with round-robin support (for tests)
|
* Get target host with round-robin support (for tests)
|
||||||
* This is just an alias for getTargetIP for easier test compatibility
|
* This is just an alias for getTargetIP for easier test compatibility
|
||||||
*/
|
*/
|
||||||
public getTargetHost(domainConfig: DomainConfig): string {
|
public getTargetHost(domainConfig: IDomainConfig): string {
|
||||||
return this.getTargetIP(domainConfig);
|
return this.getTargetIP(domainConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get target port from domain config
|
* Get target port from domain config
|
||||||
*/
|
*/
|
||||||
public getTargetPort(domainConfig: DomainConfig, defaultPort: number): number {
|
public getTargetPort(domainConfig: IDomainConfig, defaultPort: number): number {
|
||||||
return domainConfig.forwarding.target.port || defaultPort;
|
return domainConfig.forwarding.target.port || defaultPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a domain should use NetworkProxy
|
* Checks if a domain should use NetworkProxy
|
||||||
*/
|
*/
|
||||||
public shouldUseNetworkProxy(domainConfig: DomainConfig): boolean {
|
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
|
||||||
const forwardingType = this.getForwardingType(domainConfig);
|
const forwardingType = this.getForwardingType(domainConfig);
|
||||||
return forwardingType === 'https-terminate-to-http' ||
|
return forwardingType === 'https-terminate-to-http' ||
|
||||||
forwardingType === 'https-terminate-to-https';
|
forwardingType === 'https-terminate-to-https';
|
||||||
@ -140,7 +283,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Gets the NetworkProxy port for a domain
|
* Gets the NetworkProxy port for a domain
|
||||||
*/
|
*/
|
||||||
public getNetworkProxyPort(domainConfig: DomainConfig): number | undefined {
|
public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined {
|
||||||
// First check if we should use NetworkProxy at all
|
// First check if we should use NetworkProxy at all
|
||||||
if (!this.shouldUseNetworkProxy(domainConfig)) {
|
if (!this.shouldUseNetworkProxy(domainConfig)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -148,14 +291,14 @@ export class DomainConfigManager {
|
|||||||
|
|
||||||
return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort;
|
return domainConfig.forwarding.advanced?.networkProxyPort || this.settings.networkProxyPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get effective allowed and blocked IPs for a domain
|
* Get effective allowed and blocked IPs for a domain
|
||||||
*
|
*
|
||||||
* This method combines domain-specific security rules from the forwarding configuration
|
* This method combines domain-specific security rules from the forwarding configuration
|
||||||
* with global security defaults when necessary.
|
* with global security defaults when necessary.
|
||||||
*/
|
*/
|
||||||
public getEffectiveIPRules(domainConfig: DomainConfig): {
|
public getEffectiveIPRules(domainConfig: IDomainConfig): {
|
||||||
allowedIPs: string[],
|
allowedIPs: string[],
|
||||||
blockedIPs: string[]
|
blockedIPs: string[]
|
||||||
} {
|
} {
|
||||||
@ -201,7 +344,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Get connection timeout for a domain
|
* Get connection timeout for a domain
|
||||||
*/
|
*/
|
||||||
public getConnectionTimeout(domainConfig?: DomainConfig): number {
|
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
|
||||||
if (domainConfig?.forwarding.advanced?.timeout) {
|
if (domainConfig?.forwarding.advanced?.timeout) {
|
||||||
return domainConfig.forwarding.advanced.timeout;
|
return domainConfig.forwarding.advanced.timeout;
|
||||||
}
|
}
|
||||||
@ -212,7 +355,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Creates a forwarding handler for a domain configuration
|
* Creates a forwarding handler for a domain configuration
|
||||||
*/
|
*/
|
||||||
private createForwardingHandler(domainConfig: DomainConfig): ForwardingHandler {
|
private createForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
|
||||||
// Create a new handler using the factory
|
// Create a new handler using the factory
|
||||||
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
|
const handler = ForwardingHandlerFactory.createHandler(domainConfig.forwarding);
|
||||||
|
|
||||||
@ -228,7 +371,7 @@ export class DomainConfigManager {
|
|||||||
* Gets a forwarding handler for a domain config
|
* Gets a forwarding handler for a domain config
|
||||||
* If no handler exists, creates one
|
* If no handler exists, creates one
|
||||||
*/
|
*/
|
||||||
public getForwardingHandler(domainConfig: DomainConfig): ForwardingHandler {
|
public getForwardingHandler(domainConfig: IDomainConfig): ForwardingHandler {
|
||||||
// If we already have a handler, return it
|
// If we already have a handler, return it
|
||||||
if (this.forwardingHandlers.has(domainConfig)) {
|
if (this.forwardingHandlers.has(domainConfig)) {
|
||||||
return this.forwardingHandlers.get(domainConfig)!;
|
return this.forwardingHandlers.get(domainConfig)!;
|
||||||
@ -244,7 +387,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Gets the forwarding type for a domain config
|
* Gets the forwarding type for a domain config
|
||||||
*/
|
*/
|
||||||
public getForwardingType(domainConfig?: DomainConfig): ForwardingType | undefined {
|
public getForwardingType(domainConfig?: IDomainConfig): TForwardingType | undefined {
|
||||||
if (!domainConfig?.forwarding) return undefined;
|
if (!domainConfig?.forwarding) return undefined;
|
||||||
return domainConfig.forwarding.type;
|
return domainConfig.forwarding.type;
|
||||||
}
|
}
|
||||||
@ -252,7 +395,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Checks if the forwarding type requires TLS termination
|
* Checks if the forwarding type requires TLS termination
|
||||||
*/
|
*/
|
||||||
public requiresTlsTermination(domainConfig?: DomainConfig): boolean {
|
public requiresTlsTermination(domainConfig?: IDomainConfig): boolean {
|
||||||
if (!domainConfig) return false;
|
if (!domainConfig) return false;
|
||||||
|
|
||||||
const forwardingType = this.getForwardingType(domainConfig);
|
const forwardingType = this.getForwardingType(domainConfig);
|
||||||
@ -263,7 +406,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Checks if the forwarding type supports HTTP
|
* Checks if the forwarding type supports HTTP
|
||||||
*/
|
*/
|
||||||
public supportsHttp(domainConfig?: DomainConfig): boolean {
|
public supportsHttp(domainConfig?: IDomainConfig): boolean {
|
||||||
if (!domainConfig) return false;
|
if (!domainConfig) return false;
|
||||||
|
|
||||||
const forwardingType = this.getForwardingType(domainConfig);
|
const forwardingType = this.getForwardingType(domainConfig);
|
||||||
@ -285,7 +428,7 @@ export class DomainConfigManager {
|
|||||||
/**
|
/**
|
||||||
* Checks if HTTP requests should be redirected to HTTPS
|
* Checks if HTTP requests should be redirected to HTTPS
|
||||||
*/
|
*/
|
||||||
public shouldRedirectToHttps(domainConfig?: DomainConfig): boolean {
|
public shouldRedirectToHttps(domainConfig?: IDomainConfig): boolean {
|
||||||
if (!domainConfig?.forwarding) return false;
|
if (!domainConfig?.forwarding) return false;
|
||||||
|
|
||||||
// Only check for redirect if HTTP is enabled
|
// Only check for redirect if HTTP is enabled
|
@ -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,33 +1,57 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../../plugins.js';
|
||||||
import type { ForwardConfig } 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
|
||||||
*/
|
*/
|
||||||
export type SmartProxyCertProvisionObject = 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 interface DomainConfig {
|
export type IRoutedSmartProxyOptions = ISmartProxyOptions;
|
||||||
domains: string[]; // Glob patterns for domain(s)
|
|
||||||
forwarding: ForwardConfig; // Unified forwarding configuration
|
/**
|
||||||
|
* Helper functions for type checking configuration types
|
||||||
|
*/
|
||||||
|
export function isLegacyOptions(options: any): boolean {
|
||||||
|
// Legacy options are no longer supported
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRoutedOptions(options: any): boolean {
|
||||||
|
// All configurations are now route-based
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for the SmartProxy
|
* SmartProxy configuration options
|
||||||
*/
|
*/
|
||||||
import type { AcmeOptions } from '../../../certificate/models/certificate-types.js';
|
export interface ISmartProxyOptions {
|
||||||
export interface SmartProxyOptions {
|
// The unified configuration array (required)
|
||||||
fromPort: number;
|
routes: IRouteConfig[];
|
||||||
toPort: number;
|
|
||||||
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
// Port range configuration
|
||||||
domainConfigs: DomainConfig[];
|
globalPortRanges?: Array<{ from: number; to: number }>;
|
||||||
sniEnabled?: boolean;
|
forwardAllGlobalRanges?: boolean;
|
||||||
defaultAllowedIPs?: string[];
|
|
||||||
defaultBlockedIPs?: string[];
|
|
||||||
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 +74,6 @@ export interface SmartProxyOptions {
|
|||||||
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)
|
||||||
@ -81,19 +103,19 @@ export interface SmartProxyOptions {
|
|||||||
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
||||||
|
|
||||||
// ACME configuration options for SmartProxy
|
// ACME configuration options for SmartProxy
|
||||||
acme?: AcmeOptions;
|
acme?: IAcmeOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
* Optional certificate provider callback. Return 'http01' to use HTTP-01 challenges,
|
||||||
* or a static certificate object for immediate provisioning.
|
* or a static certificate object for immediate provisioning.
|
||||||
*/
|
*/
|
||||||
certProvisionFunction?: (domain: string) => Promise<SmartProxyCertProvisionObject>;
|
certProvisionFunction?: (domain: string) => Promise<TSmartProxyCertProvisionObject>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced connection record
|
* Enhanced connection record
|
||||||
*/
|
*/
|
||||||
export interface ConnectionRecord {
|
export interface IConnectionRecord {
|
||||||
id: string; // Unique connection identifier
|
id: string; // Unique connection identifier
|
||||||
incoming: plugins.net.Socket;
|
incoming: plugins.net.Socket;
|
||||||
outgoing: plugins.net.Socket | null;
|
outgoing: plugins.net.Socket | null;
|
||||||
@ -116,7 +138,7 @@ export interface ConnectionRecord {
|
|||||||
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?: DomainConfig; // 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
|
||||||
@ -133,10 +155,4 @@ export interface ConnectionRecord {
|
|||||||
// Browser connection tracking
|
// Browser connection tracking
|
||||||
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
||||||
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility types
|
|
||||||
export type ISmartProxyCertProvisionObject = SmartProxyCertProvisionObject;
|
|
||||||
export interface IDomainConfig extends DomainConfig {}
|
|
||||||
export interface ISmartProxyOptions extends SmartProxyOptions {}
|
|
||||||
export interface IConnectionRecord extends ConnectionRecord {}
|
|
223
ts/proxies/smart-proxy/models/route-types.ts
Normal file
223
ts/proxies/smart-proxy/models/route-types.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authentication options
|
||||||
|
*/
|
||||||
|
export interface IRouteAuthentication {
|
||||||
|
type: 'basic' | 'digest' | 'oauth' | 'jwt';
|
||||||
|
credentials?: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}[];
|
||||||
|
realm?: string;
|
||||||
|
jwtSecret?: string;
|
||||||
|
jwtIssuer?: string;
|
||||||
|
oauthProvider?: string;
|
||||||
|
oauthClientId?: string;
|
||||||
|
oauthClientSecret?: string;
|
||||||
|
oauthRedirectUri?: string;
|
||||||
|
[key: string]: any; // Allow additional auth-specific options
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security options for route actions
|
||||||
|
*/
|
||||||
|
export interface IRouteSecurity {
|
||||||
|
allowedIps?: string[];
|
||||||
|
blockedIps?: string[];
|
||||||
|
maxConnections?: number;
|
||||||
|
authentication?: IRouteAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static file server configuration
|
||||||
|
*/
|
||||||
|
export interface IRouteStaticFiles {
|
||||||
|
directory: string;
|
||||||
|
indexFiles?: string[];
|
||||||
|
cacheControl?: string;
|
||||||
|
expires?: number;
|
||||||
|
followSymlinks?: boolean;
|
||||||
|
disableDirectoryListing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test route response configuration
|
||||||
|
*/
|
||||||
|
export interface IRouteTestResponse {
|
||||||
|
status: number;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced options for route actions
|
||||||
|
*/
|
||||||
|
export interface IRouteAdvanced {
|
||||||
|
timeout?: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
keepAlive?: boolean;
|
||||||
|
staticFiles?: IRouteStaticFiles;
|
||||||
|
testResponse?: IRouteTestResponse;
|
||||||
|
// 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>;
|
||||||
|
}
|
@ -3,17 +3,26 @@ import { NetworkProxy } from '../network-proxy/index.js';
|
|||||||
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
import { Port80Handler } from '../../http/port80/port80-handler.js';
|
||||||
import { Port80HandlerEvents } from '../../core/models/common-types.js';
|
import { Port80HandlerEvents } from '../../core/models/common-types.js';
|
||||||
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
import { subscribeToPort80Handler } from '../../core/utils/event-utils.js';
|
||||||
import type { CertificateData } from '../../certificate/models/certificate-types.js';
|
import type { ICertificateData } from '../../certificate/models/certificate-types.js';
|
||||||
import type { ConnectionRecord, SmartProxyOptions, DomainConfig } from './models/interfaces.js';
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||||
|
import type { IRouteConfig } from './models/route-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages NetworkProxy integration for TLS termination
|
* Manages NetworkProxy integration for TLS termination
|
||||||
|
*
|
||||||
|
* NetworkProxyBridge connects SmartProxy with NetworkProxy to handle TLS termination.
|
||||||
|
* It converts route configurations to NetworkProxy configuration format and manages
|
||||||
|
* certificate provisioning through Port80Handler when ACME is enabled.
|
||||||
|
*
|
||||||
|
* It is used by SmartProxy for routes that have:
|
||||||
|
* - TLS mode of 'terminate' or 'terminate-and-reencrypt'
|
||||||
|
* - Certificate set to 'auto' or custom certificate
|
||||||
*/
|
*/
|
||||||
export class NetworkProxyBridge {
|
export class NetworkProxyBridge {
|
||||||
private networkProxy: NetworkProxy | null = null;
|
private networkProxy: NetworkProxy | null = null;
|
||||||
private port80Handler: Port80Handler | null = null;
|
private port80Handler: Port80Handler | null = null;
|
||||||
|
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
constructor(private settings: ISmartProxyOptions) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the Port80Handler to use for certificate management
|
* Set the Port80Handler to use for certificate management
|
||||||
@ -58,31 +67,31 @@ export class NetworkProxyBridge {
|
|||||||
this.networkProxy.setExternalPort80Handler(this.port80Handler);
|
this.networkProxy.setExternalPort80Handler(this.port80Handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert and apply domain configurations to NetworkProxy
|
// Apply route configurations to NetworkProxy
|
||||||
await this.syncDomainConfigsToNetworkProxy();
|
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle certificate issuance or renewal events
|
* Handle certificate issuance or renewal events
|
||||||
*/
|
*/
|
||||||
private handleCertificateEvent(data: CertificateData): void {
|
private handleCertificateEvent(data: ICertificateData): void {
|
||||||
if (!this.networkProxy) return;
|
if (!this.networkProxy) return;
|
||||||
|
|
||||||
console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`);
|
console.log(`Received certificate for ${data.domain} from Port80Handler, updating NetworkProxy`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find existing config for this domain
|
// Find existing config for this domain
|
||||||
const existingConfigs = this.networkProxy.getProxyConfigs()
|
const existingConfigs = this.networkProxy.getProxyConfigs()
|
||||||
.filter(config => config.hostName === data.domain);
|
.filter(config => config.hostName === data.domain);
|
||||||
|
|
||||||
if (existingConfigs.length > 0) {
|
if (existingConfigs.length > 0) {
|
||||||
// Update existing configs with new certificate
|
// Update existing configs with new certificate
|
||||||
for (const config of existingConfigs) {
|
for (const config of existingConfigs) {
|
||||||
config.privateKey = data.privateKey;
|
config.privateKey = data.privateKey;
|
||||||
config.publicKey = data.certificate;
|
config.publicKey = data.certificate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply updated configs
|
// Apply updated configs
|
||||||
this.networkProxy.updateProxyConfigs(existingConfigs)
|
this.networkProxy.updateProxyConfigs(existingConfigs)
|
||||||
.then(() => console.log(`Updated certificate for ${data.domain} in NetworkProxy`))
|
.then(() => console.log(`Updated certificate for ${data.domain} in NetworkProxy`))
|
||||||
@ -95,11 +104,11 @@ export class NetworkProxyBridge {
|
|||||||
console.log(`Error handling certificate event: ${err}`);
|
console.log(`Error handling certificate event: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply an external (static) certificate into NetworkProxy
|
* Apply an external (static) certificate into NetworkProxy
|
||||||
*/
|
*/
|
||||||
public applyExternalCertificate(data: CertificateData): void {
|
public applyExternalCertificate(data: ICertificateData): void {
|
||||||
if (!this.networkProxy) {
|
if (!this.networkProxy) {
|
||||||
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
console.log(`NetworkProxy not initialized: cannot apply external certificate for ${data.domain}`);
|
||||||
return;
|
return;
|
||||||
@ -183,7 +192,7 @@ export class NetworkProxyBridge {
|
|||||||
public forwardToNetworkProxy(
|
public forwardToNetworkProxy(
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
socket: plugins.net.Socket,
|
socket: plugins.net.Socket,
|
||||||
record: ConnectionRecord,
|
record: IConnectionRecord,
|
||||||
initialData: Buffer,
|
initialData: Buffer,
|
||||||
customProxyPort?: number,
|
customProxyPort?: number,
|
||||||
onError?: (reason: string) => void
|
onError?: (reason: string) => void
|
||||||
@ -249,9 +258,19 @@ export class NetworkProxyBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronizes domain configurations to NetworkProxy
|
* Synchronizes routes to NetworkProxy
|
||||||
|
*
|
||||||
|
* This method converts route configurations to NetworkProxy format and updates
|
||||||
|
* the NetworkProxy with the converted configurations. It handles:
|
||||||
|
*
|
||||||
|
* - Extracting domain, target, and certificate information from routes
|
||||||
|
* - Converting TLS mode settings to NetworkProxy configuration
|
||||||
|
* - Applying security and advanced settings
|
||||||
|
* - Registering domains for ACME certificate provisioning when needed
|
||||||
|
*
|
||||||
|
* @param routes The route configurations to sync to NetworkProxy
|
||||||
*/
|
*/
|
||||||
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
|
public async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
|
||||||
if (!this.networkProxy) {
|
if (!this.networkProxy) {
|
||||||
console.log('Cannot sync configurations - NetworkProxy not initialized');
|
console.log('Cannot sync configurations - NetworkProxy not initialized');
|
||||||
return;
|
return;
|
||||||
@ -282,38 +301,106 @@ export class NetworkProxyBridge {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert domain configs to NetworkProxy configs
|
// Convert routes to NetworkProxy configs
|
||||||
const proxyConfigs = this.networkProxy.convertSmartProxyConfigs(
|
const proxyConfigs = this.convertRoutesToNetworkProxyConfigs(routes, certPair);
|
||||||
this.settings.domainConfigs,
|
|
||||||
certPair
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log ACME-eligible domains
|
// Update the proxy configs
|
||||||
const acmeEnabled = !!this.settings.acme?.enabled;
|
|
||||||
if (acmeEnabled) {
|
|
||||||
const acmeEligibleDomains = proxyConfigs
|
|
||||||
.filter((config) => !config.hostName.includes('*')) // Exclude wildcards
|
|
||||||
.map((config) => config.hostName);
|
|
||||||
|
|
||||||
if (acmeEligibleDomains.length > 0) {
|
|
||||||
console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
|
|
||||||
|
|
||||||
// Register these domains with Port80Handler if available
|
|
||||||
if (this.port80Handler) {
|
|
||||||
this.registerDomainsWithPort80Handler(acmeEligibleDomains);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('No domains eligible for ACME certificates found in configuration');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update NetworkProxy with the converted configs
|
|
||||||
await this.networkProxy.updateProxyConfigs(proxyConfigs);
|
await this.networkProxy.updateProxyConfigs(proxyConfigs);
|
||||||
console.log(`Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`);
|
console.log(`Synced ${proxyConfigs.length} configurations to NetworkProxy`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`Failed to sync configurations: ${err}`);
|
console.log(`Error syncing routes to NetworkProxy: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert routes to NetworkProxy configuration format
|
||||||
|
*
|
||||||
|
* This method transforms route-based configuration to NetworkProxy's configuration format.
|
||||||
|
* It processes each route and creates appropriate NetworkProxy configs for domains
|
||||||
|
* that require TLS termination.
|
||||||
|
*
|
||||||
|
* @param routes Array of route configurations to convert
|
||||||
|
* @param defaultCertPair Default certificate to use if no custom certificate is specified
|
||||||
|
* @returns Array of NetworkProxy configurations
|
||||||
|
*/
|
||||||
|
public convertRoutesToNetworkProxyConfigs(
|
||||||
|
routes: IRouteConfig[],
|
||||||
|
defaultCertPair: { key: string; cert: string }
|
||||||
|
): plugins.tsclass.network.IReverseProxyConfig[] {
|
||||||
|
const configs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||||
|
|
||||||
|
for (const route of routes) {
|
||||||
|
// Skip routes without domains
|
||||||
|
if (!route.match.domains) continue;
|
||||||
|
|
||||||
|
// Skip non-forward routes
|
||||||
|
if (route.action.type !== 'forward') continue;
|
||||||
|
|
||||||
|
// Skip routes without TLS configuration
|
||||||
|
if (!route.action.tls || !route.action.target) continue;
|
||||||
|
|
||||||
|
// Get domains from route
|
||||||
|
const domains = Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains
|
||||||
|
: [route.match.domains];
|
||||||
|
|
||||||
|
// Create a config for each domain
|
||||||
|
for (const domain of domains) {
|
||||||
|
// Determine if this route requires TLS termination
|
||||||
|
const needsTermination = route.action.tls.mode === 'terminate' ||
|
||||||
|
route.action.tls.mode === 'terminate-and-reencrypt';
|
||||||
|
|
||||||
|
// Skip passthrough domains for NetworkProxy
|
||||||
|
if (route.action.tls.mode === 'passthrough') continue;
|
||||||
|
|
||||||
|
// Get certificate
|
||||||
|
let certKey = defaultCertPair.key;
|
||||||
|
let certCert = defaultCertPair.cert;
|
||||||
|
|
||||||
|
// Use custom certificate if specified
|
||||||
|
if (route.action.tls.certificate !== 'auto' && typeof route.action.tls.certificate === 'object') {
|
||||||
|
certKey = route.action.tls.certificate.key;
|
||||||
|
certCert = route.action.tls.certificate.cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine target hosts and ports
|
||||||
|
const targetHosts = Array.isArray(route.action.target.host)
|
||||||
|
? route.action.target.host
|
||||||
|
: [route.action.target.host];
|
||||||
|
|
||||||
|
const targetPort = route.action.target.port;
|
||||||
|
|
||||||
|
// Create NetworkProxy config
|
||||||
|
const config: plugins.tsclass.network.IReverseProxyConfig = {
|
||||||
|
hostName: domain,
|
||||||
|
privateKey: certKey,
|
||||||
|
publicKey: certCert,
|
||||||
|
destinationIps: targetHosts,
|
||||||
|
destinationPorts: [targetPort],
|
||||||
|
// Use backendProtocol for TLS re-encryption:
|
||||||
|
backendProtocol: route.action.tls.mode === 'terminate-and-reencrypt' ? 'http2' : 'http1',
|
||||||
|
// Add rewriteHostHeader for host header handling:
|
||||||
|
rewriteHostHeader: route.action.advanced?.headers ? true : false
|
||||||
|
};
|
||||||
|
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This method is deprecated and will be removed in a future version.
|
||||||
|
* Use syncRoutesToNetworkProxy() instead.
|
||||||
|
*
|
||||||
|
* This legacy method exists only for backward compatibility and
|
||||||
|
* simply forwards to syncRoutesToNetworkProxy().
|
||||||
|
*/
|
||||||
|
public async syncDomainConfigsToNetworkProxy(): Promise<void> {
|
||||||
|
console.log('Method syncDomainConfigsToNetworkProxy is deprecated. Use syncRoutesToNetworkProxy instead.');
|
||||||
|
await this.syncRoutesToNetworkProxy(this.settings.routes || []);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a certificate for a specific domain
|
* Request a certificate for a specific domain
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { SmartProxyOptions } from './models/interfaces.js';
|
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages port ranges and port-based configuration
|
* Manages port ranges and port-based configuration
|
||||||
*/
|
*/
|
||||||
export class PortRangeManager {
|
export class PortRangeManager {
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
constructor(private settings: ISmartProxyOptions) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all ports that should be listened on
|
* Get all ports that should be listened on
|
954
ts/proxies/smart-proxy/route-connection-handler.ts
Normal file
954
ts/proxies/smart-proxy/route-connection-handler.ts
Normal file
@ -0,0 +1,954 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type {
|
||||||
|
IConnectionRecord,
|
||||||
|
ISmartProxyOptions
|
||||||
|
} from './models/interfaces.js';
|
||||||
|
import {
|
||||||
|
isRoutedOptions
|
||||||
|
} from './models/interfaces.js';
|
||||||
|
import type {
|
||||||
|
IRouteConfig,
|
||||||
|
IRouteAction
|
||||||
|
} from './models/route-types.js';
|
||||||
|
import { ConnectionManager } from './connection-manager.js';
|
||||||
|
import { SecurityManager } from './security-manager.js';
|
||||||
|
import { TlsManager } from './tls-manager.js';
|
||||||
|
import { NetworkProxyBridge } from './network-proxy-bridge.js';
|
||||||
|
import { TimeoutManager } from './timeout-manager.js';
|
||||||
|
import { RouteManager } from './route-manager.js';
|
||||||
|
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles new connection processing and setup logic with support for route-based configuration
|
||||||
|
*/
|
||||||
|
export class RouteConnectionHandler {
|
||||||
|
private settings: ISmartProxyOptions;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
settings: ISmartProxyOptions,
|
||||||
|
private connectionManager: ConnectionManager,
|
||||||
|
private securityManager: SecurityManager,
|
||||||
|
private tlsManager: TlsManager,
|
||||||
|
private networkProxyBridge: NetworkProxyBridge,
|
||||||
|
private timeoutManager: TimeoutManager,
|
||||||
|
private routeManager: RouteManager
|
||||||
|
) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a new incoming connection
|
||||||
|
*/
|
||||||
|
public handleConnection(socket: plugins.net.Socket): void {
|
||||||
|
const remoteIP = socket.remoteAddress || '';
|
||||||
|
const localPort = socket.localPort || 0;
|
||||||
|
|
||||||
|
// Validate IP against rate limits and connection limits
|
||||||
|
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||||
|
if (!ipValidation.allowed) {
|
||||||
|
console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
|
||||||
|
socket.end();
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new connection record
|
||||||
|
const record = this.connectionManager.createConnection(socket);
|
||||||
|
const connectionId = record.id;
|
||||||
|
|
||||||
|
// Apply socket optimizations
|
||||||
|
socket.setNoDelay(this.settings.noDelay);
|
||||||
|
|
||||||
|
// Apply keep-alive settings if enabled
|
||||||
|
if (this.settings.keepAlive) {
|
||||||
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
||||||
|
record.hasKeepAlive = true;
|
||||||
|
|
||||||
|
// Apply enhanced TCP keep-alive options if enabled
|
||||||
|
if (this.settings.enableKeepAliveProbes) {
|
||||||
|
try {
|
||||||
|
// These are platform-specific and may not be available
|
||||||
|
if ('setKeepAliveProbes' in socket) {
|
||||||
|
(socket as any).setKeepAliveProbes(10);
|
||||||
|
}
|
||||||
|
if ('setKeepAliveInterval' in socket) {
|
||||||
|
(socket as any).setKeepAliveInterval(1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore errors - these are optional enhancements
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
||||||
|
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
||||||
|
`Active connections: ${this.connectionManager.getConnectionCount()}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start TLS SNI handling
|
||||||
|
this.handleTlsConnection(socket, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a connection and wait for TLS handshake for SNI extraction if needed
|
||||||
|
*/
|
||||||
|
private handleTlsConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
const localPort = record.localPort;
|
||||||
|
let initialDataReceived = false;
|
||||||
|
|
||||||
|
// Set an initial timeout for handshake data
|
||||||
|
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
|
if (!initialDataReceived) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a grace period
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!initialDataReceived) {
|
||||||
|
console.log(`[${connectionId}] Final initial data timeout after grace period`);
|
||||||
|
if (record.incomingTerminationReason === null) {
|
||||||
|
record.incomingTerminationReason = 'initial_timeout';
|
||||||
|
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'initial_timeout');
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
}, this.settings.initialDataTimeout!);
|
||||||
|
|
||||||
|
// Make sure timeout doesn't keep the process alive
|
||||||
|
if (initialTimeout.unref) {
|
||||||
|
initialTimeout.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up error handler
|
||||||
|
socket.on('error', this.connectionManager.handleError('incoming', record));
|
||||||
|
|
||||||
|
// First data handler to capture initial TLS handshake
|
||||||
|
socket.once('data', (chunk: Buffer) => {
|
||||||
|
// Clear the initial timeout since we've received data
|
||||||
|
if (initialTimeout) {
|
||||||
|
clearTimeout(initialTimeout);
|
||||||
|
initialTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
initialDataReceived = true;
|
||||||
|
record.hasReceivedInitialData = true;
|
||||||
|
|
||||||
|
// Block non-TLS connections on port 443
|
||||||
|
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Non-TLS connection detected on port 443. ` +
|
||||||
|
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
|
||||||
|
);
|
||||||
|
if (record.incomingTerminationReason === null) {
|
||||||
|
record.incomingTerminationReason = 'non_tls_blocked';
|
||||||
|
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
|
||||||
|
}
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this looks like a TLS handshake
|
||||||
|
let serverName = '';
|
||||||
|
if (this.tlsManager.isTlsHandshake(chunk)) {
|
||||||
|
record.isTLS = true;
|
||||||
|
|
||||||
|
// Check for ClientHello to extract SNI
|
||||||
|
if (this.tlsManager.isClientHello(chunk)) {
|
||||||
|
// Create connection info for SNI extraction
|
||||||
|
const connInfo = {
|
||||||
|
sourceIp: record.remoteIP,
|
||||||
|
sourcePort: socket.remotePort || 0,
|
||||||
|
destIp: socket.localAddress || '',
|
||||||
|
destPort: socket.localPort || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract SNI
|
||||||
|
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
|
||||||
|
|
||||||
|
// Lock the connection to the negotiated SNI
|
||||||
|
record.lockedDomain = serverName;
|
||||||
|
|
||||||
|
// Check if we should reject connections without SNI
|
||||||
|
if (!serverName && this.settings.allowSessionTicket === false) {
|
||||||
|
console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`);
|
||||||
|
if (record.incomingTerminationReason === null) {
|
||||||
|
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
|
||||||
|
this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
|
||||||
|
}
|
||||||
|
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
|
||||||
|
try {
|
||||||
|
socket.cork();
|
||||||
|
socket.write(alert);
|
||||||
|
socket.uncork();
|
||||||
|
socket.end();
|
||||||
|
} catch {
|
||||||
|
socket.end();
|
||||||
|
}
|
||||||
|
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the appropriate route for this connection
|
||||||
|
this.routeConnection(socket, record, serverName, chunk);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route the connection based on match criteria
|
||||||
|
*/
|
||||||
|
private routeConnection(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
serverName: string,
|
||||||
|
initialChunk?: Buffer
|
||||||
|
): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
const localPort = record.localPort;
|
||||||
|
const remoteIP = record.remoteIP;
|
||||||
|
|
||||||
|
// Find matching route
|
||||||
|
const routeMatch = this.routeManager.findMatchingRoute({
|
||||||
|
port: localPort,
|
||||||
|
domain: serverName,
|
||||||
|
clientIp: remoteIP,
|
||||||
|
path: undefined, // We don't have path info at this point
|
||||||
|
tlsVersion: undefined // We don't extract TLS version yet
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!routeMatch) {
|
||||||
|
console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`);
|
||||||
|
|
||||||
|
// No matching route, use default/fallback handling
|
||||||
|
console.log(`[${connectionId}] Using default route handling for connection`);
|
||||||
|
|
||||||
|
// Check default security settings
|
||||||
|
const defaultSecuritySettings = this.settings.defaults?.security;
|
||||||
|
if (defaultSecuritySettings) {
|
||||||
|
if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) {
|
||||||
|
const isAllowed = this.securityManager.isIPAuthorized(
|
||||||
|
remoteIP,
|
||||||
|
defaultSecuritySettings.allowedIps,
|
||||||
|
defaultSecuritySettings.blockedIps || []
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'ip_blocked');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup direct connection with default settings
|
||||||
|
if (this.settings.defaults?.target) {
|
||||||
|
// Use defaults from configuration
|
||||||
|
const targetHost = this.settings.defaults.target.host;
|
||||||
|
const targetPort = this.settings.defaults.target.port;
|
||||||
|
|
||||||
|
return this.setupDirectConnection(
|
||||||
|
socket,
|
||||||
|
record,
|
||||||
|
undefined,
|
||||||
|
serverName,
|
||||||
|
initialChunk,
|
||||||
|
undefined,
|
||||||
|
targetHost,
|
||||||
|
targetPort
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// No default target available, terminate the connection
|
||||||
|
console.log(`[${connectionId}] No default target configured. Closing connection.`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'no_default_target');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A matching route was found
|
||||||
|
const route = routeMatch.route;
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${serverName || 'connection'} on port ${localPort}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the route based on its action type
|
||||||
|
switch (route.action.type) {
|
||||||
|
case 'forward':
|
||||||
|
return this.handleForwardAction(socket, record, route, initialChunk);
|
||||||
|
|
||||||
|
case 'redirect':
|
||||||
|
return this.handleRedirectAction(socket, record, route);
|
||||||
|
|
||||||
|
case 'block':
|
||||||
|
return this.handleBlockAction(socket, record, route);
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'unknown_action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a forward action for a route
|
||||||
|
*/
|
||||||
|
private handleForwardAction(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
route: IRouteConfig,
|
||||||
|
initialChunk?: Buffer
|
||||||
|
): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
const action = route.action;
|
||||||
|
|
||||||
|
// We should have a target configuration for forwarding
|
||||||
|
if (!action.target) {
|
||||||
|
console.log(`[${connectionId}] Forward action missing target configuration`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'missing_target');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this needs TLS handling
|
||||||
|
if (action.tls) {
|
||||||
|
switch (action.tls.mode) {
|
||||||
|
case 'passthrough':
|
||||||
|
// For TLS passthrough, just forward directly
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] Using TLS passthrough to ${action.target.host}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for array of hosts
|
||||||
|
const targetHost = Array.isArray(action.target.host)
|
||||||
|
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
||||||
|
: action.target.host;
|
||||||
|
|
||||||
|
// Determine target port - either target port or preserve incoming port
|
||||||
|
const targetPort = action.target.preservePort ? record.localPort : action.target.port;
|
||||||
|
|
||||||
|
return this.setupDirectConnection(
|
||||||
|
socket,
|
||||||
|
record,
|
||||||
|
undefined,
|
||||||
|
record.lockedDomain,
|
||||||
|
initialChunk,
|
||||||
|
undefined,
|
||||||
|
targetHost,
|
||||||
|
targetPort
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'terminate':
|
||||||
|
case 'terminate-and-reencrypt':
|
||||||
|
// For TLS termination, use NetworkProxy
|
||||||
|
if (this.networkProxyBridge.getNetworkProxy()) {
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have an initial chunk with TLS data, start processing it
|
||||||
|
if (initialChunk && record.isTLS) {
|
||||||
|
return this.networkProxyBridge.forwardToNetworkProxy(
|
||||||
|
connectionId,
|
||||||
|
socket,
|
||||||
|
record,
|
||||||
|
initialChunk,
|
||||||
|
this.settings.networkProxyPort,
|
||||||
|
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This shouldn't normally happen - we should have TLS data at this point
|
||||||
|
console.log(`[${connectionId}] TLS termination route without TLS data`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'tls_error');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`[${connectionId}] NetworkProxy not available for TLS termination`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'no_network_proxy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No TLS settings - basic forwarding
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for array of hosts
|
||||||
|
const targetHost = Array.isArray(action.target.host)
|
||||||
|
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
|
||||||
|
: action.target.host;
|
||||||
|
|
||||||
|
// Determine target port - either target port or preserve incoming port
|
||||||
|
const targetPort = action.target.preservePort ? record.localPort : action.target.port;
|
||||||
|
|
||||||
|
return this.setupDirectConnection(
|
||||||
|
socket,
|
||||||
|
record,
|
||||||
|
undefined,
|
||||||
|
record.lockedDomain,
|
||||||
|
initialChunk,
|
||||||
|
undefined,
|
||||||
|
targetHost,
|
||||||
|
targetPort
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a redirect action for a route
|
||||||
|
*/
|
||||||
|
private handleRedirectAction(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
route: IRouteConfig
|
||||||
|
): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
const action = route.action;
|
||||||
|
|
||||||
|
// We should have a redirect configuration
|
||||||
|
if (!action.redirect) {
|
||||||
|
console.log(`[${connectionId}] Redirect action missing redirect configuration`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'missing_redirect');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TLS connections, we can't do redirects at the TCP level
|
||||||
|
if (record.isTLS) {
|
||||||
|
console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the first HTTP request to perform the redirect
|
||||||
|
const dataListeners: ((chunk: Buffer) => void)[] = [];
|
||||||
|
|
||||||
|
const httpDataHandler = (chunk: Buffer) => {
|
||||||
|
// Remove all data listeners to avoid duplicated processing
|
||||||
|
for (const listener of dataListeners) {
|
||||||
|
socket.removeListener('data', listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse HTTP request to get path
|
||||||
|
try {
|
||||||
|
const headersEnd = chunk.indexOf('\r\n\r\n');
|
||||||
|
if (headersEnd === -1) {
|
||||||
|
// Not a complete HTTP request, need more data
|
||||||
|
socket.once('data', httpDataHandler);
|
||||||
|
dataListeners.push(httpDataHandler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpHeaders = chunk.slice(0, headersEnd).toString();
|
||||||
|
const requestLine = httpHeaders.split('\r\n')[0];
|
||||||
|
const [method, path] = requestLine.split(' ');
|
||||||
|
|
||||||
|
// Extract Host header
|
||||||
|
const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
|
||||||
|
const host = hostMatch ? hostMatch[1].trim() : record.lockedDomain || '';
|
||||||
|
|
||||||
|
// Process the redirect URL with template variables
|
||||||
|
let redirectUrl = action.redirect.to;
|
||||||
|
redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
|
||||||
|
redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
|
||||||
|
redirectUrl = redirectUrl.replace(/\{port\}/g, record.localPort.toString());
|
||||||
|
|
||||||
|
// Prepare the HTTP redirect response
|
||||||
|
const redirectResponse = [
|
||||||
|
`HTTP/1.1 ${action.redirect.status} Moved`,
|
||||||
|
`Location: ${redirectUrl}`,
|
||||||
|
'Connection: close',
|
||||||
|
'Content-Length: 0',
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
].join('\r\n');
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the redirect response
|
||||||
|
socket.end(redirectResponse);
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'redirect_complete');
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`[${connectionId}] Error processing HTTP redirect: ${err}`);
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'redirect_error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Setup the HTTP data handler
|
||||||
|
socket.once('data', httpDataHandler);
|
||||||
|
dataListeners.push(httpDataHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a block action for a route
|
||||||
|
*/
|
||||||
|
private handleBlockAction(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
route: IRouteConfig
|
||||||
|
): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply close the connection
|
||||||
|
socket.end();
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy connection handling has been removed in favor of pure route-based approach
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a direct connection to the target
|
||||||
|
*/
|
||||||
|
private setupDirectConnection(
|
||||||
|
socket: plugins.net.Socket,
|
||||||
|
record: IConnectionRecord,
|
||||||
|
_unused?: any, // kept for backward compatibility
|
||||||
|
serverName?: string,
|
||||||
|
initialChunk?: Buffer,
|
||||||
|
overridePort?: number,
|
||||||
|
targetHost?: string,
|
||||||
|
targetPort?: number
|
||||||
|
): void {
|
||||||
|
const connectionId = record.id;
|
||||||
|
|
||||||
|
// Determine target host and port if not provided
|
||||||
|
const finalTargetHost = targetHost ||
|
||||||
|
(this.settings.defaults?.target?.host || 'localhost');
|
||||||
|
|
||||||
|
// Determine target port
|
||||||
|
const finalTargetPort = targetPort ||
|
||||||
|
(overridePort !== undefined ? overridePort :
|
||||||
|
(this.settings.defaults?.target?.port || 443));
|
||||||
|
|
||||||
|
// Setup connection options
|
||||||
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||||
|
host: finalTargetHost,
|
||||||
|
port: finalTargetPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Preserve source IP if configured
|
||||||
|
if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
|
||||||
|
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a safe queue for incoming data
|
||||||
|
const dataQueue: Buffer[] = [];
|
||||||
|
let queueSize = 0;
|
||||||
|
let processingQueue = false;
|
||||||
|
let drainPending = false;
|
||||||
|
let pipingEstablished = false;
|
||||||
|
|
||||||
|
// Pause the incoming socket to prevent buffer overflows
|
||||||
|
socket.pause();
|
||||||
|
|
||||||
|
// Function to safely process the data queue without losing events
|
||||||
|
const processDataQueue = () => {
|
||||||
|
if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
|
||||||
|
|
||||||
|
processingQueue = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process all queued chunks with the current active handler
|
||||||
|
while (dataQueue.length > 0) {
|
||||||
|
const chunk = dataQueue.shift()!;
|
||||||
|
queueSize -= chunk.length;
|
||||||
|
|
||||||
|
// Once piping is established, we shouldn't get here,
|
||||||
|
// but just in case, pass to the outgoing socket directly
|
||||||
|
if (pipingEstablished && record.outgoing) {
|
||||||
|
record.outgoing.write(chunk);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track bytes received
|
||||||
|
record.bytesReceived += chunk.length;
|
||||||
|
|
||||||
|
// Check for TLS handshake
|
||||||
|
if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
|
||||||
|
record.isTLS = true;
|
||||||
|
|
||||||
|
if (this.settings.enableTlsDebugLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if adding this chunk would exceed the buffer limit
|
||||||
|
const newSize = record.pendingDataSize + chunk.length;
|
||||||
|
|
||||||
|
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
||||||
|
);
|
||||||
|
socket.end(); // Gracefully close the socket
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer the chunk and update the size counter
|
||||||
|
record.pendingData.push(Buffer.from(chunk));
|
||||||
|
record.pendingDataSize = newSize;
|
||||||
|
this.timeoutManager.updateActivity(record);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processingQueue = false;
|
||||||
|
|
||||||
|
// If there's a pending drain and we've processed everything,
|
||||||
|
// signal we're ready for more data if we haven't established piping yet
|
||||||
|
if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
|
||||||
|
drainPending = false;
|
||||||
|
socket.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unified data handler that safely queues incoming data
|
||||||
|
const safeDataHandler = (chunk: Buffer) => {
|
||||||
|
// If piping is already established, just let the pipe handle it
|
||||||
|
if (pipingEstablished) return;
|
||||||
|
|
||||||
|
// Add to our queue for orderly processing
|
||||||
|
dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
|
||||||
|
queueSize += chunk.length;
|
||||||
|
|
||||||
|
// If queue is getting large, pause socket until we catch up
|
||||||
|
if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
|
||||||
|
socket.pause();
|
||||||
|
drainPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the queue
|
||||||
|
processDataQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add our safe data handler
|
||||||
|
socket.on('data', safeDataHandler);
|
||||||
|
|
||||||
|
// Add initial chunk to pending data if present
|
||||||
|
if (initialChunk) {
|
||||||
|
record.bytesReceived += initialChunk.length;
|
||||||
|
record.pendingData.push(Buffer.from(initialChunk));
|
||||||
|
record.pendingDataSize = initialChunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the target socket but don't set up piping immediately
|
||||||
|
const targetSocket = plugins.net.connect(connectionOptions);
|
||||||
|
record.outgoing = targetSocket;
|
||||||
|
record.outgoingStartTime = Date.now();
|
||||||
|
|
||||||
|
// Apply socket optimizations
|
||||||
|
targetSocket.setNoDelay(this.settings.noDelay);
|
||||||
|
|
||||||
|
// Apply keep-alive settings to the outgoing connection as well
|
||||||
|
if (this.settings.keepAlive) {
|
||||||
|
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
||||||
|
|
||||||
|
// Apply enhanced TCP keep-alive options if enabled
|
||||||
|
if (this.settings.enableKeepAliveProbes) {
|
||||||
|
try {
|
||||||
|
if ('setKeepAliveProbes' in targetSocket) {
|
||||||
|
(targetSocket as any).setKeepAliveProbes(10);
|
||||||
|
}
|
||||||
|
if ('setKeepAliveInterval' in targetSocket) {
|
||||||
|
(targetSocket as any).setKeepAliveInterval(1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Ignore errors - these are optional enhancements
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup specific error handler for connection phase
|
||||||
|
targetSocket.once('error', (err) => {
|
||||||
|
// This handler runs only once during the initial connection phase
|
||||||
|
const code = (err as any).code;
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resume the incoming socket to prevent it from hanging
|
||||||
|
socket.resume();
|
||||||
|
|
||||||
|
if (code === 'ECONNREFUSED') {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
|
||||||
|
);
|
||||||
|
} else if (code === 'ETIMEDOUT') {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} timed out`
|
||||||
|
);
|
||||||
|
} else if (code === 'ECONNRESET') {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} was reset`
|
||||||
|
);
|
||||||
|
} else if (code === 'EHOSTUNREACH') {
|
||||||
|
console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing error handler after connection phase
|
||||||
|
targetSocket.removeAllListeners('error');
|
||||||
|
|
||||||
|
// Re-add the normal error handler for established connections
|
||||||
|
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
|
||||||
|
|
||||||
|
if (record.outgoingTerminationReason === null) {
|
||||||
|
record.outgoingTerminationReason = 'connection_failed';
|
||||||
|
this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route-based configuration doesn't use domain handlers
|
||||||
|
|
||||||
|
// Clean up the connection
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup close handler
|
||||||
|
targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
|
||||||
|
socket.on('close', this.connectionManager.handleClose('incoming', record));
|
||||||
|
|
||||||
|
// Handle timeouts with keep-alive awareness
|
||||||
|
socket.on('timeout', () => {
|
||||||
|
// For keep-alive connections, just log a warning instead of closing
|
||||||
|
if (record.hasKeepAlive) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
|
||||||
|
record.remoteIP
|
||||||
|
} after ${plugins.prettyMs(
|
||||||
|
this.settings.socketTimeout || 3600000
|
||||||
|
)}. Connection preserved.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-keep-alive connections, proceed with normal cleanup
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Timeout on incoming side from ${
|
||||||
|
record.remoteIP
|
||||||
|
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
||||||
|
);
|
||||||
|
if (record.incomingTerminationReason === null) {
|
||||||
|
record.incomingTerminationReason = 'timeout';
|
||||||
|
this.connectionManager.incrementTerminationStat('incoming', 'timeout');
|
||||||
|
}
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
|
||||||
|
});
|
||||||
|
|
||||||
|
targetSocket.on('timeout', () => {
|
||||||
|
// For keep-alive connections, just log a warning instead of closing
|
||||||
|
if (record.hasKeepAlive) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
|
||||||
|
record.remoteIP
|
||||||
|
} after ${plugins.prettyMs(
|
||||||
|
this.settings.socketTimeout || 3600000
|
||||||
|
)}. Connection preserved.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-keep-alive connections, proceed with normal cleanup
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Timeout on outgoing side from ${
|
||||||
|
record.remoteIP
|
||||||
|
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
|
||||||
|
);
|
||||||
|
if (record.outgoingTerminationReason === null) {
|
||||||
|
record.outgoingTerminationReason = 'timeout';
|
||||||
|
this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
|
||||||
|
}
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply socket timeouts
|
||||||
|
this.timeoutManager.applySocketTimeouts(record);
|
||||||
|
|
||||||
|
// Track outgoing data for bytes counting
|
||||||
|
targetSocket.on('data', (chunk: Buffer) => {
|
||||||
|
record.bytesSent += chunk.length;
|
||||||
|
this.timeoutManager.updateActivity(record);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the outgoing connection to be ready before setting up piping
|
||||||
|
targetSocket.once('connect', () => {
|
||||||
|
// Clear the initial connection error handler
|
||||||
|
targetSocket.removeAllListeners('error');
|
||||||
|
|
||||||
|
// Add the normal error handler for established connections
|
||||||
|
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
|
||||||
|
|
||||||
|
// Process any remaining data in the queue before switching to piping
|
||||||
|
processDataQueue();
|
||||||
|
|
||||||
|
// Set up piping immediately
|
||||||
|
pipingEstablished = true;
|
||||||
|
|
||||||
|
// Flush all pending data to target
|
||||||
|
if (record.pendingData.length > 0) {
|
||||||
|
const combinedData = Buffer.concat(record.pendingData);
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write pending data immediately
|
||||||
|
targetSocket.write(combinedData, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
|
||||||
|
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the buffer now that we've processed it
|
||||||
|
record.pendingData = [];
|
||||||
|
record.pendingDataSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup piping in both directions without any delays
|
||||||
|
socket.pipe(targetSocket);
|
||||||
|
targetSocket.pipe(socket);
|
||||||
|
|
||||||
|
// Resume the socket to ensure data flows
|
||||||
|
socket.resume();
|
||||||
|
|
||||||
|
// Process any data that might be queued in the interim
|
||||||
|
if (dataQueue.length > 0) {
|
||||||
|
// Write any remaining queued data directly to the target socket
|
||||||
|
for (const chunk of dataQueue) {
|
||||||
|
targetSocket.write(chunk);
|
||||||
|
}
|
||||||
|
// Clear the queue
|
||||||
|
dataQueue.length = 0;
|
||||||
|
queueSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
|
||||||
|
`${
|
||||||
|
serverName
|
||||||
|
? ` (SNI: ${serverName})`
|
||||||
|
: record.lockedDomain
|
||||||
|
? ` (Domain: ${record.lockedDomain})`
|
||||||
|
: ''
|
||||||
|
}` +
|
||||||
|
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
||||||
|
record.hasKeepAlive ? 'Yes' : 'No'
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
|
||||||
|
`${
|
||||||
|
serverName
|
||||||
|
? ` (SNI: ${serverName})`
|
||||||
|
: record.lockedDomain
|
||||||
|
? ` (Domain: ${record.lockedDomain})`
|
||||||
|
: ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the renegotiation handler for SNI validation
|
||||||
|
if (serverName) {
|
||||||
|
// Create connection info object for the existing connection
|
||||||
|
const connInfo = {
|
||||||
|
sourceIp: record.remoteIP,
|
||||||
|
sourcePort: record.incoming.remotePort || 0,
|
||||||
|
destIp: record.incoming.localAddress || '',
|
||||||
|
destPort: record.incoming.localPort || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a renegotiation handler function
|
||||||
|
const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
|
||||||
|
connectionId,
|
||||||
|
serverName,
|
||||||
|
connInfo,
|
||||||
|
(connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the handler in the connection record so we can remove it during cleanup
|
||||||
|
record.renegotiationHandler = renegotiationHandler;
|
||||||
|
|
||||||
|
// Add the handler to the socket
|
||||||
|
socket.on('data', renegotiationHandler);
|
||||||
|
|
||||||
|
if (this.settings.enableDetailedLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
|
||||||
|
);
|
||||||
|
if (this.settings.allowSessionTicket === false) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set connection timeout
|
||||||
|
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
|
||||||
|
);
|
||||||
|
this.connectionManager.initiateCleanupOnce(record, reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark TLS handshake as complete for TLS connections
|
||||||
|
if (record.isTLS) {
|
||||||
|
record.tlsHandshakeComplete = true;
|
||||||
|
|
||||||
|
if (this.settings.enableTlsDebugLogging) {
|
||||||
|
console.log(
|
||||||
|
`[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
497
ts/proxies/smart-proxy/route-helpers.ts
Normal file
497
ts/proxies/smart-proxy/route-helpers.ts
Normal file
@ -0,0 +1,497 @@
|
|||||||
|
import type {
|
||||||
|
IRouteConfig,
|
||||||
|
IRouteMatch,
|
||||||
|
IRouteAction,
|
||||||
|
IRouteTarget,
|
||||||
|
IRouteTls,
|
||||||
|
IRouteRedirect,
|
||||||
|
IRouteSecurity,
|
||||||
|
IRouteAdvanced,
|
||||||
|
TPortRange
|
||||||
|
} from './models/route-types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic helper function to create a route configuration
|
||||||
|
*/
|
||||||
|
export function createRoute(
|
||||||
|
match: IRouteMatch,
|
||||||
|
action: IRouteAction,
|
||||||
|
metadata?: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return {
|
||||||
|
match,
|
||||||
|
action,
|
||||||
|
...metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a basic HTTP route configuration
|
||||||
|
*/
|
||||||
|
export function createHttpRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 80
|
||||||
|
domains?: string | string[];
|
||||||
|
path?: string;
|
||||||
|
target: IRouteTarget;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || 80,
|
||||||
|
...(options.domains ? { domains: options.domains } : {}),
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: options.target,
|
||||||
|
...(options.headers || options.security ? {
|
||||||
|
advanced: {
|
||||||
|
...(options.headers ? { headers: options.headers } : {})
|
||||||
|
},
|
||||||
|
...(options.security ? { security: options.security } : {})
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'HTTP Route',
|
||||||
|
description: options.description,
|
||||||
|
priority: options.priority,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTPS route configuration with TLS termination
|
||||||
|
*/
|
||||||
|
export function createHttpsRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 443
|
||||||
|
domains: string | string[];
|
||||||
|
path?: string;
|
||||||
|
target: IRouteTarget;
|
||||||
|
tlsMode?: 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || 443,
|
||||||
|
domains: options.domains,
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: options.target,
|
||||||
|
tls: {
|
||||||
|
mode: options.tlsMode || 'terminate',
|
||||||
|
certificate: options.certificate || 'auto'
|
||||||
|
},
|
||||||
|
...(options.headers || options.security ? {
|
||||||
|
advanced: {
|
||||||
|
...(options.headers ? { headers: options.headers } : {})
|
||||||
|
},
|
||||||
|
...(options.security ? { security: options.security } : {})
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'HTTPS Route',
|
||||||
|
description: options.description,
|
||||||
|
priority: options.priority,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTPS passthrough route configuration
|
||||||
|
*/
|
||||||
|
export function createPassthroughRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 443
|
||||||
|
domains?: string | string[];
|
||||||
|
target: IRouteTarget;
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || 443,
|
||||||
|
...(options.domains ? { domains: options.domains } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: options.target,
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough'
|
||||||
|
},
|
||||||
|
...(options.security ? { security: options.security } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'HTTPS Passthrough Route',
|
||||||
|
description: options.description,
|
||||||
|
priority: options.priority,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a redirect route configuration
|
||||||
|
*/
|
||||||
|
export function createRedirectRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 80
|
||||||
|
domains?: string | string[];
|
||||||
|
path?: string;
|
||||||
|
redirectTo: string;
|
||||||
|
statusCode?: 301 | 302 | 307 | 308;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || 80,
|
||||||
|
...(options.domains ? { domains: options.domains } : {}),
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'redirect',
|
||||||
|
redirect: {
|
||||||
|
to: options.redirectTo,
|
||||||
|
status: options.statusCode || 301
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'Redirect Route',
|
||||||
|
description: options.description,
|
||||||
|
priority: options.priority,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an HTTP to HTTPS redirect route configuration
|
||||||
|
*/
|
||||||
|
export function createHttpToHttpsRedirect(
|
||||||
|
options: {
|
||||||
|
domains: string | string[];
|
||||||
|
statusCode?: 301 | 302 | 307 | 308;
|
||||||
|
name?: string;
|
||||||
|
priority?: number;
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
|
||||||
|
|
||||||
|
return createRedirectRoute({
|
||||||
|
ports: 80,
|
||||||
|
domains: options.domains,
|
||||||
|
redirectTo: 'https://{domain}{path}',
|
||||||
|
statusCode: options.statusCode || 301,
|
||||||
|
name: options.name || `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
|
||||||
|
priority: options.priority || 100 // High priority for redirects
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a block route configuration
|
||||||
|
*/
|
||||||
|
export function createBlockRoute(
|
||||||
|
options: {
|
||||||
|
ports: number | number[];
|
||||||
|
domains?: string | string[];
|
||||||
|
clientIp?: string[];
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports,
|
||||||
|
...(options.domains ? { domains: options.domains } : {}),
|
||||||
|
...(options.clientIp ? { clientIp: options.clientIp } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'block'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'Block Route',
|
||||||
|
description: options.description,
|
||||||
|
priority: options.priority || 1000, // Very high priority for blocks
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a load balancer route configuration
|
||||||
|
*/
|
||||||
|
export function createLoadBalancerRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 443
|
||||||
|
domains: string | string[];
|
||||||
|
path?: string;
|
||||||
|
targets: string[]; // Array of host names/IPs for load balancing
|
||||||
|
targetPort: number;
|
||||||
|
tlsMode?: 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
const useTls = options.tlsMode !== undefined;
|
||||||
|
const defaultPort = useTls ? 443 : 80;
|
||||||
|
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || defaultPort,
|
||||||
|
domains: options.domains,
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: options.targets,
|
||||||
|
port: options.targetPort
|
||||||
|
},
|
||||||
|
...(useTls ? {
|
||||||
|
tls: {
|
||||||
|
mode: options.tlsMode!,
|
||||||
|
...(options.tlsMode !== 'passthrough' && options.certificate ? {
|
||||||
|
certificate: options.certificate
|
||||||
|
} : {})
|
||||||
|
}
|
||||||
|
} : {}),
|
||||||
|
...(options.headers || options.security ? {
|
||||||
|
advanced: {
|
||||||
|
...(options.headers ? { headers: options.headers } : {})
|
||||||
|
},
|
||||||
|
...(options.security ? { security: options.security } : {})
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'Load Balanced Route',
|
||||||
|
description: options.description || `Load balancing across ${options.targets.length} backends`,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a complete HTTPS server configuration with HTTP redirect
|
||||||
|
*/
|
||||||
|
export function createHttpsServer(
|
||||||
|
options: {
|
||||||
|
domains: string | string[];
|
||||||
|
target: IRouteTarget;
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
addHttpRedirect?: boolean;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
): IRouteConfig[] {
|
||||||
|
const routes: IRouteConfig[] = [];
|
||||||
|
const domainArray = Array.isArray(options.domains) ? options.domains : [options.domains];
|
||||||
|
|
||||||
|
// Add HTTPS route
|
||||||
|
routes.push(createHttpsRoute({
|
||||||
|
domains: options.domains,
|
||||||
|
target: options.target,
|
||||||
|
certificate: options.certificate || 'auto',
|
||||||
|
security: options.security,
|
||||||
|
name: options.name || `HTTPS Server for ${domainArray.join(', ')}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add HTTP to HTTPS redirect if requested
|
||||||
|
if (options.addHttpRedirect !== false) {
|
||||||
|
routes.push(createHttpToHttpsRedirect({
|
||||||
|
domains: options.domains,
|
||||||
|
name: `HTTP to HTTPS Redirect for ${domainArray.join(', ')}`,
|
||||||
|
priority: 100
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a port range configuration from various input formats
|
||||||
|
*/
|
||||||
|
export function createPortRange(
|
||||||
|
ports: number | number[] | string | Array<{ from: number; to: number }>
|
||||||
|
): TPortRange {
|
||||||
|
// If it's a string like "80,443" or "8000-9000", parse it
|
||||||
|
if (typeof ports === 'string') {
|
||||||
|
if (ports.includes('-')) {
|
||||||
|
// Handle range like "8000-9000"
|
||||||
|
const [start, end] = ports.split('-').map(p => parseInt(p.trim(), 10));
|
||||||
|
return [{ from: start, to: end }];
|
||||||
|
} else if (ports.includes(',')) {
|
||||||
|
// Handle comma-separated list like "80,443,8080"
|
||||||
|
return ports.split(',').map(p => parseInt(p.trim(), 10));
|
||||||
|
} else {
|
||||||
|
// Handle single port as string
|
||||||
|
return parseInt(ports.trim(), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return as is
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a security configuration object
|
||||||
|
*/
|
||||||
|
export function createSecurityConfig(
|
||||||
|
options: {
|
||||||
|
allowedIps?: string[];
|
||||||
|
blockedIps?: string[];
|
||||||
|
maxConnections?: number;
|
||||||
|
authentication?: {
|
||||||
|
type: 'basic' | 'digest' | 'oauth';
|
||||||
|
// Auth-specific options
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
): IRouteSecurity {
|
||||||
|
return {
|
||||||
|
...(options.allowedIps ? { allowedIps: options.allowedIps } : {}),
|
||||||
|
...(options.blockedIps ? { blockedIps: options.blockedIps } : {}),
|
||||||
|
...(options.maxConnections ? { maxConnections: options.maxConnections } : {}),
|
||||||
|
...(options.authentication ? { authentication: options.authentication } : {})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a static file server route
|
||||||
|
*/
|
||||||
|
export function createStaticFileRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 80
|
||||||
|
domains: string | string[];
|
||||||
|
path?: string;
|
||||||
|
targetDirectory: string;
|
||||||
|
tlsMode?: 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
certificate?: 'auto' | { key: string; cert: string };
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
security?: IRouteSecurity;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
priority?: number;
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
const useTls = options.tlsMode !== undefined;
|
||||||
|
const defaultPort = useTls ? 443 : 80;
|
||||||
|
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || defaultPort,
|
||||||
|
domains: options.domains,
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'localhost', // Static file serving is typically handled locally
|
||||||
|
port: 0, // Special value indicating a static file server
|
||||||
|
preservePort: false
|
||||||
|
},
|
||||||
|
...(useTls ? {
|
||||||
|
tls: {
|
||||||
|
mode: options.tlsMode!,
|
||||||
|
certificate: options.certificate || 'auto'
|
||||||
|
}
|
||||||
|
} : {}),
|
||||||
|
advanced: {
|
||||||
|
...(options.headers ? { headers: options.headers } : {}),
|
||||||
|
staticFiles: {
|
||||||
|
directory: options.targetDirectory,
|
||||||
|
indexFiles: ['index.html', 'index.htm']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...(options.security ? { security: options.security } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'Static File Server',
|
||||||
|
description: options.description || `Serving static files from ${options.targetDirectory}`,
|
||||||
|
priority: options.priority,
|
||||||
|
tags: options.tags
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a test route for debugging purposes
|
||||||
|
*/
|
||||||
|
export function createTestRoute(
|
||||||
|
options: {
|
||||||
|
ports?: number | number[]; // Default: 8000
|
||||||
|
domains?: string | string[];
|
||||||
|
path?: string;
|
||||||
|
response?: {
|
||||||
|
status?: number;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
body?: string;
|
||||||
|
};
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
): IRouteConfig {
|
||||||
|
return createRoute(
|
||||||
|
{
|
||||||
|
ports: options.ports || 8000,
|
||||||
|
...(options.domains ? { domains: options.domains } : {}),
|
||||||
|
...(options.path ? { path: options.path } : {})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'forward',
|
||||||
|
target: {
|
||||||
|
host: 'test', // Special value indicating a test route
|
||||||
|
port: 0
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
testResponse: {
|
||||||
|
status: options.response?.status || 200,
|
||||||
|
headers: options.response?.headers || { 'Content-Type': 'text/plain' },
|
||||||
|
body: options.response?.body || 'Test route is working!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: options.name || 'Test Route',
|
||||||
|
description: 'Route for testing and debugging',
|
||||||
|
priority: 500,
|
||||||
|
tags: ['test', 'debug']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
9
ts/proxies/smart-proxy/route-helpers/index.ts
Normal file
9
ts/proxies/smart-proxy/route-helpers/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Route helpers for SmartProxy
|
||||||
|
*
|
||||||
|
* This module provides helper functions for creating various types of route configurations
|
||||||
|
* to be used with the SmartProxy system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-export all functions from the route-helpers.ts file
|
||||||
|
export * from '../route-helpers.js';
|
586
ts/proxies/smart-proxy/route-manager.ts
Normal file
586
ts/proxies/smart-proxy/route-manager.ts
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type {
|
||||||
|
IRouteConfig,
|
||||||
|
IRouteMatch,
|
||||||
|
IRouteAction,
|
||||||
|
TPortRange
|
||||||
|
} from './models/route-types.js';
|
||||||
|
import type {
|
||||||
|
ISmartProxyOptions,
|
||||||
|
IRoutedSmartProxyOptions
|
||||||
|
} 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,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { SmartProxyOptions } from './models/interfaces.js';
|
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||||
@ -8,7 +8,7 @@ export class SecurityManager {
|
|||||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||||
|
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
constructor(private settings: ISmartProxyOptions) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get connections count by IP
|
* Get connections count by IP
|
||||||
|
@ -1,30 +1,42 @@
|
|||||||
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 { TlsManager } from './tls-manager.js';
|
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 { CertificateData } 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 { ForwardingType } 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 { SmartProxyOptions, DomainConfig } from './models/interfaces.js';
|
import type {
|
||||||
// Provide backward compatibility types
|
ISmartProxyOptions,
|
||||||
export type { SmartProxyOptions as IPortProxySettings, DomainConfig as IDomainConfig };
|
IRoutedSmartProxyOptions
|
||||||
|
} 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 - Pure route-based API
|
||||||
|
*
|
||||||
|
* SmartProxy is a unified proxy system that works with routes to define connection handling behavior.
|
||||||
|
* Each route contains matching criteria (ports, domains, etc.) and an action to take (forward, redirect, block).
|
||||||
|
*
|
||||||
|
* Configuration is provided through a set of routes, with each route defining:
|
||||||
|
* - What to match (ports, domains, paths, client IPs)
|
||||||
|
* - What to do with matching traffic (forward, redirect, block)
|
||||||
|
* - How to handle TLS (passthrough, terminate, terminate-and-reencrypt)
|
||||||
|
* - Security settings (IP restrictions, connection limits)
|
||||||
|
* - Advanced options (timeout, headers, etc.)
|
||||||
*/
|
*/
|
||||||
export class SmartProxy extends plugins.EventEmitter {
|
export class SmartProxy extends plugins.EventEmitter {
|
||||||
private netServers: plugins.net.Server[] = [];
|
private netServers: plugins.net.Server[] = [];
|
||||||
@ -34,24 +46,55 @@ 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 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(settingsArg: SmartProxyOptions) {
|
/**
|
||||||
|
* Constructor for SmartProxy
|
||||||
|
*
|
||||||
|
* @param settingsArg Configuration options containing routes and other settings
|
||||||
|
* Routes define how traffic is matched and handled, with each route having:
|
||||||
|
* - match: criteria for matching traffic (ports, domains, paths, IPs)
|
||||||
|
* - action: what to do with matched traffic (forward, redirect, block)
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```ts
|
||||||
|
* const proxy = new SmartProxy({
|
||||||
|
* routes: [
|
||||||
|
* {
|
||||||
|
* match: {
|
||||||
|
* ports: 443,
|
||||||
|
* domains: ['example.com', '*.example.com']
|
||||||
|
* },
|
||||||
|
* action: {
|
||||||
|
* type: 'forward',
|
||||||
|
* target: { host: '10.0.0.1', port: 8443 },
|
||||||
|
* tls: { mode: 'passthrough' }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ],
|
||||||
|
* defaults: {
|
||||||
|
* target: { host: 'localhost', port: 8080 },
|
||||||
|
* security: { allowedIps: ['*'] }
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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,
|
||||||
@ -63,12 +106,12 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
|
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000,
|
||||||
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
|
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024,
|
||||||
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
||||||
enableKeepAliveProbes:
|
enableKeepAliveProbes:
|
||||||
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
|
||||||
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
||||||
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
||||||
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
||||||
allowSessionTicket:
|
allowSessionTicket:
|
||||||
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
|
settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
|
||||||
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
|
||||||
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
|
||||||
@ -76,12 +119,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 +133,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
autoRenew: true,
|
autoRenew: true,
|
||||||
certificateStore: './certs',
|
certificateStore: './certs',
|
||||||
skipConfiguredCerts: false,
|
skipConfiguredCerts: false,
|
||||||
httpsRedirectPort: this.settings.fromPort,
|
httpsRedirectPort: 443,
|
||||||
renewCheckIntervalHours: 24,
|
renewCheckIntervalHours: 24,
|
||||||
domainForwards: []
|
domainForwards: []
|
||||||
};
|
};
|
||||||
@ -105,28 +147,33 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
this.securityManager,
|
this.securityManager,
|
||||||
this.timeoutManager
|
this.timeoutManager
|
||||||
);
|
);
|
||||||
this.domainConfigManager = new DomainConfigManager(this.settings);
|
|
||||||
|
// Create the route manager
|
||||||
|
this.routeManager = new RouteManager(this.settings);
|
||||||
|
|
||||||
|
// Create port range manager
|
||||||
|
// this.portRangeManager = new PortRangeManager(this.settings);
|
||||||
|
|
||||||
|
// Create other required components
|
||||||
this.tlsManager = new TlsManager(this.settings);
|
this.tlsManager = new TlsManager(this.settings);
|
||||||
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
|
this.networkProxyBridge = new NetworkProxyBridge(this.settings);
|
||||||
this.portRangeManager = new PortRangeManager(this.settings);
|
|
||||||
|
|
||||||
// Initialize connection handler
|
// Initialize connection handler with route support
|
||||||
this.connectionHandler = new ConnectionHandler(
|
this.routeConnectionHandler = new RouteConnectionHandler(
|
||||||
this.settings,
|
this.settings,
|
||||||
this.connectionManager,
|
this.connectionManager,
|
||||||
this.securityManager,
|
this.securityManager,
|
||||||
this.domainConfigManager,
|
|
||||||
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: SmartProxyOptions;
|
public settings: ISmartProxyOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the Port80Handler for ACME certificate management
|
* Initialize the Port80Handler for ACME certificate management
|
||||||
@ -142,8 +189,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 || 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 +202,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 +211,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process domain configs
|
// Pure route-based configuration - no domain configs needed
|
||||||
// Note: ensureForwardingConfig is no longer needed since forwarding is now required
|
|
||||||
|
|
||||||
// Initialize domain config manager with the processed configs
|
|
||||||
this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs);
|
|
||||||
|
|
||||||
// Initialize Port80Handler if enabled
|
// Initialize Port80Handler if enabled
|
||||||
await this.initializePort80Handler();
|
await this.initializePort80Handler();
|
||||||
@ -176,20 +220,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
|
||||||
const domainForwards = acme.domainForwards?.map(f => {
|
const domainForwards = acme.domainForwards?.map(f => {
|
||||||
// If the domain has a forwarding config in domainConfigs, use that
|
// Check if a matching route exists
|
||||||
const domainConfig = this.settings.domainConfigs.find(
|
const matchingRoute = this.settings.routes.find(
|
||||||
dc => dc.domains.some(d => d === f.domain)
|
route => Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains.some(d => d === f.domain)
|
||||||
|
: route.match.domains === f.domain
|
||||||
);
|
);
|
||||||
|
|
||||||
if (domainConfig?.forwarding) {
|
if (matchingRoute) {
|
||||||
return {
|
return {
|
||||||
domain: f.domain,
|
domain: f.domain,
|
||||||
forwardConfig: f.forwardConfig,
|
forwardConfig: f.forwardConfig,
|
||||||
acmeForwardConfig: f.acmeForwardConfig,
|
acmeForwardConfig: f.acmeForwardConfig,
|
||||||
sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false
|
sslRedirect: f.sslRedirect || false
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
// In route mode, look for matching route
|
||||||
|
const route = this.routeManager.findMatchingRoute({
|
||||||
|
port: 443,
|
||||||
|
domain: f.domain,
|
||||||
|
clientIp: '127.0.0.1' // Dummy IP for finding routes
|
||||||
|
})?.route;
|
||||||
|
|
||||||
|
if (route && route.action.type === 'forward' && route.action.tls) {
|
||||||
|
// If we found a matching route with TLS settings
|
||||||
|
return {
|
||||||
|
domain: f.domain,
|
||||||
|
forwardConfig: f.forwardConfig,
|
||||||
|
acmeForwardConfig: f.acmeForwardConfig,
|
||||||
|
sslRedirect: f.sslRedirect || false
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise use the existing configuration
|
// Otherwise use the existing configuration
|
||||||
@ -201,8 +264,11 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
// Create CertProvisioner with appropriate parameters
|
||||||
|
// No longer need to support multiple configuration types
|
||||||
|
// Just pass the routes directly
|
||||||
this.certProvisioner = new CertProvisioner(
|
this.certProvisioner = new CertProvisioner(
|
||||||
this.settings.domainConfigs,
|
this.settings.routes,
|
||||||
this.port80Handler,
|
this.port80Handler,
|
||||||
this.networkProxyBridge,
|
this.networkProxyBridge,
|
||||||
this.settings.certProvisionFunction,
|
this.settings.certProvisionFunction,
|
||||||
@ -212,6 +278,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
domainForwards
|
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,8 +332,8 @@ 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)' : ''
|
isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''
|
||||||
}${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
|
}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -348,12 +412,19 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract domain configurations from routes for certificate provisioning
|
||||||
|
*
|
||||||
|
* Note: This method has been removed as we now work directly with routes
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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();
|
||||||
@ -412,44 +483,73 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the domain configurations for the proxy
|
* Updates the domain configurations for the proxy
|
||||||
|
*
|
||||||
|
* Note: This legacy method has been removed. Use updateRoutes instead.
|
||||||
*/
|
*/
|
||||||
public async updateDomainConfigs(newDomainConfigs: DomainConfig[]): Promise<void> {
|
public async updateDomainConfigs(): Promise<void> {
|
||||||
console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
|
console.warn('Method updateDomainConfigs() is deprecated. Use updateRoutes() instead.');
|
||||||
|
throw new Error('updateDomainConfigs() is deprecated - use updateRoutes() instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update routes with new configuration
|
||||||
|
*
|
||||||
|
* This method replaces the current route configuration with the provided routes.
|
||||||
|
* It also provisions certificates for routes that require TLS termination and have
|
||||||
|
* `certificate: 'auto'` set in their TLS configuration.
|
||||||
|
*
|
||||||
|
* @param newRoutes Array of route configurations to use
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```ts
|
||||||
|
* proxy.updateRoutes([
|
||||||
|
* {
|
||||||
|
* match: { ports: 443, domains: 'secure.example.com' },
|
||||||
|
* action: {
|
||||||
|
* type: 'forward',
|
||||||
|
* target: { host: '10.0.0.1', port: 8443 },
|
||||||
|
* tls: { mode: 'terminate', certificate: 'auto' }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ]);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
|
||||||
|
console.log(`Updating routes (${newRoutes.length} routes)`);
|
||||||
|
|
||||||
// Update domain configs in DomainConfigManager
|
// Update routes in RouteManager
|
||||||
this.domainConfigManager.updateDomainConfigs(newDomainConfigs);
|
this.routeManager.updateRoutes(newRoutes);
|
||||||
|
|
||||||
// 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.syncRoutesToNetworkProxy(newRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Port80Handler is running, provision certificates based on forwarding type
|
// If Port80Handler is running, provision certificates based on routes
|
||||||
if (this.port80Handler && this.settings.acme?.enabled) {
|
if (this.port80Handler && this.settings.acme?.enabled) {
|
||||||
for (const domainConfig of newDomainConfigs) {
|
for (const route of newRoutes) {
|
||||||
// Skip certificate provisioning for http-only or passthrough configs that don't need certs
|
// Skip routes without domains
|
||||||
const forwardingType = domainConfig.forwarding.type;
|
if (!route.match.domains) continue;
|
||||||
const needsCertificate =
|
|
||||||
forwardingType === 'https-terminate-to-http' ||
|
|
||||||
forwardingType === 'https-terminate-to-https';
|
|
||||||
|
|
||||||
// Skip certificate provisioning if ACME is explicitly disabled for this domain
|
// Skip non-forward routes
|
||||||
const acmeDisabled = domainConfig.forwarding.acme?.enabled === false;
|
if (route.action.type !== 'forward') continue;
|
||||||
|
|
||||||
if (!needsCertificate || acmeDisabled) {
|
// Skip routes without TLS termination
|
||||||
if (this.settings.enableDetailedLogging) {
|
if (!route.action.tls ||
|
||||||
console.log(`Skipping certificate provisioning for ${domainConfig.domains.join(', ')} (${forwardingType})`);
|
route.action.tls.mode === 'passthrough' ||
|
||||||
}
|
!route.action.target) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const domain of domainConfig.domains) {
|
// 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('*');
|
const isWildcard = domain.includes('*');
|
||||||
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
let provision: string | plugins.tsclass.network.ICert = 'http01';
|
||||||
|
|
||||||
// Check for ACME forwarding configuration in the domain
|
|
||||||
const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges;
|
|
||||||
|
|
||||||
if (this.settings.certProvisionFunction) {
|
if (this.settings.certProvisionFunction) {
|
||||||
try {
|
try {
|
||||||
provision = await this.settings.certProvisionFunction(domain);
|
provision = await this.settings.certProvisionFunction(domain);
|
||||||
@ -467,15 +567,18 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Port80Handler options from the forwarding configuration
|
// Register domain with Port80Handler
|
||||||
const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding);
|
this.port80Handler.addDomain({
|
||||||
|
domainName: domain,
|
||||||
|
sslRedirect: true,
|
||||||
|
acmeMaintenance: true
|
||||||
|
});
|
||||||
|
|
||||||
this.port80Handler.addDomain(port80Config);
|
|
||||||
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`);
|
||||||
} else {
|
} else {
|
||||||
// Static certificate (e.g., DNS-01 provisioned) supports wildcards
|
// Handle static certificate (e.g., DNS-01 provisioned)
|
||||||
const certObj = provision as plugins.tsclass.network.ICert;
|
const certObj = provision as plugins.tsclass.network.ICert;
|
||||||
const certData: CertificateData = {
|
const certData: ICertificateData = {
|
||||||
domain: certObj.domainName,
|
domain: certObj.domainName,
|
||||||
certificate: certObj.publicKey,
|
certificate: certObj.publicKey,
|
||||||
privateKey: certObj.privateKey,
|
privateKey: certObj.privateKey,
|
||||||
@ -486,11 +589,11 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Provisioned certificates for new domains');
|
|
||||||
|
console.log('Provisioned certificates for new routes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request a certificate for a specific domain
|
* Request a certificate for a specific domain
|
||||||
*/
|
*/
|
||||||
@ -583,7 +686,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 +695,34 @@ export class SmartProxy extends plugins.EventEmitter {
|
|||||||
* Get a list of eligible domains for ACME certificates
|
* Get a list of eligible domains for ACME certificates
|
||||||
*/
|
*/
|
||||||
public getEligibleDomainsForCertificates(): string[] {
|
public getEligibleDomainsForCertificates(): string[] {
|
||||||
// Collect all non-wildcard domains from domain configs
|
|
||||||
const domains: string[] = [];
|
const domains: string[] = [];
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy mode is no longer supported
|
||||||
|
|
||||||
return domains;
|
return domains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import type { ConnectionRecord, SmartProxyOptions } from './models/interfaces.js';
|
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages timeouts and inactivity tracking for connections
|
* Manages timeouts and inactivity tracking for connections
|
||||||
*/
|
*/
|
||||||
export class TimeoutManager {
|
export class TimeoutManager {
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
constructor(private settings: ISmartProxyOptions) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure timeout values don't exceed Node.js max safe integer
|
* Ensure timeout values don't exceed Node.js max safe integer
|
||||||
@ -28,7 +28,7 @@ export class TimeoutManager {
|
|||||||
/**
|
/**
|
||||||
* Update connection activity timestamp
|
* Update connection activity timestamp
|
||||||
*/
|
*/
|
||||||
public updateActivity(record: ConnectionRecord): void {
|
public updateActivity(record: IConnectionRecord): void {
|
||||||
record.lastActivity = Date.now();
|
record.lastActivity = Date.now();
|
||||||
|
|
||||||
// Clear any inactivity warning
|
// Clear any inactivity warning
|
||||||
@ -36,11 +36,11 @@ export class TimeoutManager {
|
|||||||
record.inactivityWarningIssued = false;
|
record.inactivityWarningIssued = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate effective inactivity timeout based on connection type
|
* Calculate effective inactivity timeout based on connection type
|
||||||
*/
|
*/
|
||||||
public getEffectiveInactivityTimeout(record: ConnectionRecord): number {
|
public getEffectiveInactivityTimeout(record: IConnectionRecord): number {
|
||||||
let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default
|
let effectiveTimeout = this.settings.inactivityTimeout || 14400000; // 4 hours default
|
||||||
|
|
||||||
// For immortal keep-alive connections, use an extremely long timeout
|
// For immortal keep-alive connections, use an extremely long timeout
|
||||||
@ -60,7 +60,7 @@ export class TimeoutManager {
|
|||||||
/**
|
/**
|
||||||
* Calculate effective max lifetime based on connection type
|
* Calculate effective max lifetime based on connection type
|
||||||
*/
|
*/
|
||||||
public getEffectiveMaxLifetime(record: ConnectionRecord): number {
|
public getEffectiveMaxLifetime(record: IConnectionRecord): number {
|
||||||
// Use domain-specific timeout from forwarding.advanced if available
|
// Use domain-specific timeout from forwarding.advanced if available
|
||||||
const baseTimeout = record.domainConfig?.forwarding?.advanced?.timeout ||
|
const baseTimeout = record.domainConfig?.forwarding?.advanced?.timeout ||
|
||||||
this.settings.maxConnectionLifetime ||
|
this.settings.maxConnectionLifetime ||
|
||||||
@ -91,8 +91,8 @@ export class TimeoutManager {
|
|||||||
* @returns The cleanup timer
|
* @returns The cleanup timer
|
||||||
*/
|
*/
|
||||||
public setupConnectionTimeout(
|
public setupConnectionTimeout(
|
||||||
record: ConnectionRecord,
|
record: IConnectionRecord,
|
||||||
onTimeout: (record: ConnectionRecord, reason: string) => void
|
onTimeout: (record: IConnectionRecord, reason: string) => void
|
||||||
): NodeJS.Timeout {
|
): NodeJS.Timeout {
|
||||||
// Clear any existing timer
|
// Clear any existing timer
|
||||||
if (record.cleanupTimer) {
|
if (record.cleanupTimer) {
|
||||||
@ -120,7 +120,7 @@ export class TimeoutManager {
|
|||||||
* Check for inactivity on a connection
|
* Check for inactivity on a connection
|
||||||
* @returns Object with check results
|
* @returns Object with check results
|
||||||
*/
|
*/
|
||||||
public checkInactivity(record: ConnectionRecord): {
|
public checkInactivity(record: IConnectionRecord): {
|
||||||
isInactive: boolean;
|
isInactive: boolean;
|
||||||
shouldWarn: boolean;
|
shouldWarn: boolean;
|
||||||
inactivityTime: number;
|
inactivityTime: number;
|
||||||
@ -169,7 +169,7 @@ export class TimeoutManager {
|
|||||||
/**
|
/**
|
||||||
* Apply socket timeout settings
|
* Apply socket timeout settings
|
||||||
*/
|
*/
|
||||||
public applySocketTimeouts(record: ConnectionRecord): void {
|
public applySocketTimeouts(record: IConnectionRecord): void {
|
||||||
// Skip for immortal keep-alive connections
|
// Skip for immortal keep-alive connections
|
||||||
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
||||||
// Disable timeouts completely for immortal connections
|
// Disable timeouts completely for immortal connections
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { SmartProxyOptions } from './models/interfaces.js';
|
import type { ISmartProxyOptions } from './models/interfaces.js';
|
||||||
import { SniHandler } from '../../tls/sni/sni-handler.js';
|
import { SniHandler } from '../../tls/sni/sni-handler.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,7 +16,7 @@ interface IConnectionInfo {
|
|||||||
* Manages TLS-related operations including SNI extraction and validation
|
* Manages TLS-related operations including SNI extraction and validation
|
||||||
*/
|
*/
|
||||||
export class TlsManager {
|
export class TlsManager {
|
||||||
constructor(private settings: SmartProxyOptions) {}
|
constructor(private settings: ISmartProxyOptions) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a data chunk appears to be a TLS handshake
|
* Check if a data chunk appears to be a TLS handshake
|
||||||
|
Reference in New Issue
Block a user