Compare commits

...

4 Commits

Author SHA1 Message Date
3e411667e6 18.0.0
Some checks failed
Default (tags) / security (push) Successful in 43s
Default (tags) / test (push) Failing after 1h11m0s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-05-15 09:34:01 +00:00
35d7dfcedf BREAKING CHANGE(IRouteSecurity): Consolidate duplicated IRouteSecurity interfaces by unifying property names 2025-05-15 09:34:01 +00:00
1067177d82 17.0.0
Some checks failed
Default (tags) / security (push) Successful in 45s
Default (tags) / test (push) Failing after 1h11m2s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-05-15 08:56:27 +00:00
ac3a888453 BREAKING CHANGE(smartproxy): Remove legacy migration utilities and deprecated forwarding helpers; consolidate route utilities, streamline interface definitions, and normalize IPv6-mapped IPv4 addresses 2025-05-15 08:56:27 +00:00
23 changed files with 471 additions and 502 deletions

View File

@ -1,5 +1,26 @@
# Changelog # Changelog
## 2025-05-15 - 18.0.0 - BREAKING CHANGE(IRouteSecurity)
Consolidate duplicated IRouteSecurity interfaces by unifying property names (using 'ipAllowList' and 'ipBlockList' exclusively) and removing legacy definitions, updating security checks throughout the codebase to handle IPv6-mapped IPv4 addresses and cleaning up deprecated forwarding helpers.
- Unified duplicate IRouteSecurity definitions into a single interface with consistent property names.
- Replaced 'allowedIps' and 'blockedIps' with 'ipAllowList' and 'ipBlockList' respectively.
- Updated references in security and route managers to use the new properties.
- Ensured consistent IPv6-mapped IPv4 normalization in IP security checks.
- Removed deprecated helpers and legacy code affecting port forwarding and route migration.
## 2025-05-15 - 17.0.0 - BREAKING CHANGE(smartproxy)
Remove legacy migration utilities and deprecated forwarding helpers; consolidate route utilities, streamline interface definitions, and normalize IPv6-mapped IPv4 addresses
- Deleted ts/proxies/smart-proxy/utils/route-migration-utils.ts and removed its re-exports
- Removed deprecated helper functions (httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough) from ts/forwarding/config/forwarding-types.ts
- Updated ts/common/port80-adapter.ts to consistently normalize IPv6-mapped IPv4 addresses in IP comparisons
- Cleaned up legacy connection handling code in route-connection-handler.ts by removing unused parameters and obsolete comments
- Consolidated route utilities by replacing imports from route-helpers.js with route-patterns.js in multiple modules
- Simplified interface definitions by removing legacy aliases and type checking functions from models/interfaces.ts
- Enhanced type safety by replacing any remaining 'any' types with specific types throughout the codebase
- Updated documentation comments and removed references to deprecated functionality
## 2025-05-14 - 16.0.4 - fix(smartproxy) ## 2025-05-14 - 16.0.4 - fix(smartproxy)
Update dynamic port mapping to support 'preserve' target port value Update dynamic port mapping to support 'preserve' target port value

View File

