153 lines
4.7 KiB
TypeScript
153 lines
4.7 KiB
TypeScript
|
|
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;
|
||
|
|
}
|
||
|
|
}
|