initial
This commit is contained in:
152
ts/smartvpn.classes.vpnbridge.ts
Normal file
152
ts/smartvpn.classes.vpnbridge.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as plugins from './smartvpn.plugins.js';
|
||||
import * as paths from './smartvpn.paths.js';
|
||||
import type {
|
||||
TVpnTransportOptions,
|
||||
IVpnTransportSocket,
|
||||
} from './smartvpn.interfaces.js';
|
||||
import type { TCommandMap } from '@push.rocks/smartrust';
|
||||
|
||||
/**
|
||||
* Get the package root directory.
|
||||
*/
|
||||
function getPackageRoot(): string {
|
||||
const thisDir = plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url));
|
||||
return plugins.path.resolve(thisDir, '..');
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Node.js platform/arch to tsrust's platform suffix.
|
||||
*/
|
||||
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 smartvpn daemon binary.
|
||||
*/
|
||||
function buildLocalPaths(binaryName: string): string[] {
|
||||
const packageRoot = getPackageRoot();
|
||||
const suffix = getTsrustPlatformSuffix();
|
||||
const paths: string[] = [];
|
||||
|
||||
// dist_rust/ (tsrust cross-compiled output)
|
||||
if (suffix) {
|
||||
paths.push(plugins.path.join(packageRoot, 'dist_rust', `${binaryName}_${suffix}`));
|
||||
}
|
||||
paths.push(plugins.path.join(packageRoot, 'dist_rust', binaryName));
|
||||
|
||||
// Local dev build paths
|
||||
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'release', binaryName));
|
||||
paths.push(plugins.path.resolve(process.cwd(), 'rust', 'target', 'debug', binaryName));
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared bridge wrapper around smartrust RustBridge.
|
||||
* Supports stdio mode (dev: spawn child process) and socket mode (production: connect to running daemon).
|
||||
*/
|
||||
export class VpnBridge<TCommands extends TCommandMap> extends plugins.events.EventEmitter {
|
||||
private bridge: plugins.smartrust.RustBridge<TCommands>;
|
||||
private transportOptions: TVpnTransportOptions;
|
||||
private mode: 'client' | 'server';
|
||||
|
||||
constructor(options: {
|
||||
transport: TVpnTransportOptions;
|
||||
mode: 'client' | 'server';
|
||||
binaryName?: string;
|
||||
}) {
|
||||
super();
|
||||
|
||||
const binaryName = options.binaryName || 'smartvpn_daemon';
|
||||
this.transportOptions = options.transport;
|
||||
this.mode = options.mode;
|
||||
|
||||
this.bridge = new plugins.smartrust.RustBridge<TCommands>({
|
||||
binaryName,
|
||||
envVarName: 'SMARTVPN_RUST_BINARY',
|
||||
platformPackagePrefix: '@push.rocks/smartvpn',
|
||||
localPaths: buildLocalPaths(binaryName),
|
||||
cliArgs: ['--management', '--mode', this.mode],
|
||||
maxPayloadSize: 10 * 1024 * 1024, // 10 MB
|
||||
});
|
||||
|
||||
// Forward events from inner bridge
|
||||
this.bridge.on('exit', (code: number | null, signal: string | null) => {
|
||||
this.emit('exit', code, signal);
|
||||
});
|
||||
this.bridge.on('reconnected', () => {
|
||||
this.emit('reconnected');
|
||||
});
|
||||
|
||||
// Forward management events from the daemon
|
||||
// smartrust emits 'management:<eventName>' for unsolicited events
|
||||
this.bridge.on('management:status', (data: any) => {
|
||||
this.emit('status', data);
|
||||
});
|
||||
this.bridge.on('management:error', (data: any) => {
|
||||
this.emit('error', data);
|
||||
});
|
||||
this.bridge.on('management:client-connected', (data: any) => {
|
||||
this.emit('client-connected', data);
|
||||
});
|
||||
this.bridge.on('management:client-disconnected', (data: any) => {
|
||||
this.emit('client-disconnected', data);
|
||||
});
|
||||
this.bridge.on('management:started', (data: any) => {
|
||||
this.emit('started', data);
|
||||
});
|
||||
this.bridge.on('management:stopped', (data: any) => {
|
||||
this.emit('stopped', data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the bridge: spawn in stdio mode or connect in socket mode.
|
||||
*/
|
||||
public async start(): Promise<boolean> {
|
||||
if (this.transportOptions.transport === 'socket') {
|
||||
const socketOpts = this.transportOptions as IVpnTransportSocket;
|
||||
return this.bridge.connect(socketOpts.socketPath, {
|
||||
autoReconnect: socketOpts.autoReconnect,
|
||||
reconnectBaseDelayMs: socketOpts.reconnectBaseDelayMs,
|
||||
reconnectMaxDelayMs: socketOpts.reconnectMaxDelayMs,
|
||||
maxReconnectAttempts: socketOpts.maxReconnectAttempts,
|
||||
});
|
||||
} else {
|
||||
return this.bridge.spawn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the bridge. In socket mode, closes the socket (daemon stays alive).
|
||||
* In stdio mode, kills the child process.
|
||||
*/
|
||||
public stop(): void {
|
||||
this.bridge.kill();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a typed command to the daemon.
|
||||
*/
|
||||
public async sendCommand<K extends string & keyof TCommands>(
|
||||
method: K,
|
||||
params: TCommands[K]['params'],
|
||||
): Promise<TCommands[K]['result']> {
|
||||
return this.bridge.sendCommand(method, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the bridge is currently running/connected.
|
||||
*/
|
||||
public get running(): boolean {
|
||||
return this.bridge.running;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user