fix(network-proxy, route-utils, route-manager): Normalize IPv6-mapped IPv4 addresses in IP matching functions and remove deprecated legacy configuration methods in NetworkProxy. Update route-utils and route-manager to compare both canonical and IPv6-mapped IP forms, adjust tests accordingly, and clean up legacy exports.
This commit is contained in:
@ -291,12 +291,15 @@ export class RouteManager {
|
||||
|
||||
/**
|
||||
* Match an IP pattern against an IP
|
||||
* Supports exact matches, wildcard patterns, and CIDR notation
|
||||
*/
|
||||
private matchIp(pattern: string, ip: string): boolean {
|
||||
// Exact match
|
||||
if (pattern === ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wildcard matching (e.g., 192.168.0.*)
|
||||
if (pattern.includes('*')) {
|
||||
const regexPattern = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
@ -306,10 +309,65 @@ export class RouteManager {
|
||||
return regex.test(ip);
|
||||
}
|
||||
|
||||
// TODO: Implement CIDR matching
|
||||
// CIDR matching (e.g., 192.168.0.0/24)
|
||||
if (pattern.includes('/')) {
|
||||
try {
|
||||
const [subnet, bits] = pattern.split('/');
|
||||
|
||||
// Convert IP addresses to numeric format for comparison
|
||||
const ipBinary = this.ipToBinary(ip);
|
||||
const subnetBinary = this.ipToBinary(subnet);
|
||||
|
||||
if (!ipBinary || !subnetBinary) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the subnet mask from CIDR notation
|
||||
const mask = parseInt(bits, 10);
|
||||
if (isNaN(mask) || mask < 0 || mask > 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the first 'mask' bits match between IP and subnet
|
||||
return ipBinary.slice(0, mask) === subnetBinary.slice(0, mask);
|
||||
} catch (error) {
|
||||
// If we encounter any error during CIDR matching, return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an IP address to its binary representation
|
||||
* @param ip The IP address to convert
|
||||
* @returns Binary string representation or null if invalid
|
||||
*/
|
||||
private ipToBinary(ip: string): string | null {
|
||||
// Handle IPv4 addresses only for now
|
||||
const parts = ip.split('.');
|
||||
|
||||
// Validate IP format
|
||||
if (parts.length !== 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert each octet to 8-bit binary and concatenate
|
||||
try {
|
||||
return parts
|
||||
.map(part => {
|
||||
const num = parseInt(part, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255) {
|
||||
throw new Error('Invalid IP octet');
|
||||
}
|
||||
return num.toString(2).padStart(8, '0');
|
||||
})
|
||||
.join('');
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -500,68 +500,8 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use updateRouteConfigs instead
|
||||
* Legacy method for updating proxy configurations using IReverseProxyConfig
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public async updateProxyConfigs(
|
||||
proxyConfigsArg: IReverseProxyConfig[]
|
||||
): Promise<void> {
|
||||
this.logger.info(`Converting ${proxyConfigsArg.length} legacy configs to route configs`);
|
||||
|
||||
// Convert legacy configs to route configs
|
||||
const routes: IRouteConfig[] = proxyConfigsArg.map(config =>
|
||||
convertLegacyConfigToRouteConfig(config, this.options.port)
|
||||
);
|
||||
|
||||
// Use the primary method
|
||||
return this.updateRouteConfigs(routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use route-based configuration instead
|
||||
* Converts SmartProxy domain configurations to NetworkProxy configs
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public convertSmartProxyConfigs(
|
||||
domainConfigs: Array<{
|
||||
domains: string[];
|
||||
targetIPs?: string[];
|
||||
allowedIPs?: string[];
|
||||
}>,
|
||||
sslKeyPair?: { key: string; cert: string }
|
||||
): IReverseProxyConfig[] {
|
||||
this.logger.warn('convertSmartProxyConfigs is deprecated - use route-based configuration instead');
|
||||
|
||||
const proxyConfigs: IReverseProxyConfig[] = [];
|
||||
|
||||
// Use default certificates if not provided
|
||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
||||
const sslKey = sslKeyPair?.key || defaultCerts.key;
|
||||
const sslCert = sslKeyPair?.cert || defaultCerts.cert;
|
||||
|
||||
for (const domainConfig of domainConfigs) {
|
||||
// Each domain in the domains array gets its own config
|
||||
for (const domain of domainConfig.domains) {
|
||||
// Skip non-hostname patterns (like IP addresses)
|
||||
if (domain.match(/^\d+\.\d+\.\d+\.\d+$/) || domain === '*' || domain === 'localhost') {
|
||||
continue;
|
||||
}
|
||||
|
||||
proxyConfigs.push({
|
||||
hostName: domain,
|
||||
destinationIps: domainConfig.targetIPs || ['localhost'],
|
||||
destinationPorts: [this.options.port], // Use the NetworkProxy port
|
||||
privateKey: sslKey,
|
||||
publicKey: sslCert
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Converted ${domainConfigs.length} SmartProxy configs to ${proxyConfigs.length} NetworkProxy configs`);
|
||||
return proxyConfigs;
|
||||
}
|
||||
// Legacy methods have been removed.
|
||||
// Please use updateRouteConfigs() directly with modern route-based configuration.
|
||||
|
||||
/**
|
||||
* Adds default headers to be included in all responses
|
||||
@ -650,62 +590,4 @@ export class NetworkProxy implements IMetricsTracker {
|
||||
public getRouteConfigs(): IRouteConfig[] {
|
||||
return this.routeManager.getRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use getRouteConfigs instead
|
||||
* Gets all proxy configurations currently in use in the legacy format
|
||||
* This method is maintained for backward compatibility
|
||||
*/
|
||||
public getProxyConfigs(): IReverseProxyConfig[] {
|
||||
this.logger.warn('getProxyConfigs is deprecated - use getRouteConfigs instead');
|
||||
|
||||
// Create legacy proxy configs from our route configurations
|
||||
const legacyConfigs: IReverseProxyConfig[] = [];
|
||||
const currentRoutes = this.routeManager.getRoutes();
|
||||
|
||||
for (const route of currentRoutes) {
|
||||
// Skip non-forward routes or routes without domains
|
||||
if (route.action.type !== 'forward' || !route.match.domains || !route.action.target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip routes with function-based targets
|
||||
if (typeof route.action.target.host === 'function' || typeof route.action.target.port === 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get domains
|
||||
const domains = Array.isArray(route.match.domains)
|
||||
? route.match.domains.filter(d => !d.includes('*'))
|
||||
: route.match.domains.includes('*') ? [] : [route.match.domains];
|
||||
|
||||
// Get certificate
|
||||
let privateKey = '';
|
||||
let publicKey = '';
|
||||
|
||||
if (route.action.tls?.certificate && route.action.tls.certificate !== 'auto') {
|
||||
privateKey = route.action.tls.certificate.key;
|
||||
publicKey = route.action.tls.certificate.cert;
|
||||
} else {
|
||||
const defaultCerts = this.certificateManager.getDefaultCertificates();
|
||||
privateKey = defaultCerts.key;
|
||||
publicKey = defaultCerts.cert;
|
||||
}
|
||||
|
||||
// Create legacy config for each domain
|
||||
for (const domain of domains) {
|
||||
legacyConfigs.push({
|
||||
hostName: domain,
|
||||
destinationIps: Array.isArray(route.action.target.host)
|
||||
? route.action.target.host
|
||||
: [route.action.target.host],
|
||||
destinationPorts: [route.action.target.port],
|
||||
privateKey,
|
||||
publicKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return legacyConfigs;
|
||||
}
|
||||
}
|
@ -661,150 +661,6 @@ export class RequestHandler {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Find target based on hostname
|
||||
const proxyConfig = this.router.routeReq(req);
|
||||
|
||||
if (!proxyConfig) {
|
||||
// No matching proxy configuration
|
||||
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found: No proxy configuration for this host');
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get destination IP using round-robin if multiple IPs configured
|
||||
const destination = this.connectionPool.getNextTarget(
|
||||
proxyConfig.destinationIps,
|
||||
proxyConfig.destinationPorts[0]
|
||||
);
|
||||
|
||||
// Create options for the proxy request
|
||||
const options: plugins.http.RequestOptions = {
|
||||
hostname: destination.host,
|
||||
port: destination.port,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: { ...req.headers }
|
||||
};
|
||||
|
||||
// Remove host header to avoid issues with virtual hosts on target server
|
||||
// The host header should match the target server's expected hostname
|
||||
if (options.headers && options.headers.host) {
|
||||
if ((proxyConfig as IReverseProxyConfig).rewriteHostHeader) {
|
||||
options.headers.host = `${destination.host}:${destination.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Proxying request to ${destination.host}:${destination.port}${req.url}`,
|
||||
{ method: req.method }
|
||||
);
|
||||
|
||||
// Create proxy request
|
||||
const proxyReq = plugins.http.request(options, (proxyRes) => {
|
||||
// Copy status code
|
||||
res.statusCode = proxyRes.statusCode || 500;
|
||||
|
||||
// Copy headers from proxy response to client response
|
||||
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
||||
if (value !== undefined) {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe proxy response to client response
|
||||
proxyRes.pipe(res);
|
||||
|
||||
// Increment served requests counter when the response finishes
|
||||
res.on('finish', () => {
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementRequestsServed();
|
||||
}
|
||||
|
||||
// Log the completed request
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.debug(
|
||||
`Request completed in ${duration}ms: ${req.method} ${req.url} ${res.statusCode}`,
|
||||
{ duration, statusCode: res.statusCode }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle proxy request errors
|
||||
proxyReq.on('error', (error) => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.error(
|
||||
`Proxy error for ${req.method} ${req.url}: ${error.message}`,
|
||||
{ duration, error: error.message }
|
||||
);
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
// Check if headers have already been sent
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502;
|
||||
res.end(`Bad Gateway: ${error.message}`);
|
||||
} else {
|
||||
// If headers already sent, just close the connection
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
// Pipe request body to proxy request and handle client-side errors
|
||||
req.pipe(proxyReq);
|
||||
|
||||
// Handle client disconnection
|
||||
req.on('error', (error) => {
|
||||
this.logger.debug(`Client connection error: ${error.message}`);
|
||||
proxyReq.destroy();
|
||||
|
||||
// Increment failed requests counter on client errors
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle response errors
|
||||
res.on('error', (error) => {
|
||||
this.logger.debug(`Response error: ${error.message}`);
|
||||
proxyReq.destroy();
|
||||
|
||||
// Increment failed requests counter on response errors
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Handle any unexpected errors
|
||||
this.logger.error(
|
||||
`Unexpected error handling request: ${error.message}`,
|
||||
{ error: error.stack }
|
||||
);
|
||||
|
||||
// Increment failed requests counter
|
||||
if (this.metricsTracker) {
|
||||
this.metricsTracker.incrementFailedRequests();
|
||||
}
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end('Internal Server Error');
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,7 +56,7 @@ export interface IRouteContext {
|
||||
routeId?: string; // The ID of the matched route
|
||||
|
||||
// Target information (resolved from dynamic mapping)
|
||||
targetHost?: string; // The resolved target host
|
||||
targetHost?: string | string[]; // The resolved target host(s)
|
||||
targetPort?: number; // The resolved target port
|
||||
|
||||
// Additional properties
|
||||
@ -68,8 +68,8 @@ export interface IRouteContext {
|
||||
* Target configuration for forwarding
|
||||
*/
|
||||
export interface IRouteTarget {
|
||||
host: string | string[] | ((context: any) => string | string[]); // Support static or dynamic host selection with any compatible context
|
||||
port: number | ((context: any) => number); // Support static or dynamic port mapping with any compatible context
|
||||
host: string | string[] | ((context: IRouteContext) => string | string[]); // Host or hosts with optional function for dynamic resolution
|
||||
port: number | ((context: IRouteContext) => number); // Port with optional function for dynamic mapping
|
||||
preservePort?: boolean; // Use incoming port as target port (ignored if port is a function)
|
||||
}
|
||||
|
||||
@ -108,7 +108,8 @@ export interface IRouteAuthentication {
|
||||
oauthClientId?: string;
|
||||
oauthClientSecret?: string;
|
||||
oauthRedirectUri?: string;
|
||||
[key: string]: any; // Allow additional auth-specific options
|
||||
// Specific options for different auth types
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,498 +0,0 @@
|
||||
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: {
|
||||
root: options.targetDirectory,
|
||||
index: ['index.html', 'index.htm'],
|
||||
directory: options.targetDirectory // For backward compatibility
|
||||
}
|
||||
},
|
||||
...(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']
|
||||
}
|
||||
);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
/**
|
||||
* 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';
|
@ -244,21 +244,36 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
* Match an IP against a pattern
|
||||
*/
|
||||
private matchIpPattern(pattern: string, ip: string): boolean {
|
||||
// Handle exact match
|
||||
if (pattern === ip) {
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
const normalizedPattern = pattern.startsWith('::ffff:') ? pattern.substring(7) : pattern;
|
||||
|
||||
// Handle exact match with normalized addresses
|
||||
if (pattern === ip || normalizedPattern === normalizedIp ||
|
||||
pattern === normalizedIp || normalizedPattern === ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle CIDR notation (e.g., 192.168.1.0/24)
|
||||
if (pattern.includes('/')) {
|
||||
return this.matchIpCidr(pattern, ip);
|
||||
return this.matchIpCidr(pattern, normalizedIp) ||
|
||||
(normalizedPattern !== pattern && this.matchIpCidr(normalizedPattern, normalizedIp));
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (regex.test(ip) || regex.test(normalizedIp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If pattern was normalized, also test with normalized pattern
|
||||
if (normalizedPattern !== pattern) {
|
||||
const normalizedRegexPattern = normalizedPattern.replace(/\./g, '\\.').replace(/\*/g, '.*');
|
||||
const normalizedRegex = new RegExp(`^${normalizedRegexPattern}$`);
|
||||
return normalizedRegex.test(ip) || normalizedRegex.test(normalizedIp);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -274,9 +289,13 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
const [subnet, bits] = cidr.split('/');
|
||||
const mask = parseInt(bits, 10);
|
||||
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
const normalizedSubnet = subnet.startsWith('::ffff:') ? subnet.substring(7) : subnet;
|
||||
|
||||
// Convert IP addresses to numeric values
|
||||
const ipNum = this.ipToNumber(ip);
|
||||
const subnetNum = this.ipToNumber(subnet);
|
||||
const ipNum = this.ipToNumber(normalizedIp);
|
||||
const subnetNum = this.ipToNumber(normalizedSubnet);
|
||||
|
||||
// Calculate subnet mask
|
||||
const maskNum = ~(2 ** (32 - mask) - 1);
|
||||
@ -293,7 +312,10 @@ export class RouteManager extends plugins.EventEmitter {
|
||||
* Convert an IP address to a numeric value
|
||||
*/
|
||||
private ipToNumber(ip: string): number {
|
||||
const parts = ip.split('.').map(part => parseInt(part, 10));
|
||||
// Normalize IPv6-mapped IPv4 addresses
|
||||
const normalizedIp = ip.startsWith('::ffff:') ? ip.substring(7) : ip;
|
||||
|
||||
const parts = normalizedIp.split('.').map(part => parseInt(part, 10));
|
||||
return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user