feat(smart-proxy): add typed Rust config serialization and regex header contract coverage
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '27.6.0',
|
||||
version: '27.7.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.'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { IProtocolCacheEntry, IProtocolDistribution } from './metrics-types.js';
|
||||
import type { IAcmeOptions, ISmartProxyOptions } from './interfaces.js';
|
||||
import type {
|
||||
IRouteAction,
|
||||
IRouteConfig,
|
||||
IRouteMatch,
|
||||
IRouteTarget,
|
||||
ITargetMatch,
|
||||
IRouteUdp,
|
||||
} from './route-types.js';
|
||||
|
||||
export type TRustHeaderMatchers = Record<string, string>;
|
||||
|
||||
export interface IRustRouteMatch extends Omit<IRouteMatch, 'headers'> {
|
||||
headers?: TRustHeaderMatchers;
|
||||
}
|
||||
|
||||
export interface IRustTargetMatch extends Omit<ITargetMatch, 'headers'> {
|
||||
headers?: TRustHeaderMatchers;
|
||||
}
|
||||
|
||||
export interface IRustRouteTarget extends Omit<IRouteTarget, 'host' | 'port' | 'match'> {
|
||||
host: string | string[];
|
||||
port: number | 'preserve';
|
||||
match?: IRustTargetMatch;
|
||||
}
|
||||
|
||||
export interface IRustRouteUdp extends Omit<IRouteUdp, 'maxSessionsPerIP'> {
|
||||
maxSessionsPerIp?: number;
|
||||
}
|
||||
|
||||
export interface IRustDefaultConfig extends Omit<NonNullable<ISmartProxyOptions['defaults']>, 'preserveSourceIP'> {
|
||||
preserveSourceIp?: boolean;
|
||||
}
|
||||
|
||||
export interface IRustRouteAction
|
||||
extends Omit<IRouteAction, 'targets' | 'socketHandler' | 'datagramHandler' | 'forwardingEngine' | 'nftables' | 'udp'> {
|
||||
targets?: IRustRouteTarget[];
|
||||
udp?: IRustRouteUdp;
|
||||
}
|
||||
|
||||
export interface IRustRouteConfig extends Omit<IRouteConfig, 'match' | 'action'> {
|
||||
match: IRustRouteMatch;
|
||||
action: IRustRouteAction;
|
||||
}
|
||||
|
||||
export interface IRustAcmeOptions extends Omit<IAcmeOptions, 'routeForwards'> {}
|
||||
|
||||
export interface IRustProxyOptions {
|
||||
routes: IRustRouteConfig[];
|
||||
preserveSourceIp?: boolean;
|
||||
proxyIps?: string[];
|
||||
acceptProxyProtocol?: boolean;
|
||||
sendProxyProtocol?: boolean;
|
||||
defaults?: IRustDefaultConfig;
|
||||
connectionTimeout?: number;
|
||||
initialDataTimeout?: number;
|
||||
socketTimeout?: number;
|
||||
inactivityCheckInterval?: number;
|
||||
maxConnectionLifetime?: number;
|
||||
inactivityTimeout?: number;
|
||||
gracefulShutdownTimeout?: number;
|
||||
noDelay?: boolean;
|
||||
keepAlive?: boolean;
|
||||
keepAliveInitialDelay?: number;
|
||||
maxPendingDataSize?: number;
|
||||
disableInactivityCheck?: boolean;
|
||||
enableKeepAliveProbes?: boolean;
|
||||
enableDetailedLogging?: boolean;
|
||||
enableTlsDebugLogging?: boolean;
|
||||
enableRandomizedTimeouts?: boolean;
|
||||
maxConnectionsPerIp?: number;
|
||||
connectionRateLimitPerMinute?: number;
|
||||
keepAliveTreatment?: ISmartProxyOptions['keepAliveTreatment'];
|
||||
keepAliveInactivityMultiplier?: number;
|
||||
extendedKeepAliveLifetime?: number;
|
||||
metrics?: ISmartProxyOptions['metrics'];
|
||||
acme?: IRustAcmeOptions;
|
||||
}
|
||||
|
||||
export interface IRustStatistics {
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
routesCount: number;
|
||||
listeningPorts: number[];
|
||||
uptimeSeconds: number;
|
||||
}
|
||||
|
||||
export interface IRustCertificateStatus {
|
||||
domain: string;
|
||||
source: string;
|
||||
expiresAt: number;
|
||||
isValid: boolean;
|
||||
}
|
||||
|
||||
export interface IRustThroughputSample {
|
||||
timestampMs: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
}
|
||||
|
||||
export interface IRustRouteMetrics {
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
throughputInBytesPerSec: number;
|
||||
throughputOutBytesPerSec: number;
|
||||
throughputRecentInBytesPerSec: number;
|
||||
throughputRecentOutBytesPerSec: number;
|
||||
}
|
||||
|
||||
export interface IRustIpMetrics {
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
throughputInBytesPerSec: number;
|
||||
throughputOutBytesPerSec: number;
|
||||
domainRequests: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface IRustBackendMetrics {
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
protocol: string;
|
||||
connectErrors: number;
|
||||
handshakeErrors: number;
|
||||
requestErrors: number;
|
||||
totalConnectTimeUs: number;
|
||||
connectCount: number;
|
||||
poolHits: number;
|
||||
poolMisses: number;
|
||||
h2Failures: number;
|
||||
}
|
||||
|
||||
export interface IRustMetricsSnapshot {
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
bytesIn: number;
|
||||
bytesOut: number;
|
||||
throughputInBytesPerSec: number;
|
||||
throughputOutBytesPerSec: number;
|
||||
throughputRecentInBytesPerSec: number;
|
||||
throughputRecentOutBytesPerSec: number;
|
||||
routes: Record<string, IRustRouteMetrics>;
|
||||
ips: Record<string, IRustIpMetrics>;
|
||||
backends: Record<string, IRustBackendMetrics>;
|
||||
throughputHistory: IRustThroughputSample[];
|
||||
totalHttpRequests: number;
|
||||
httpRequestsPerSec: number;
|
||||
httpRequestsPerSecRecent: number;
|
||||
activeUdpSessions: number;
|
||||
totalUdpSessions: number;
|
||||
totalDatagramsIn: number;
|
||||
totalDatagramsOut: number;
|
||||
detectedProtocols: IProtocolCacheEntry[];
|
||||
frontendProtocols: IProtocolDistribution;
|
||||
backendProtocols: IProtocolDistribution;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IRouteConfig, IRouteAction, IRouteTarget } from './models/route-types.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import type { IRustRouteConfig } from './models/rust-types.js';
|
||||
import { serializeRouteForRust } from './utils/rust-config.js';
|
||||
|
||||
/**
|
||||
* Preprocesses routes before sending them to Rust.
|
||||
@@ -24,7 +25,7 @@ export class RoutePreprocessor {
|
||||
* - Non-serializable fields are stripped
|
||||
* - Original routes are preserved in the local map for handler lookup
|
||||
*/
|
||||
public preprocessForRust(routes: IRouteConfig[]): IRouteConfig[] {
|
||||
public preprocessForRust(routes: IRouteConfig[]): IRustRouteConfig[] {
|
||||
this.originalRoutes.clear();
|
||||
return routes.map((route, index) => this.preprocessRoute(route, index));
|
||||
}
|
||||
@@ -43,7 +44,7 @@ export class RoutePreprocessor {
|
||||
return new Map(this.originalRoutes);
|
||||
}
|
||||
|
||||
private preprocessRoute(route: IRouteConfig, index: number): IRouteConfig {
|
||||
private preprocessRoute(route: IRouteConfig, index: number): IRustRouteConfig {
|
||||
const routeKey = route.name || route.id || `route_${index}`;
|
||||
|
||||
// Check if this route needs TS-side handling
|
||||
@@ -57,7 +58,7 @@ export class RoutePreprocessor {
|
||||
// Create a clean copy for Rust
|
||||
const cleanRoute: IRouteConfig = {
|
||||
...route,
|
||||
action: this.cleanAction(route.action, routeKey, needsTsHandling),
|
||||
action: this.cleanAction(route.action, needsTsHandling),
|
||||
};
|
||||
|
||||
// Ensure we have a name for handler lookup
|
||||
@@ -65,7 +66,7 @@ export class RoutePreprocessor {
|
||||
cleanRoute.name = routeKey;
|
||||
}
|
||||
|
||||
return cleanRoute;
|
||||
return serializeRouteForRust(cleanRoute);
|
||||
}
|
||||
|
||||
private routeNeedsTsHandling(route: IRouteConfig): boolean {
|
||||
@@ -91,15 +92,16 @@ export class RoutePreprocessor {
|
||||
return false;
|
||||
}
|
||||
|
||||
private cleanAction(action: IRouteAction, routeKey: string, needsTsHandling: boolean): IRouteAction {
|
||||
const cleanAction: IRouteAction = { ...action };
|
||||
private cleanAction(action: IRouteAction, needsTsHandling: boolean): IRouteAction {
|
||||
let cleanAction: IRouteAction = { ...action };
|
||||
|
||||
if (needsTsHandling) {
|
||||
// Convert to socket-handler type for Rust (Rust will relay back to TS)
|
||||
cleanAction.type = 'socket-handler';
|
||||
// Remove the JS handlers (not serializable)
|
||||
delete (cleanAction as any).socketHandler;
|
||||
delete (cleanAction as any).datagramHandler;
|
||||
const { socketHandler: _socketHandler, datagramHandler: _datagramHandler, ...serializableAction } = cleanAction;
|
||||
cleanAction = {
|
||||
...serializableAction,
|
||||
type: 'socket-handler',
|
||||
};
|
||||
}
|
||||
|
||||
// Clean targets - replace functions with static values
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { IMetrics, IBackendMetrics, IProtocolCacheEntry, IProtocolDistribution, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
|
||||
import type { RustProxyBridge } from './rust-proxy-bridge.js';
|
||||
import type { IRustBackendMetrics, IRustIpMetrics, IRustMetricsSnapshot, IRustRouteMetrics } from './models/rust-types.js';
|
||||
|
||||
/**
|
||||
* Adapts Rust JSON metrics to the IMetrics interface.
|
||||
@@ -14,7 +15,7 @@ import type { RustProxyBridge } from './rust-proxy-bridge.js';
|
||||
*/
|
||||
export class RustMetricsAdapter implements IMetrics {
|
||||
private bridge: RustProxyBridge;
|
||||
private cache: any = null;
|
||||
private cache: IRustMetricsSnapshot | null = null;
|
||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private pollIntervalMs: number;
|
||||
|
||||
@@ -65,8 +66,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
byRoute: (): Map<string, number> => {
|
||||
const result = new Map<string, number>();
|
||||
if (this.cache?.routes) {
|
||||
for (const [name, rm] of Object.entries(this.cache.routes)) {
|
||||
result.set(name, (rm as any).activeConnections ?? 0);
|
||||
for (const [name, rm] of Object.entries(this.cache.routes) as Array<[string, IRustRouteMetrics]>) {
|
||||
result.set(name, rm.activeConnections ?? 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -74,8 +75,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
byIP: (): Map<string, number> => {
|
||||
const result = new Map<string, number>();
|
||||
if (this.cache?.ips) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips)) {
|
||||
result.set(ip, (im as any).activeConnections ?? 0);
|
||||
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
|
||||
result.set(ip, im.activeConnections ?? 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -83,8 +84,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => {
|
||||
const result: Array<{ ip: string; count: number }> = [];
|
||||
if (this.cache?.ips) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips)) {
|
||||
result.push({ ip, count: (im as any).activeConnections ?? 0 });
|
||||
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
|
||||
result.push({ ip, count: im.activeConnections ?? 0 });
|
||||
}
|
||||
}
|
||||
result.sort((a, b) => b.count - a.count);
|
||||
@@ -93,8 +94,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
domainRequestsByIP: (): Map<string, Map<string, number>> => {
|
||||
const result = new Map<string, Map<string, number>>();
|
||||
if (this.cache?.ips) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips)) {
|
||||
const dr = (im as any).domainRequests;
|
||||
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
|
||||
const dr = im.domainRequests;
|
||||
if (dr && typeof dr === 'object') {
|
||||
const domainMap = new Map<string, number>();
|
||||
for (const [domain, count] of Object.entries(dr)) {
|
||||
@@ -111,8 +112,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
topDomainRequests: (limit: number = 20): Array<{ ip: string; domain: string; count: number }> => {
|
||||
const result: Array<{ ip: string; domain: string; count: number }> = [];
|
||||
if (this.cache?.ips) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips)) {
|
||||
const dr = (im as any).domainRequests;
|
||||
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
|
||||
const dr = im.domainRequests;
|
||||
if (dr && typeof dr === 'object') {
|
||||
for (const [domain, count] of Object.entries(dr)) {
|
||||
result.push({ ip, domain, count: count as number });
|
||||
@@ -176,7 +177,7 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
},
|
||||
history: (seconds: number): Array<IThroughputHistoryPoint> => {
|
||||
if (!this.cache?.throughputHistory) return [];
|
||||
return this.cache.throughputHistory.slice(-seconds).map((p: any) => ({
|
||||
return this.cache.throughputHistory.slice(-seconds).map((p) => ({
|
||||
timestamp: p.timestampMs,
|
||||
in: p.bytesIn,
|
||||
out: p.bytesOut,
|
||||
@@ -185,10 +186,10 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
byRoute: (_windowSeconds?: number): Map<string, IThroughputData> => {
|
||||
const result = new Map<string, IThroughputData>();
|
||||
if (this.cache?.routes) {
|
||||
for (const [name, rm] of Object.entries(this.cache.routes)) {
|
||||
for (const [name, rm] of Object.entries(this.cache.routes) as Array<[string, IRustRouteMetrics]>) {
|
||||
result.set(name, {
|
||||
in: (rm as any).throughputInBytesPerSec ?? 0,
|
||||
out: (rm as any).throughputOutBytesPerSec ?? 0,
|
||||
in: rm.throughputInBytesPerSec ?? 0,
|
||||
out: rm.throughputOutBytesPerSec ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -197,10 +198,10 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
byIP: (_windowSeconds?: number): Map<string, IThroughputData> => {
|
||||
const result = new Map<string, IThroughputData>();
|
||||
if (this.cache?.ips) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips)) {
|
||||
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
|
||||
result.set(ip, {
|
||||
in: (im as any).throughputInBytesPerSec ?? 0,
|
||||
out: (im as any).throughputOutBytesPerSec ?? 0,
|
||||
in: im.throughputInBytesPerSec ?? 0,
|
||||
out: im.throughputOutBytesPerSec ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -236,23 +237,22 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
byBackend: (): Map<string, IBackendMetrics> => {
|
||||
const result = new Map<string, IBackendMetrics>();
|
||||
if (this.cache?.backends) {
|
||||
for (const [key, bm] of Object.entries(this.cache.backends)) {
|
||||
const m = bm as any;
|
||||
const totalTimeUs = m.totalConnectTimeUs ?? 0;
|
||||
const count = m.connectCount ?? 0;
|
||||
const poolHits = m.poolHits ?? 0;
|
||||
const poolMisses = m.poolMisses ?? 0;
|
||||
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
|
||||
const totalTimeUs = bm.totalConnectTimeUs ?? 0;
|
||||
const count = bm.connectCount ?? 0;
|
||||
const poolHits = bm.poolHits ?? 0;
|
||||
const poolMisses = bm.poolMisses ?? 0;
|
||||
const poolTotal = poolHits + poolMisses;
|
||||
result.set(key, {
|
||||
protocol: m.protocol ?? 'unknown',
|
||||
activeConnections: m.activeConnections ?? 0,
|
||||
totalConnections: m.totalConnections ?? 0,
|
||||
connectErrors: m.connectErrors ?? 0,
|
||||
handshakeErrors: m.handshakeErrors ?? 0,
|
||||
requestErrors: m.requestErrors ?? 0,
|
||||
protocol: bm.protocol ?? 'unknown',
|
||||
activeConnections: bm.activeConnections ?? 0,
|
||||
totalConnections: bm.totalConnections ?? 0,
|
||||
connectErrors: bm.connectErrors ?? 0,
|
||||
handshakeErrors: bm.handshakeErrors ?? 0,
|
||||
requestErrors: bm.requestErrors ?? 0,
|
||||
avgConnectTimeMs: count > 0 ? (totalTimeUs / count) / 1000 : 0,
|
||||
poolHitRate: poolTotal > 0 ? poolHits / poolTotal : 0,
|
||||
h2Failures: m.h2Failures ?? 0,
|
||||
h2Failures: bm.h2Failures ?? 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -261,8 +261,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
protocols: (): Map<string, string> => {
|
||||
const result = new Map<string, string>();
|
||||
if (this.cache?.backends) {
|
||||
for (const [key, bm] of Object.entries(this.cache.backends)) {
|
||||
result.set(key, (bm as any).protocol ?? 'unknown');
|
||||
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
|
||||
result.set(key, bm.protocol ?? 'unknown');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -270,9 +270,8 @@ export class RustMetricsAdapter implements IMetrics {
|
||||
topByErrors: (limit: number = 10): Array<{ backend: string; errors: number }> => {
|
||||
const result: Array<{ backend: string; errors: number }> = [];
|
||||
if (this.cache?.backends) {
|
||||
for (const [key, bm] of Object.entries(this.cache.backends)) {
|
||||
const m = bm as any;
|
||||
const errors = (m.connectErrors ?? 0) + (m.handshakeErrors ?? 0) + (m.requestErrors ?? 0);
|
||||
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
|
||||
const errors = (bm.connectErrors ?? 0) + (bm.handshakeErrors ?? 0) + (bm.requestErrors ?? 0);
|
||||
if (errors > 0) result.push({ backend: key, errors });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
import type {
|
||||
IRustCertificateStatus,
|
||||
IRustMetricsSnapshot,
|
||||
IRustProxyOptions,
|
||||
IRustRouteConfig,
|
||||
IRustStatistics,
|
||||
} from './models/rust-types.js';
|
||||
|
||||
/**
|
||||
* Type-safe command definitions for the Rust proxy IPC protocol.
|
||||
*/
|
||||
type TSmartProxyCommands = {
|
||||
start: { params: { config: any }; result: void };
|
||||
stop: { params: Record<string, never>; result: void };
|
||||
updateRoutes: { params: { routes: IRouteConfig[] }; result: void };
|
||||
getMetrics: { params: Record<string, never>; result: any };
|
||||
getStatistics: { params: Record<string, never>; result: any };
|
||||
provisionCertificate: { params: { routeName: string }; result: void };
|
||||
renewCertificate: { params: { routeName: string }; result: void };
|
||||
getCertificateStatus: { params: { routeName: string }; result: any };
|
||||
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
||||
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
||||
addListeningPort: { params: { port: number }; result: void };
|
||||
removeListeningPort: { params: { port: number }; result: void };
|
||||
start: { params: { config: IRustProxyOptions }; result: void };
|
||||
stop: { params: Record<string, never>; result: void };
|
||||
updateRoutes: { params: { routes: IRustRouteConfig[] }; result: void };
|
||||
getMetrics: { params: Record<string, never>; result: IRustMetricsSnapshot };
|
||||
getStatistics: { params: Record<string, never>; result: IRustStatistics };
|
||||
provisionCertificate: { params: { routeName: string }; result: void };
|
||||
renewCertificate: { params: { routeName: string }; result: void };
|
||||
getCertificateStatus: { params: { routeName: string }; result: IRustCertificateStatus | null };
|
||||
getListeningPorts: { params: Record<string, never>; result: { ports: number[] } };
|
||||
setSocketHandlerRelay: { params: { socketPath: string }; result: void };
|
||||
addListeningPort: { params: { port: number }; result: void };
|
||||
removeListeningPort: { params: { port: number }; result: void };
|
||||
loadCertificate: { params: { domain: string; cert: string; key: string; ca?: string }; result: void };
|
||||
setDatagramHandlerRelay: { params: { socketPath: string }; result: void };
|
||||
};
|
||||
@@ -121,7 +127,7 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
||||
|
||||
// --- Convenience methods for each management command ---
|
||||
|
||||
public async startProxy(config: any): Promise<void> {
|
||||
public async startProxy(config: IRustProxyOptions): Promise<void> {
|
||||
await this.bridge.sendCommand('start', { config });
|
||||
}
|
||||
|
||||
@@ -129,15 +135,15 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
||||
await this.bridge.sendCommand('stop', {} as Record<string, never>);
|
||||
}
|
||||
|
||||
public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
|
||||
public async updateRoutes(routes: IRustRouteConfig[]): Promise<void> {
|
||||
await this.bridge.sendCommand('updateRoutes', { routes });
|
||||
}
|
||||
|
||||
public async getMetrics(): Promise<any> {
|
||||
public async getMetrics(): Promise<IRustMetricsSnapshot> {
|
||||
return this.bridge.sendCommand('getMetrics', {} as Record<string, never>);
|
||||
}
|
||||
|
||||
public async getStatistics(): Promise<any> {
|
||||
public async getStatistics(): Promise<IRustStatistics> {
|
||||
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
|
||||
}
|
||||
|
||||
@@ -149,7 +155,7 @@ export class RustProxyBridge extends plugins.EventEmitter {
|
||||
await this.bridge.sendCommand('renewCertificate', { routeName });
|
||||
}
|
||||
|
||||
public async getCertificateStatus(routeName: string): Promise<any> {
|
||||
public async getCertificateStatus(routeName: string): Promise<IRustCertificateStatus | null> {
|
||||
return this.bridge.sendCommand('getCertificateStatus', { routeName });
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RustMetricsAdapter } from './rust-metrics-adapter.js';
|
||||
// Route management
|
||||
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
|
||||
import { RouteValidator } from './utils/route-validator.js';
|
||||
import { buildRustProxyOptions } from './utils/rust-config.js';
|
||||
import { generateDefaultCertificate } from './utils/default-cert-generator.js';
|
||||
import { Mutex } from './utils/mutex.js';
|
||||
import { ConcurrencySemaphore } from './utils/concurrency-semaphore.js';
|
||||
@@ -19,6 +20,7 @@ import { ConcurrencySemaphore } from './utils/concurrency-semaphore.js';
|
||||
import type { ISmartProxyOptions, TSmartProxyCertProvisionObject, IAcmeOptions, ICertProvisionEventComms, ICertificateIssuedEvent, ICertificateFailedEvent } from './models/interfaces.js';
|
||||
import type { IRouteConfig } from './models/route-types.js';
|
||||
import type { IMetrics } from './models/metrics-types.js';
|
||||
import type { IRustCertificateStatus, IRustProxyOptions, IRustStatistics } from './models/rust-types.js';
|
||||
|
||||
/**
|
||||
* SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
|
||||
@@ -365,7 +367,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* Get certificate status for a route (async - calls Rust).
|
||||
*/
|
||||
public async getCertificateStatus(routeName: string): Promise<any> {
|
||||
public async getCertificateStatus(routeName: string): Promise<IRustCertificateStatus | null> {
|
||||
return this.bridge.getCertificateStatus(routeName);
|
||||
}
|
||||
|
||||
@@ -379,7 +381,7 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* Get statistics (async - calls Rust).
|
||||
*/
|
||||
public async getStatistics(): Promise<any> {
|
||||
public async getStatistics(): Promise<IRustStatistics> {
|
||||
return this.bridge.getStatistics();
|
||||
}
|
||||
|
||||
@@ -484,37 +486,8 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
/**
|
||||
* Build the Rust configuration object from TS settings.
|
||||
*/
|
||||
private buildRustConfig(routes: IRouteConfig[], acmeOverride?: IAcmeOptions): any {
|
||||
const acme = acmeOverride !== undefined ? acmeOverride : this.settings.acme;
|
||||
return {
|
||||
routes,
|
||||
defaults: this.settings.defaults,
|
||||
acme: acme
|
||||
? {
|
||||
enabled: acme.enabled,
|
||||
email: acme.email,
|
||||
useProduction: acme.useProduction,
|
||||
port: acme.port,
|
||||
renewThresholdDays: acme.renewThresholdDays,
|
||||
autoRenew: acme.autoRenew,
|
||||
renewCheckIntervalHours: acme.renewCheckIntervalHours,
|
||||
}
|
||||
: undefined,
|
||||
connectionTimeout: this.settings.connectionTimeout,
|
||||
initialDataTimeout: this.settings.initialDataTimeout,
|
||||
socketTimeout: this.settings.socketTimeout,
|
||||
maxConnectionLifetime: this.settings.maxConnectionLifetime,
|
||||
gracefulShutdownTimeout: this.settings.gracefulShutdownTimeout,
|
||||
maxConnectionsPerIp: this.settings.maxConnectionsPerIP,
|
||||
connectionRateLimitPerMinute: this.settings.connectionRateLimitPerMinute,
|
||||
keepAliveTreatment: this.settings.keepAliveTreatment,
|
||||
keepAliveInactivityMultiplier: this.settings.keepAliveInactivityMultiplier,
|
||||
extendedKeepAliveLifetime: this.settings.extendedKeepAliveLifetime,
|
||||
proxyIps: this.settings.proxyIPs,
|
||||
acceptProxyProtocol: this.settings.acceptProxyProtocol,
|
||||
sendProxyProtocol: this.settings.sendProxyProtocol,
|
||||
metrics: this.settings.metrics,
|
||||
};
|
||||
private buildRustConfig(routes: IRustProxyOptions['routes'], acmeOverride?: IAcmeOptions): IRustProxyOptions {
|
||||
return buildRustProxyOptions(this.settings, routes, acmeOverride);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,14 +168,28 @@ export function routeMatchesHeaders(
|
||||
if (!route.match?.headers || Object.keys(route.match.headers).length === 0) {
|
||||
return true; // No headers specified means it matches any headers
|
||||
}
|
||||
|
||||
// Convert RegExp patterns to strings for HeaderMatcher
|
||||
const stringHeaders: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(route.match.headers)) {
|
||||
stringHeaders[key] = value instanceof RegExp ? value.source : value;
|
||||
|
||||
for (const [headerName, expectedValue] of Object.entries(route.match.headers)) {
|
||||
const actualKey = Object.keys(headers).find((key) => key.toLowerCase() === headerName.toLowerCase());
|
||||
const actualValue = actualKey ? headers[actualKey] : undefined;
|
||||
|
||||
if (actualValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expectedValue instanceof RegExp) {
|
||||
if (!expectedValue.test(actualValue)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!HeaderMatcher.match(expectedValue, actualValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return HeaderMatcher.matchAll(stringHeaders, headers);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,4 +297,4 @@ export function generateRouteId(route: IRouteConfig): string {
|
||||
*/
|
||||
export function cloneRoute(route: IRouteConfig): IRouteConfig {
|
||||
return JSON.parse(JSON.stringify(route));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import type { IAcmeOptions, ISmartProxyOptions } from '../models/interfaces.js';
|
||||
import type { IRouteAction, IRouteConfig, IRouteMatch, IRouteTarget, ITargetMatch } from '../models/route-types.js';
|
||||
import type {
|
||||
IRustAcmeOptions,
|
||||
IRustDefaultConfig,
|
||||
IRustProxyOptions,
|
||||
IRustRouteAction,
|
||||
IRustRouteConfig,
|
||||
IRustRouteMatch,
|
||||
IRustRouteTarget,
|
||||
IRustTargetMatch,
|
||||
IRustRouteUdp,
|
||||
TRustHeaderMatchers,
|
||||
} from '../models/rust-types.js';
|
||||
|
||||
const SUPPORTED_REGEX_FLAGS = new Set(['i', 'm', 's', 'u', 'g']);
|
||||
|
||||
export function serializeHeaderMatchValue(value: string | RegExp): string {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const unsupportedFlags = Array.from(new Set(value.flags)).filter((flag) => !SUPPORTED_REGEX_FLAGS.has(flag));
|
||||
if (unsupportedFlags.length > 0) {
|
||||
throw new Error(
|
||||
`Header RegExp uses unsupported flags for Rust serialization: ${unsupportedFlags.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return `/${value.source}/${value.flags}`;
|
||||
}
|
||||
|
||||
export function serializeHeaderMatchers(headers?: Record<string, string | RegExp>): TRustHeaderMatchers | undefined {
|
||||
if (!headers) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(headers).map(([key, value]) => [key, serializeHeaderMatchValue(value)])
|
||||
);
|
||||
}
|
||||
|
||||
export function serializeTargetMatchForRust(match?: ITargetMatch): IRustTargetMatch | undefined {
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...match,
|
||||
headers: serializeHeaderMatchers(match.headers),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeRouteMatchForRust(match: IRouteMatch): IRustRouteMatch {
|
||||
return {
|
||||
...match,
|
||||
headers: serializeHeaderMatchers(match.headers),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeRouteTargetForRust(target: IRouteTarget): IRustRouteTarget {
|
||||
if (typeof target.host !== 'string' && !Array.isArray(target.host)) {
|
||||
throw new Error('Route target host must be serialized before sending to Rust');
|
||||
}
|
||||
|
||||
if (typeof target.port !== 'number' && target.port !== 'preserve') {
|
||||
throw new Error('Route target port must be serialized before sending to Rust');
|
||||
}
|
||||
|
||||
return {
|
||||
...target,
|
||||
host: target.host,
|
||||
port: target.port,
|
||||
match: serializeTargetMatchForRust(target.match),
|
||||
};
|
||||
}
|
||||
|
||||
function serializeUdpForRust(udp?: IRouteAction['udp']): IRustRouteUdp | undefined {
|
||||
if (!udp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { maxSessionsPerIP, ...rest } = udp;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
maxSessionsPerIp: maxSessionsPerIP,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeRouteActionForRust(action: IRouteAction): IRustRouteAction {
|
||||
const {
|
||||
socketHandler: _socketHandler,
|
||||
datagramHandler: _datagramHandler,
|
||||
forwardingEngine: _forwardingEngine,
|
||||
nftables: _nftables,
|
||||
targets,
|
||||
udp,
|
||||
...rest
|
||||
} = action;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
targets: targets?.map((target) => serializeRouteTargetForRust(target)),
|
||||
udp: serializeUdpForRust(udp),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeRouteForRust(route: IRouteConfig): IRustRouteConfig {
|
||||
return {
|
||||
...route,
|
||||
match: serializeRouteMatchForRust(route.match),
|
||||
action: serializeRouteActionForRust(route.action),
|
||||
};
|
||||
}
|
||||
|
||||
function serializeAcmeForRust(acme?: IAcmeOptions): IRustAcmeOptions | undefined {
|
||||
if (!acme) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: acme.enabled,
|
||||
email: acme.email,
|
||||
environment: acme.environment,
|
||||
accountEmail: acme.accountEmail,
|
||||
port: acme.port,
|
||||
useProduction: acme.useProduction,
|
||||
renewThresholdDays: acme.renewThresholdDays,
|
||||
autoRenew: acme.autoRenew,
|
||||
skipConfiguredCerts: acme.skipConfiguredCerts,
|
||||
renewCheckIntervalHours: acme.renewCheckIntervalHours,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeDefaultsForRust(defaults?: ISmartProxyOptions['defaults']): IRustDefaultConfig | undefined {
|
||||
if (!defaults) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { preserveSourceIP, ...rest } = defaults;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
preserveSourceIp: preserveSourceIP,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRustProxyOptions(
|
||||
settings: ISmartProxyOptions,
|
||||
routes: IRustRouteConfig[],
|
||||
acmeOverride?: IAcmeOptions,
|
||||
): IRustProxyOptions {
|
||||
const acme = acmeOverride !== undefined ? acmeOverride : settings.acme;
|
||||
|
||||
return {
|
||||
routes,
|
||||
preserveSourceIp: settings.preserveSourceIP,
|
||||
proxyIps: settings.proxyIPs,
|
||||
acceptProxyProtocol: settings.acceptProxyProtocol,
|
||||
sendProxyProtocol: settings.sendProxyProtocol,
|
||||
defaults: serializeDefaultsForRust(settings.defaults),
|
||||
connectionTimeout: settings.connectionTimeout,
|
||||
initialDataTimeout: settings.initialDataTimeout,
|
||||
socketTimeout: settings.socketTimeout,
|
||||
inactivityCheckInterval: settings.inactivityCheckInterval,
|
||||
maxConnectionLifetime: settings.maxConnectionLifetime,
|
||||
inactivityTimeout: settings.inactivityTimeout,
|
||||
gracefulShutdownTimeout: settings.gracefulShutdownTimeout,
|
||||
noDelay: settings.noDelay,
|
||||
keepAlive: settings.keepAlive,
|
||||
keepAliveInitialDelay: settings.keepAliveInitialDelay,
|
||||
maxPendingDataSize: settings.maxPendingDataSize,
|
||||
disableInactivityCheck: settings.disableInactivityCheck,
|
||||
enableKeepAliveProbes: settings.enableKeepAliveProbes,
|
||||
enableDetailedLogging: settings.enableDetailedLogging,
|
||||
enableTlsDebugLogging: settings.enableTlsDebugLogging,
|
||||
enableRandomizedTimeouts: settings.enableRandomizedTimeouts,
|
||||
maxConnectionsPerIp: settings.maxConnectionsPerIP,
|
||||
connectionRateLimitPerMinute: settings.connectionRateLimitPerMinute,
|
||||
keepAliveTreatment: settings.keepAliveTreatment,
|
||||
keepAliveInactivityMultiplier: settings.keepAliveInactivityMultiplier,
|
||||
extendedKeepAliveLifetime: settings.extendedKeepAliveLifetime,
|
||||
metrics: settings.metrics,
|
||||
acme: serializeAcmeForRust(acme),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user