181 lines
6.4 KiB
TypeScript
181 lines
6.4 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
||
import { logger } from '../../core/utils/logger.js';
|
||
import type { IRouteConfig } from './models/route-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[] } };
|
||
getNftablesStatus: { params: Record<string, never>; result: any };
|
||
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 };
|
||
};
|
||
|
||
/**
|
||
* Get the package root directory using import.meta.url.
|
||
* This file is at ts/proxies/smart-proxy/, so package root is 3 levels up.
|
||
*/
|
||
function getPackageRoot(): string {
|
||
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
||
return plugins.path.resolve(thisDir, '..', '..', '..');
|
||
}
|
||
|
||
/**
|
||
* Map Node.js process.platform/process.arch to tsrust's friendly name suffix.
|
||
* tsrust names cross-compiled binaries as: rustproxy_linux_amd64, rustproxy_linux_arm64, etc.
|
||
*/
|
||
function getTsrustPlatformSuffix(): string | null {
|
||
const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' };
|
||
const osMap: Record<string, string> = { linux: 'linux', darwin: 'macos' };
|
||
const os = osMap[process.platform];
|
||
const arch = archMap[process.arch];
|
||
if (os && arch) {
|
||
return `${os}_${arch}`;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Build local search paths for the Rust binary, including dist_rust/ candidates
|
||
* (built by tsrust) and local development build paths.
|
||
*/
|
||
function buildLocalPaths(): string[] {
|
||
const packageRoot = getPackageRoot();
|
||
const suffix = getTsrustPlatformSuffix();
|
||
const paths: string[] = [];
|
||
|
||
// dist_rust/ candidates (tsrust cross-compiled output)
|
||
if (suffix) {
|
||
paths.push(plugins.path.join(packageRoot, 'dist_rust', `rustproxy_${suffix}`));
|
||
}
|
||
paths.push(plugins.path.join(packageRoot, 'dist_rust', 'rustproxy'));
|
||
|
||
// Local dev build paths
|
||
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', 'rustproxy'));
|
||
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', 'rustproxy'));
|
||
|
||
return paths;
|
||
}
|
||
|
||
/**
|
||
* Bridge between TypeScript SmartProxy and the Rust binary.
|
||
* Wraps @push.rocks/smartrust's RustBridge with type-safe command definitions.
|
||
*/
|
||
export class RustProxyBridge extends plugins.EventEmitter {
|
||
private bridge: plugins.smartrust.RustBridge<TSmartProxyCommands>;
|
||
|
||
constructor() {
|
||
super();
|
||
|
||
this.bridge = new plugins.smartrust.RustBridge<TSmartProxyCommands>({
|
||
binaryName: 'rustproxy',
|
||
envVarName: 'SMARTPROXY_RUST_BINARY',
|
||
platformPackagePrefix: '@push.rocks/smartproxy',
|
||
localPaths: buildLocalPaths(),
|
||
maxPayloadSize: 100 * 1024 * 1024, // 100 MB – route configs with many entries can be large
|
||
logger: {
|
||
log: (level: string, message: string, data?: Record<string, any>) => {
|
||
logger.log(level as any, message, data);
|
||
},
|
||
},
|
||
});
|
||
|
||
// Forward events from the inner bridge
|
||
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
||
this.emit('exit', code, signal);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Spawn the Rust binary in management mode.
|
||
* Returns true if the binary was found and spawned successfully.
|
||
*/
|
||
public async spawn(): Promise<boolean> {
|
||
return this.bridge.spawn();
|
||
}
|
||
|
||
/**
|
||
* Kill the Rust process and clean up.
|
||
*/
|
||
public kill(): void {
|
||
this.bridge.kill();
|
||
}
|
||
|
||
/**
|
||
* Whether the bridge is currently running.
|
||
*/
|
||
public get running(): boolean {
|
||
return this.bridge.running;
|
||
}
|
||
|
||
// --- Convenience methods for each management command ---
|
||
|
||
public async startProxy(config: any): Promise<void> {
|
||
await this.bridge.sendCommand('start', { config });
|
||
}
|
||
|
||
public async stopProxy(): Promise<void> {
|
||
await this.bridge.sendCommand('stop', {} as Record<string, never>);
|
||
}
|
||
|
||
public async updateRoutes(routes: IRouteConfig[]): Promise<void> {
|
||
await this.bridge.sendCommand('updateRoutes', { routes });
|
||
}
|
||
|
||
public async getMetrics(): Promise<any> {
|
||
return this.bridge.sendCommand('getMetrics', {} as Record<string, never>);
|
||
}
|
||
|
||
public async getStatistics(): Promise<any> {
|
||
return this.bridge.sendCommand('getStatistics', {} as Record<string, never>);
|
||
}
|
||
|
||
public async provisionCertificate(routeName: string): Promise<void> {
|
||
await this.bridge.sendCommand('provisionCertificate', { routeName });
|
||
}
|
||
|
||
public async renewCertificate(routeName: string): Promise<void> {
|
||
await this.bridge.sendCommand('renewCertificate', { routeName });
|
||
}
|
||
|
||
public async getCertificateStatus(routeName: string): Promise<any> {
|
||
return this.bridge.sendCommand('getCertificateStatus', { routeName });
|
||
}
|
||
|
||
public async getListeningPorts(): Promise<number[]> {
|
||
const result = await this.bridge.sendCommand('getListeningPorts', {} as Record<string, never>);
|
||
return result?.ports ?? [];
|
||
}
|
||
|
||
public async getNftablesStatus(): Promise<any> {
|
||
return this.bridge.sendCommand('getNftablesStatus', {} as Record<string, never>);
|
||
}
|
||
|
||
public async setSocketHandlerRelay(socketPath: string): Promise<void> {
|
||
await this.bridge.sendCommand('setSocketHandlerRelay', { socketPath });
|
||
}
|
||
|
||
public async addListeningPort(port: number): Promise<void> {
|
||
await this.bridge.sendCommand('addListeningPort', { port });
|
||
}
|
||
|
||
public async removeListeningPort(port: number): Promise<void> {
|
||
await this.bridge.sendCommand('removeListeningPort', { port });
|
||
}
|
||
|
||
public async loadCertificate(domain: string, cert: string, key: string, ca?: string): Promise<void> {
|
||
await this.bridge.sendCommand('loadCertificate', { domain, cert, key, ca });
|
||
}
|
||
}
|