@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "16.0.4", "version": "18.0.0",
"private": false, "private": false,
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.", "description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@ -1,103 +1,146 @@
# SmartProxy Configuration Troubleshooting # SmartProxy Interface Consolidation Plan
## IPv6/IPv4 Mapping Issue ## Overview
### Problem Identified This document outlines a plan to consolidate duplicate and inconsistent interfaces in the SmartProxy codebase, specifically the `IRouteSecurity` interface which is defined twice with different properties. This inconsistency caused issues with security checks for port forwarding. The goal is to unify these interfaces, use consistent property naming, and improve code maintainability.
The SmartProxy is failing to match connections for wildcard domains (like `*.lossless.digital`) when IP restrictions are in place. After extensive debugging, the root cause has been identified:
When a connection comes in from an IPv4 address (e.g., `212.95.99.130`), the Node.js server receives it as an IPv6-mapped IPv4 address with the format `::ffff:212.95.99.130`. However, the route configuration is expecting the exact string `212.95.99.130`, causing a mismatch. ## Problem Description
From the debug logs: We currently have two separate `IRouteSecurity` interfaces defined in `ts/proxies/smart-proxy/models/route-types.ts`:
```
[DEBUG] Route rejected: clientIp mismatch. Request: ::ffff:212.95.99.130, Route patterns: ["212.95.99.130"]
```
### Solution
To fix this issue, update the route configurations to include both formats of the IP address. Here's how to modify the affected route:
1. **First definition** (lines 116-122) - Used in IRouteAction:
```typescript ```typescript
// Wildcard domain route for *.lossless.digital export interface IRouteSecurity {
{ allowedIps?: string[];
match: { blockedIps?: string[];
ports: 443, maxConnections?: number;
domains: ['*.lossless.digital'], authentication?: IRouteAuthentication;
clientIp: ['212.95.99.130', '::ffff:212.95.99.130'], // Include both formats
},
action: {
type: 'forward',
target: {
host: '212.95.99.130',
port: 443
},
tls: {
mode: 'passthrough'
},
security: {
allowedIps: ['212.95.99.130', '::ffff:212.95.99.130'] // Include both formats
}
},
name: 'Wildcard lossless.digital route (IP restricted)'
} }
``` ```
### Alternative Long-Term Fix 2. **Second definition** (lines 253-272) - Used directly in IRouteConfig:
```typescript
export interface IRouteSecurity {
rateLimit?: IRouteRateLimit;
basicAuth?: {...};
jwtAuth?: {...};
ipAllowList?: string[];
ipBlockList?: string[];
}
```
A more robust solution would be to modify the SmartProxy codebase to automatically handle IPv6-mapped IPv4 addresses by normalizing them before comparison. This would involve: This duplication with inconsistent naming (`allowedIps` vs `ipAllowList` and `blockedIps` vs `ipBlockList`) caused routing issues when IP security checks were used, as we had to implement a workaround to check both property names.
1. Modifying the `matchIpPattern` function in `route-manager.ts` to normalize IPv6-mapped IPv4 addresses: ## Implementation Plan
### Phase 1: Interface Consolidation
1. **Create a unified interface definition:**
- Create one comprehensive `IRouteSecurity` interface that includes all properties
- Use consistent property naming (standardize on `ipAllowList` and `ipBlockList`)
- Add proper documentation for each property
- Remove the duplicate interface definition
2. **Update references to use the unified interface:**
- Update all code that references the old interface properties
- Update all configurations to use the new property names
- Ensure implementation in `route-manager.ts` uses the correct property names
### Phase 2: Code and Documentation Updates
1. **Update type usages and documentation:**
- Update all code that creates or uses security configurations
- Update documentation to reflect the new interface structure
- Add examples of the correct property usage
- Document the breaking change in changelog.md
2. **Add tests:**
- Update existing tests to use the new property names
- Add test cases for all security configuration scenarios
- Verify that port range configurations with security settings work correctly
## Implementation Steps
```typescript ```typescript
private matchIpPattern(pattern: string, ip: string): boolean { // Step 1: Define the unified interface
// Normalize IPv6-mapped IPv4 addresses export interface IRouteSecurity {
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip; // Access control lists
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern; ipAllowList?: string[]; // IP addresses that are allowed to connect
ipBlockList?: string[]; // IP addresses that are blocked from connecting
// Handle exact match with normalized addresses // Connection limits
if (normalizedPattern === normalizedIp) { maxConnections?: number; // Maximum concurrent connections
// Authentication
authentication?: IRouteAuthentication;
// Rate limiting
rateLimit?: IRouteRateLimit;
// Authentication methods
basicAuth?: {
enabled: boolean;
users: Array<{ username: string; password: string }>;
realm?: string;
excludePaths?: string[];
};
jwtAuth?: {
enabled: boolean;
secret: string;
algorithm?: string;
issuer?: string;
audience?: string;
expiresIn?: number;
excludePaths?: string[];
};
}
```
Update `isClientIpAllowed` method to use only the new property names:
```typescript
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.ipBlockList && security.ipBlockList.length > 0) {
for (const pattern of security.ipBlockList) {
if (this.matchIpPattern(pattern, clientIp)) {
return false; // IP is blocked
}
}
}
// If there are allowed IPs, check them
if (security.ipAllowList && security.ipAllowList.length > 0) {
for (const pattern of security.ipAllowList) {
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; return true;
} }
// Rest of the existing function...
}
``` ```
2. Making similar modifications to other IP-related functions in the codebase. ## Expected Benefits
## Wild Card Domain Matching Issue - **Improved Consistency**: Single, unified interface with consistent property naming
- **Better Type Safety**: Eliminating confusing duplicate interface definitions
- **Reduced Errors**: Prevent misunderstandings about which property names to use
- **Forward Compatibility**: Clearer path for future security enhancements
- **Better Developer Experience**: Simplified interface with comprehensive documentation
### Explanation ## Testing Plan
The wildcard domain matching in SmartProxy works as follows: 1. Test with existing configurations using both old and new property names
2. Create specific test cases for port ranges with different security configurations
1. When a pattern like `*.lossless.digital` is specified, it's converted to a regex: `/^.*\.lossless\.digital$/i` 3. Verify that port forwarding with IP allow lists works correctly with the unified interface
2. This correctly matches any subdomain like `my.lossless.digital`, `api.lossless.digital`, etc.
3. However, it does NOT match the apex domain `lossless.digital` (without a subdomain)
If you need to match both the apex domain and subdomains, use a list:
```typescript
domains: ['lossless.digital', '*.lossless.digital']
```
## Debugging SmartProxy
To debug routing issues in SmartProxy:
1. Add detailed logging to the `route-manager.js` file in the `dist_ts` directory:
- `findMatchingRoute` method - to see what criteria are being checked
- `matchRouteDomain` method - to see domain matching logic
- `matchDomain` method - to see pattern matching
- `matchIpPattern` method - to see IP matching logic
2. Run the proxy with debugging enabled:
```
pnpm run startNew
```
3. Monitor the logs for detailed information about the routing process and identify where matches are failing.
## Priority and Route Order
Remember that routes are evaluated in priority order (higher priority first). If multiple routes could match the same request, ensure that the more specific routes have higher priority.
When routes have the same priority (or none specified), they're evaluated in the order they're defined in the configuration.

View File

@ -235,7 +235,7 @@ tap.test('SmartProxy: Should create instance with route-based config', async ()
port: 8080 port: 8080
}, },
security: { security: {
allowedIps: ['127.0.0.1', '192.168.0.*'], ipAllowList: ['127.0.0.1', '192.168.0.*'],
maxConnections: 100 maxConnections: 100
} }
}, },

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '16.0.4', version: '18.0.0',
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.' description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
} }

View File

@ -21,9 +21,21 @@ export function convertToLegacyForwardConfig(
? forwardConfig.target.host[0] // Use the first host in the array ? forwardConfig.target.host[0] // Use the first host in the array
: forwardConfig.target.host; : forwardConfig.target.host;
// Extract port number, handling different port formats
let port: number;
if (typeof forwardConfig.target.port === 'function') {
// Use a default port for function-based ports in adapter context
port = 80;
} else if (forwardConfig.target.port === 'preserve') {
// For 'preserve', use the default port 80 in this adapter context
port = 80;
} else {
port = forwardConfig.target.port;
}
return { return {
ip: host, ip: host,
port: forwardConfig.target.port port: port
}; };
} }
@ -75,11 +87,23 @@ export function createPort80HandlerOptions(
forwardConfig.type === 'https-terminate-to-https')); forwardConfig.type === 'https-terminate-to-https'));
if (supportsHttp) { if (supportsHttp) {
// Determine port value handling different formats
let port: number;
if (typeof forwardConfig.target.port === 'function') {
// Use a default port for function-based ports
port = 80;
} else if (forwardConfig.target.port === 'preserve') {
// For 'preserve', use 80 in this adapter context
port = 80;
} else {
port = forwardConfig.target.port;
}
options.forward = { options.forward = {
ip: Array.isArray(forwardConfig.target.host) ip: Array.isArray(forwardConfig.target.host)
? forwardConfig.target.host[0] ? forwardConfig.target.host[0]
: forwardConfig.target.host, : forwardConfig.target.host,
port: forwardConfig.target.port port: port
}; };
} }

View File

@ -199,8 +199,8 @@ export class SharedSecurityManager {
} }
// Check IP against route security settings // Check IP against route security settings
const ipAllowList = route.security.ipAllowList || route.security.allowedIps; const ipAllowList = route.security.ipAllowList;
const ipBlockList = route.security.ipBlockList || route.security.blockedIps; const ipBlockList = route.security.ipBlockList;
const allowed = this.isIPAuthorized(clientIp, ipAllowList, ipBlockList); const allowed = this.isIPAuthorized(clientIp, ipAllowList, ipBlockList);

View File

@ -1,10 +1,8 @@
import type * as plugins from '../../plugins.js'; import type * as plugins from '../../plugins.js';
/** /**
* @deprecated The legacy forwarding types are being replaced by the route-based configuration system.
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
*
* The primary forwarding types supported by SmartProxy * The primary forwarding types supported by SmartProxy
* Used for configuration compatibility
*/ */
export type TForwardingType = export type TForwardingType =
| 'http-only' // HTTP forwarding only (no HTTPS) | 'http-only' // HTTP forwarding only (no HTTPS)
@ -35,7 +33,7 @@ export interface IForwardingHandler extends plugins.EventEmitter {
handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void; handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void;
} }
// Import and re-export the route-based helpers for seamless transition // Route-based helpers are now available directly from route-patterns.ts
import { import {
createHttpRoute, createHttpRoute,
createHttpsTerminateRoute, createHttpsTerminateRoute,
@ -43,7 +41,7 @@ import {
createHttpToHttpsRedirect, createHttpToHttpsRedirect,
createCompleteHttpsServer, createCompleteHttpsServer,
createLoadBalancerRoute createLoadBalancerRoute
} from '../../proxies/smart-proxy/utils/route-helpers.js'; } from '../../proxies/smart-proxy/utils/route-patterns.js';
export { export {
createHttpRoute, createHttpRoute,
@ -54,23 +52,20 @@ export {
createLoadBalancerRoute createLoadBalancerRoute
}; };
/** // Note: Legacy helper functions have been removed
* @deprecated These helper functions are maintained for backward compatibility. // Please use the route-based helpers instead:
* Please use the route-based helpers instead: // - createHttpRoute
* - createHttpRoute // - createHttpsTerminateRoute
* - createHttpsTerminateRoute // - createHttpsPassthroughRoute
* - createHttpsPassthroughRoute // - createHttpToHttpsRedirect
* - createHttpToHttpsRedirect
*/
import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js'; import type { IRouteConfig } from '../../proxies/smart-proxy/models/route-types.js';
import { domainConfigToRouteConfig } from '../../proxies/smart-proxy/utils/route-migration-utils.js';
// For backward compatibility // For backward compatibility, kept only the basic configuration interface
export interface IForwardConfig { export interface IForwardConfig {
type: TForwardingType; type: TForwardingType;
target: { target: {
host: string | string[]; host: string | string[];
port: number; port: number | 'preserve' | ((ctx: any) => number);
}; };
http?: any; http?: any;
https?: any; https?: any;
@ -79,56 +74,3 @@ export interface IForwardConfig {
advanced?: any; advanced?: any;
[key: string]: any; [key: string]: any;
} }
export interface IDeprecatedForwardConfig {
type: TForwardingType;
target: {
host: string | string[];
port: number;
};
[key: string]: any;
}
/**
* @deprecated Use createHttpRoute instead
*/
export const httpOnly = (
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
): IDeprecatedForwardConfig => ({
type: 'http-only',
target: partialConfig.target,
...(partialConfig)
});
/**
* @deprecated Use createHttpsTerminateRoute instead
*/
export const tlsTerminateToHttp = (
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
): IDeprecatedForwardConfig => ({
type: 'https-terminate-to-http',
target: partialConfig.target,
...(partialConfig)
});
/**
* @deprecated Use createHttpsTerminateRoute with reencrypt option instead
*/
export const tlsTerminateToHttps = (
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
): IDeprecatedForwardConfig => ({
type: 'https-terminate-to-https',
target: partialConfig.target,
...(partialConfig)
});
/**
* @deprecated Use createHttpsPassthroughRoute instead
*/
export const httpsPassthrough = (
partialConfig: Partial<IDeprecatedForwardConfig> & Pick<IDeprecatedForwardConfig, 'target'>
): IDeprecatedForwardConfig => ({
type: 'https-passthrough',
target: partialConfig.target,
...(partialConfig)
});

View File

@ -5,5 +5,22 @@
* See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration. * See /ts/proxies/smart-proxy/models/route-types.ts for the new route-based configuration.
*/ */
export * from './forwarding-types.js'; export type {
export * from '../../proxies/smart-proxy/utils/route-helpers.js'; TForwardingType,
IForwardConfig,
IForwardingHandler
} from './forwarding-types.js';
export {
ForwardingHandlerEvents
} from './forwarding-types.js';
// Import route helpers from route-patterns instead of deleted route-helpers
export {
createHttpRoute,
createHttpsTerminateRoute,
createHttpsPassthroughRoute,
createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../../proxies/smart-proxy/utils/route-patterns.js';

View File

@ -122,9 +122,14 @@ export class ForwardingHandlerFactory {
throw new Error('Target must include a host or array of hosts'); throw new Error('Target must include a host or array of hosts');
} }
if (!config.target.port || config.target.port <= 0 || config.target.port > 65535) { // Validate port if it's a number
if (typeof config.target.port === 'number') {
if (config.target.port <= 0 || config.target.port > 65535) {
throw new Error('Target must include a valid port (1-65535)'); throw new Error('Target must include a valid port (1-65535)');
} }
} else if (config.target.port !== 'preserve' && typeof config.target.port !== 'function') {
throw new Error('Target port must be a number, "preserve", or a function');
}
// Type-specific validation // Type-specific validation
switch (config.type) { switch (config.type) {

View File

@ -40,9 +40,10 @@ export abstract class ForwardingHandler extends plugins.EventEmitter implements
/** /**
* Get a target from the configuration, supporting round-robin selection * Get a target from the configuration, supporting round-robin selection
* @param incomingPort Optional incoming port for 'preserve' mode
* @returns A resolved target object with host and port * @returns A resolved target object with host and port
*/ */
protected getTargetFromConfig(): { host: string, port: number } { protected getTargetFromConfig(incomingPort: number = 80): { host: string, port: number } {
const { target } = this.config; const { target } = this.config;
// Handle round-robin host selection // Handle round-robin host selection
@ -55,17 +56,42 @@ export abstract class ForwardingHandler extends plugins.EventEmitter implements
const randomIndex = Math.floor(Math.random() * target.host.length); const randomIndex = Math.floor(Math.random() * target.host.length);
return { return {
host: target.host[randomIndex], host: target.host[randomIndex],
port: target.port port: this.resolvePort(target.port, incomingPort)
}; };
} }
// Single host // Single host
return { return {
host: target.host, host: target.host,
port: target.port port: this.resolvePort(target.port, incomingPort)
}; };
} }
/**
* Resolves a port value, handling 'preserve' and function ports
* @param port The port value to resolve
* @param incomingPort Optional incoming port to use for 'preserve' mode
*/
protected resolvePort(
port: number | 'preserve' | ((ctx: any) => number),
incomingPort: number = 80
): number {
if (typeof port === 'function') {
try {
// Create a minimal context for the function that includes the incoming port
const ctx = { port: incomingPort };
return port(ctx);
} catch (err) {
console.error('Error resolving port function:', err);
return incomingPort; // Fall back to incoming port
}
} else if (port === 'preserve') {
return incomingPort; // Use the actual incoming port for 'preserve'
} else {
return port;
}
}
/** /**
* Redirect an HTTP request to HTTPS * Redirect an HTTP request to HTTPS
* @param req The HTTP request * @param req The HTTP request

View File

@ -38,6 +38,7 @@ export class HttpForwardingHandler extends ForwardingHandler {
// For HTTP, we mainly handle parsed requests, but we can still set up // For HTTP, we mainly handle parsed requests, but we can still set up
// some basic connection tracking // some basic connection tracking
const remoteAddress = socket.remoteAddress || 'unknown'; const remoteAddress = socket.remoteAddress || 'unknown';
const localPort = socket.localPort || 80;
socket.on('close', (hadError) => { socket.on('close', (hadError) => {
this.emit(ForwardingHandlerEvents.DISCONNECTED, { this.emit(ForwardingHandlerEvents.DISCONNECTED, {
@ -54,7 +55,8 @@ export class HttpForwardingHandler extends ForwardingHandler {
}); });
this.emit(ForwardingHandlerEvents.CONNECTED, { this.emit(ForwardingHandlerEvents.CONNECTED, {
remoteAddress remoteAddress,
localPort
}); });
} }
@ -64,8 +66,11 @@ export class HttpForwardingHandler extends ForwardingHandler {
* @param res The HTTP response * @param res The HTTP response
*/ */
public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void { public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
// Get the target from configuration // Get the local port from the request (for 'preserve' port handling)
const target = this.getTargetFromConfig(); const localPort = req.socket.localPort || 80;
// Get the target from configuration, passing the incoming port
const target = this.getTargetFromConfig(localPort);
// Create a custom headers object with variables for substitution // Create a custom headers object with variables for substitution
const variables = { const variables = {

View File

@ -3,9 +3,6 @@
* Provides a flexible and type-safe way to configure and manage various forwarding strategies * Provides a flexible and type-safe way to configure and manage various forwarding strategies
*/ */
// Export types and configuration
export * from './config/forwarding-types.js';
// Export handlers // Export handlers
export { ForwardingHandler } from './handlers/base-handler.js'; export { ForwardingHandler } from './handlers/base-handler.js';
export * from './handlers/http-handler.js'; export * from './handlers/http-handler.js';
@ -16,20 +13,23 @@ export * from './handlers/https-terminate-to-https-handler.js';
// Export factory // Export factory
export * from './factory/forwarding-factory.js'; export * from './factory/forwarding-factory.js';
// Helper functions as a convenience object // Export types - these include TForwardingType and IForwardConfig
import { export type {
httpOnly, TForwardingType,
tlsTerminateToHttp, IForwardConfig,
tlsTerminateToHttps, IForwardingHandler
httpsPassthrough
} from './config/forwarding-types.js'; } from './config/forwarding-types.js';
// Export route-based helpers from smart-proxy export {
export * from '../proxies/smart-proxy/utils/route-helpers.js'; ForwardingHandlerEvents
} from './config/forwarding-types.js';
export const helpers = { // Export route helpers directly from route-patterns
httpOnly, export {
tlsTerminateToHttp, createHttpRoute,
tlsTerminateToHttps, createHttpsTerminateRoute,
httpsPassthrough createHttpsPassthroughRoute,
}; createHttpToHttpsRedirect,
createCompleteHttpsServer,
createLoadBalancerRoute
} from '../proxies/smart-proxy/utils/route-patterns.js';

View File

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

View File

@ -8,23 +8,7 @@ import type { TForwardingType } from '../../../forwarding/config/forwarding-type
*/ */
export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01'; export type TSmartProxyCertProvisionObject = plugins.tsclass.network.ICert | 'http01';
/** // Legacy options and type checking functions have been removed
* Alias for backward compatibility with code that uses IRoutedSmartProxyOptions
*/
export type IRoutedSmartProxyOptions = ISmartProxyOptions;
/**
* 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;
}
/** /**
* SmartProxy configuration options * SmartProxy configuration options
@ -43,8 +27,8 @@ export interface ISmartProxyOptions {
port: number; // Default port to use when not specified in routes port: number; // Default port to use when not specified in routes
}; };
security?: { security?: {
allowedIps?: string[]; // Default allowed IPs ipAllowList?: string[]; // Default allowed IPs
blockedIps?: string[]; // Default blocked IPs ipBlockList?: string[]; // Default blocked IPs
maxConnections?: number; // Default max connections maxConnections?: number; // Default max connections
}; };
preserveSourceIP?: boolean; // Default source IP preservation preserveSourceIP?: boolean; // Default source IP preservation

View File

@ -112,13 +112,39 @@ export interface IRouteAuthentication {
} }
/** /**
* Security options for route actions * Security options for routes
*/ */
export interface IRouteSecurity { export interface IRouteSecurity {
allowedIps?: string[]; // Access control lists
blockedIps?: string[]; ipAllowList?: string[]; // IP addresses that are allowed to connect
maxConnections?: number; ipBlockList?: string[]; // IP addresses that are blocked from connecting
// Connection limits
maxConnections?: number; // Maximum concurrent connections
// Authentication
authentication?: IRouteAuthentication; authentication?: IRouteAuthentication;
// Rate limiting
rateLimit?: IRouteRateLimit;
// Authentication methods
basicAuth?: {
enabled: boolean;
users: Array<{ username: string; password: string }>;
realm?: string;
excludePaths?: string[];
};
jwtAuth?: {
enabled: boolean;
secret: string;
algorithm?: string;
issuer?: string;
audience?: string;
expiresIn?: number;
excludePaths?: string[];
};
} }
/** /**
@ -247,29 +273,7 @@ export interface IRouteRateLimit {
errorMessage?: string; errorMessage?: string;
} }
/** // IRouteSecurity is defined above - unified definition is used for all routes
* Security features for routes
*/
export interface IRouteSecurity {
rateLimit?: IRouteRateLimit;
basicAuth?: {
enabled: boolean;
users: Array<{ username: string; password: string }>;
realm?: string;
excludePaths?: string[];
};
jwtAuth?: {
enabled: boolean;
secret: string;
algorithm?: string;
issuer?: string;
audience?: string;
expiresIn?: number;
excludePaths?: string[];
};
ipAllowList?: string[];
ipBlockList?: string[];
}
/** /**
* CORS configuration for a route * CORS configuration for a route
@ -321,61 +325,4 @@ export interface IRouteConfig {
enabled?: boolean; // Whether the route is active (default: true) enabled?: boolean; // Whether the route is active (default: true)
} }
/** // Configuration moved to models/interfaces.ts as ISmartProxyOptions
* 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>;
}

View File

@ -3,9 +3,7 @@ import type {
IConnectionRecord, IConnectionRecord,
ISmartProxyOptions ISmartProxyOptions
} from './models/interfaces.js'; } from './models/interfaces.js';
import { // Route checking functions have been removed
isRoutedOptions
} from './models/interfaces.js';
import type { import type {
IRouteConfig, IRouteConfig,
IRouteAction, IRouteAction,
@ -291,11 +289,11 @@ export class RouteConnectionHandler {
// Check default security settings // Check default security settings
const defaultSecuritySettings = this.settings.defaults?.security; const defaultSecuritySettings = this.settings.defaults?.security;
if (defaultSecuritySettings) { if (defaultSecuritySettings) {
if (defaultSecuritySettings.allowedIps && defaultSecuritySettings.allowedIps.length > 0) { if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
const isAllowed = this.securityManager.isIPAuthorized( const isAllowed = this.securityManager.isIPAuthorized(
remoteIP, remoteIP,
defaultSecuritySettings.allowedIps, defaultSecuritySettings.ipAllowList,
defaultSecuritySettings.blockedIps || [] defaultSecuritySettings.ipBlockList || []
); );
if (!isAllowed) { if (!isAllowed) {
@ -316,7 +314,6 @@ export class RouteConnectionHandler {
return this.setupDirectConnection( return this.setupDirectConnection(
socket, socket,
record, record,
undefined,
serverName, serverName,
initialChunk, initialChunk,
undefined, undefined,
@ -457,7 +454,6 @@ export class RouteConnectionHandler {
return this.setupDirectConnection( return this.setupDirectConnection(
socket, socket,
record, record,
undefined,
record.lockedDomain, record.lockedDomain,
initialChunk, initialChunk,
undefined, undefined,
@ -538,7 +534,6 @@ export class RouteConnectionHandler {
return this.setupDirectConnection( return this.setupDirectConnection(
socket, socket,
record, record,
undefined,
record.lockedDomain, record.lockedDomain,
initialChunk, initialChunk,
undefined, undefined,
@ -656,17 +651,12 @@ export class RouteConnectionHandler {
this.connectionManager.initiateCleanupOnce(record, 'route_blocked'); 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 * Sets up a direct connection to the target
*/ */
private setupDirectConnection( private setupDirectConnection(
socket: plugins.net.Socket, socket: plugins.net.Socket,
record: IConnectionRecord, record: IConnectionRecord,
_unused?: any, // kept for backward compatibility
serverName?: string, serverName?: string,
initialChunk?: Buffer, initialChunk?: Buffer,
overridePort?: number, overridePort?: number,

View File

@ -6,12 +6,7 @@ import type {
TPortRange TPortRange
} from './models/route-types.js'; } from './models/route-types.js';
import type { import type {
ISmartProxyOptions, ISmartProxyOptions
IRoutedSmartProxyOptions
} from './models/interfaces.js';
import {
isRoutedOptions,
isLegacyOptions
} from './models/interfaces.js'; } from './models/interfaces.js';
/** /**
@ -29,12 +24,12 @@ export interface IRouteMatchResult {
export class RouteManager extends plugins.EventEmitter { export class RouteManager extends plugins.EventEmitter {
private routes: IRouteConfig[] = []; private routes: IRouteConfig[] = [];
private portMap: Map<number, IRouteConfig[]> = new Map(); private portMap: Map<number, IRouteConfig[]> = new Map();
private options: IRoutedSmartProxyOptions; private options: ISmartProxyOptions;
constructor(options: ISmartProxyOptions) { constructor(options: ISmartProxyOptions) {
super(); super();
// We no longer support legacy options, always use provided options // Store options
this.options = options; this.options = options;
// Initialize routes from either source // Initialize routes from either source
@ -218,8 +213,8 @@ export class RouteManager extends plugins.EventEmitter {
} }
// Check blocked IPs first // Check blocked IPs first
if (security.blockedIps && security.blockedIps.length > 0) { if (security.ipBlockList && security.ipBlockList.length > 0) {
for (const pattern of security.blockedIps) { for (const pattern of security.ipBlockList) {
if (this.matchIpPattern(pattern, clientIp)) { if (this.matchIpPattern(pattern, clientIp)) {
return false; // IP is blocked return false; // IP is blocked
} }
@ -227,8 +222,8 @@ export class RouteManager extends plugins.EventEmitter {
} }
// If there are allowed IPs, check them // If there are allowed IPs, check them
if (security.allowedIps && security.allowedIps.length > 0) { if (security.ipAllowList && security.ipAllowList.length > 0) {
for (const pattern of security.allowedIps) { for (const pattern of security.ipAllowList) {
if (this.matchIpPattern(pattern, clientIp)) { if (this.matchIpPattern(pattern, clientIp)) {
return true; // IP is allowed return true; // IP is allowed
} }

View File

@ -63,16 +63,15 @@ export class SecurityManager {
} }
/** /**
* Check if an IP is authorized using forwarding security rules * Check if an IP is authorized using security rules
* *
* This method is used to determine if an IP is allowed to connect, based on security * This method is used to determine if an IP is allowed to connect, based on security
* rules configured in the forwarding configuration. The allowed and blocked IPs are * rules configured in the route configuration. The allowed and blocked IPs are
* typically derived from domain.forwarding.security.allowedIps and blockedIps through * typically derived from route.security.ipAllowList and ipBlockList.
* DomainConfigManager.getEffectiveIPRules().
* *
* @param ip - The IP address to check * @param ip - The IP address to check
* @param allowedIPs - Array of allowed IP patterns from forwarding.security.allowedIps * @param allowedIPs - Array of allowed IP patterns from security.ipAllowList
* @param blockedIPs - Array of blocked IP patterns from forwarding.security.blockedIps * @param blockedIPs - Array of blocked IP patterns from security.ipBlockList
* @returns true if IP is authorized, false if blocked * @returns true if IP is authorized, false if blocked
*/ */
public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean { public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean {
@ -94,10 +93,10 @@ export class SecurityManager {
* Check if the IP matches any of the glob patterns from security configuration * Check if the IP matches any of the glob patterns from security configuration
* *
* This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization. * This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization.
* It's used to implement IP filtering based on the forwarding.security configuration. * It's used to implement IP filtering based on the route.security configuration.
* *
* @param ip - The IP address to check * @param ip - The IP address to check
* @param patterns - Array of glob patterns from forwarding.security.allowedIps or blockedIps * @param patterns - Array of glob patterns from security.ipAllowList or ipBlockList
* @returns true if IP matches any pattern, false otherwise * @returns true if IP matches any pattern, false otherwise
*/ */
private isGlobIPMatch(ip: string, patterns: string[]): boolean { private isGlobIPMatch(ip: string, patterns: string[]): boolean {

View File

@ -19,10 +19,8 @@ import { createPort80HandlerOptions } from '../../common/port80-adapter.js';
// Import types and utilities // Import types and utilities
import type { import type {
ISmartProxyOptions, ISmartProxyOptions
IRoutedSmartProxyOptions
} from './models/interfaces.js'; } from './models/interfaces.js';
import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js';
import type { IRouteConfig } from './models/route-types.js'; import type { IRouteConfig } from './models/route-types.js';
/** /**
@ -650,7 +648,7 @@ export class SmartProxy extends plugins.EventEmitter {
const domains: string[] = []; const domains: string[] = [];
// Get domains from routes // Get domains from routes
const routes = isRoutedOptions(this.settings) ? this.settings.routes : []; const routes = this.settings.routes || [];
for (const route of routes) { for (const route of routes) {
if (!route.match.domains) continue; if (!route.match.domains) continue;

View File

@ -5,8 +5,7 @@
* including helpers, validators, utilities, and patterns for working with routes. * including helpers, validators, utilities, and patterns for working with routes.
*/ */
// Export route helpers for creating routes // Route helpers have been consolidated in route-patterns.js
export * from './route-helpers.js';
// Export route validators for validating route configurations // Export route validators for validating route configurations
export * from './route-validators.js'; export * from './route-validators.js';
@ -35,6 +34,4 @@ export {
addJwtAuth addJwtAuth
}; };
// Export migration utilities for transitioning from domain-based to route-based configs // Migration utilities have been removed as they are no longer needed
// Note: These will be removed in a future version once migration is complete
export * from './route-migration-utils.js';

View File

@ -1,165 +0,0 @@
/**
* Route Migration Utilities
*
* This file provides utility functions for migrating from legacy domain-based
* configuration to the new route-based configuration system. These functions
* are temporary and will be removed after the migration is complete.
*/
import type { TForwardingType } from '../../../forwarding/config/forwarding-types.js';
import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
/**
* Legacy domain config interface (for migration only)
* @deprecated This interface will be removed in a future version
*/
export interface ILegacyDomainConfig {
domains: string[];
forwarding: {
type: TForwardingType;
target: {
host: string | string[];
port: number;
};
[key: string]: any;
};
}
/**
* Convert a legacy domain config to a route-based config
* @param domainConfig Legacy domain configuration
* @param additionalOptions Additional options to add to the route
* @returns Route configuration
* @deprecated This function will be removed in a future version
*/
export function domainConfigToRouteConfig(
domainConfig: ILegacyDomainConfig,
additionalOptions: Partial<IRouteConfig> = {}
): IRouteConfig {
// Default port based on forwarding type
let defaultPort = 80;
let tlsMode: 'passthrough' | 'terminate' | 'terminate-and-reencrypt' | undefined;
switch (domainConfig.forwarding.type) {
case 'http-only':
defaultPort = 80;
break;
case 'https-passthrough':
defaultPort = 443;
tlsMode = 'passthrough';
break;
case 'https-terminate-to-http':
defaultPort = 443;
tlsMode = 'terminate';
break;
case 'https-terminate-to-https':
defaultPort = 443;
tlsMode = 'terminate-and-reencrypt';
break;
}
// Create route match criteria
const match: IRouteMatch = {
ports: additionalOptions.match?.ports || defaultPort,
domains: domainConfig.domains
};
// Create route target
const target: IRouteTarget = {
host: domainConfig.forwarding.target.host,
port: domainConfig.forwarding.target.port
};
// Create route action
const action: IRouteAction = {
type: 'forward',
target
};
// Add TLS configuration if needed
if (tlsMode) {
action.tls = {
mode: tlsMode,
certificate: 'auto'
};
// If the legacy config has custom certificates, use them
if (domainConfig.forwarding.https?.customCert) {
action.tls.certificate = {
key: domainConfig.forwarding.https.customCert.key,
cert: domainConfig.forwarding.https.customCert.cert
};
}
}
// Add security options if present
if (domainConfig.forwarding.security) {
action.security = domainConfig.forwarding.security;
}
// Create the route config
const routeConfig: IRouteConfig = {
match,
action,
// Include a name based on domains if not provided
name: additionalOptions.name || `Legacy route for ${domainConfig.domains.join(', ')}`,
// Include a note that this was converted from a legacy config
description: additionalOptions.description || 'Converted from legacy domain configuration'
};
// Add optional properties if provided
if (additionalOptions.priority !== undefined) {
routeConfig.priority = additionalOptions.priority;
}
if (additionalOptions.tags) {
routeConfig.tags = additionalOptions.tags;
}
return routeConfig;
}
/**
* Convert an array of legacy domain configs to route configurations
* @param domainConfigs Array of legacy domain configurations
* @returns Array of route configurations
* @deprecated This function will be removed in a future version
*/
export function domainConfigsToRouteConfigs(
domainConfigs: ILegacyDomainConfig[]
): IRouteConfig[] {
return domainConfigs.map(config => domainConfigToRouteConfig(config));
}
/**
* Extract domains from a route configuration
* @param route Route configuration
* @returns Array of domains
*/
export function extractDomainsFromRoute(route: IRouteConfig): string[] {
if (!route.match.domains) {
return [];
}
return Array.isArray(route.match.domains)
? route.match.domains
: [route.match.domains];
}
/**
* Extract domains from an array of route configurations
* @param routes Array of route configurations
* @returns Array of unique domains
*/
export function extractDomainsFromRoutes(routes: IRouteConfig[]): string[] {
const domains = new Set<string>();
for (const route of routes) {
const routeDomains = extractDomainsFromRoute(route);
for (const domain of routeDomains) {
domains.add(domain);
}
}
return Array.from(domains);
}

View File

@ -5,10 +5,154 @@
* These patterns can be used as templates for creating route configurations. * These patterns can be used as templates for creating route configurations.
*/ */
import type { IRouteConfig } from '../models/route-types.js'; import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget } from '../models/route-types.js';
import { createHttpRoute, createHttpsTerminateRoute, createHttpsPassthroughRoute, createCompleteHttpsServer } from './route-helpers.js';
import { mergeRouteConfigs } from './route-utils.js'; import { mergeRouteConfigs } from './route-utils.js';
/**
* Create a basic HTTP route configuration
*/
export function createHttpRoute(
domains: string | string[],
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
options: Partial<IRouteConfig> = {}
): IRouteConfig {
const route: IRouteConfig = {
match: {
domains,
ports: 80
},
action: {
type: 'forward',
target: {
host: target.host,
port: target.port
}
},
name: options.name || `HTTP: ${Array.isArray(domains) ? domains.join(', ') : domains}`
};
return mergeRouteConfigs(route, options);
}
/**
* Create an HTTPS route with TLS termination
*/
export function createHttpsTerminateRoute(
domains: string | string[],
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
options: Partial<IRouteConfig> & {
certificate?: 'auto' | { key: string; cert: string };
reencrypt?: boolean;
} = {}
): IRouteConfig {
const route: IRouteConfig = {
match: {
domains,
ports: 443
},
action: {
type: 'forward',
target: {
host: target.host,
port: target.port
},
tls: {
mode: options.reencrypt ? 'terminate-and-reencrypt' : 'terminate',
certificate: options.certificate || 'auto'
}
},
name: options.name || `HTTPS (terminate): ${Array.isArray(domains) ? domains.join(', ') : domains}`
};
return mergeRouteConfigs(route, options);
}
/**
* Create an HTTPS route with TLS passthrough
*/
export function createHttpsPassthroughRoute(
domains: string | string[],
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
options: Partial<IRouteConfig> = {}
): IRouteConfig {
const route: IRouteConfig = {
match: {
domains,
ports: 443
},
action: {
type: 'forward',
target: {
host: target.host,
port: target.port
},
tls: {
mode: 'passthrough'
}
},
name: options.name || `HTTPS (passthrough): ${Array.isArray(domains) ? domains.join(', ') : domains}`
};
return mergeRouteConfigs(route, options);
}
/**
* Create an HTTP to HTTPS redirect route
*/
export function createHttpToHttpsRedirect(
domains: string | string[],
options: Partial<IRouteConfig> & {
redirectCode?: 301 | 302 | 307 | 308;
preservePath?: boolean;
} = {}
): IRouteConfig {
const route: IRouteConfig = {
match: {
domains,
ports: 80
},
action: {
type: 'redirect',
redirect: {
to: options.preservePath ? 'https://{domain}{path}' : 'https://{domain}',
status: options.redirectCode || 301
}
},
name: options.name || `HTTP to HTTPS redirect: ${Array.isArray(domains) ? domains.join(', ') : domains}`
};
return mergeRouteConfigs(route, options);
}
/**
* Create a complete HTTPS server with redirect from HTTP
*/
export function createCompleteHttpsServer(
domains: string | string[],
target: { host: string | string[]; port: number | 'preserve' | ((ctx: any) => number) },
options: Partial<IRouteConfig> & {
certificate?: 'auto' | { key: string; cert: string };
tlsMode?: 'terminate' | 'passthrough' | 'terminate-and-reencrypt';
redirectCode?: 301 | 302 | 307 | 308;
} = {}
): IRouteConfig[] {
// Create the TLS route based on the selected mode
const tlsRoute = options.tlsMode === 'passthrough'
? createHttpsPassthroughRoute(domains, target, options)
: createHttpsTerminateRoute(domains, target, {
...options,
reencrypt: options.tlsMode === 'terminate-and-reencrypt'
});
// Create the HTTP to HTTPS redirect route
const redirectRoute = createHttpToHttpsRedirect(domains, {
redirectCode: options.redirectCode,
preservePath: true
});
return [tlsRoute, redirectRoute];
}
/** /**
* Create an API Gateway route pattern * Create an API Gateway route pattern
* @param domains Domain(s) to match * @param domains Domain(s) to match