Skip to main content
Glama
ooples

MCP Console Automation Server

VNCProtocol.ts85.8 kB
import { Socket, createConnection } from 'net'; import { createSecureContext, TLSSocket, connect as tlsConnect } from 'tls'; import * as crypto from 'crypto'; import * as zlib from 'zlib'; import * as fs from 'fs'; import * as path from 'path'; import { ChildProcess, spawn, SpawnOptionsWithoutStdio } from 'child_process'; import { BaseProtocol } from '../core/BaseProtocol.js'; import { SessionState } from '../core/IProtocol.js'; import { VNCConnectionOptions, VNCSession, VNCEncoding, VNCSecurityType, VeNCryptSubType, VNCRectangle, VNCFramebufferUpdate, VNCKeyEvent, VNCPointerEvent, VNCClientCutText, VNCFileTransfer, VNCClipboardSync, VNCPerformanceMetrics, VNCServerInfo, VNCRepeaterInfo, VNCProtocolConfig, VNCCapabilities, VNCMonitorConfig, ConsoleOutput, SessionOptions, ConsoleSession, ConsoleType, } from '../types/index.js'; import { ProtocolCapabilities, ProtocolHealthStatus, } from '../core/ProtocolFactory.js'; // RFB Protocol Constants (RFC 6143) const RFB_PROTOCOL_VERSION_3_3 = 'RFB 003.003\n'; const RFB_PROTOCOL_VERSION_3_7 = 'RFB 003.007\n'; const RFB_PROTOCOL_VERSION_3_8 = 'RFB 003.008\n'; // RFB Message Types const RFB_CLIENT_MESSAGES = { SET_PIXEL_FORMAT: 0, SET_ENCODINGS: 2, FRAMEBUFFER_UPDATE_REQUEST: 3, KEY_EVENT: 4, POINTER_EVENT: 5, CLIENT_CUT_TEXT: 6, ENABLE_CONTINUOUS_UPDATES: 150, // TigerVNC extension FENCE: 248, // TigerVNC fence extension XVP: 250, // Xvp extension QEMU: 255, // QEMU extension } as const; const RFB_SERVER_MESSAGES = { FRAMEBUFFER_UPDATE: 0, SET_COLOUR_MAP_ENTRIES: 1, BELL: 2, SERVER_CUT_TEXT: 3, RESIZE_FRAME_BUFFER: 4, // ExtendedDesktopSize pseudo-encoding END_OF_CONTINUOUS_UPDATES: 150, // TigerVNC extension FENCE: 248, // TigerVNC fence extension XVP: 250, // Xvp extension QEMU: 255, // QEMU extension } as const; // Encoding Constants const RFB_ENCODINGS = { RAW: 0, COPY_RECT: 1, RRE: 2, HEXTILE: 5, TRLE: 15, ZRLE: 16, CURSOR: -239, DESKTOP_SIZE: -223, LAST_RECT: -224, PSEUDO_DESKTOP_SIZE: -223, PSEUDO_CURSOR: -239, PSEUDO_X_CURSOR: -240, PSEUDO_DESKTOP_NAME: -307, PSEUDO_EXTENDED_DESKTOP_SIZE: -308, PSEUDO_XVP: -309, PSEUDO_FENCE: -312, PSEUDO_CONTINUOUS_UPDATES: -313, TIGHT: 7, ULTRA: 6, ZLIB: 6, ZLIBHEX: 8, JPEG: 21, // TurboVNC JRLE: 22, // TurboVNC } as const; // Security Types const RFB_SECURITY_TYPES = { INVALID: 0, NONE: 1, VNC_AUTH: 2, RA2: 5, RA2NE: 6, TIGHT: 16, ULTRA: 17, TLS: 18, VENCRYPT: 19, SASL: 20, MD5_HASH: 21, XVP: 22, } as const; // VeNCrypt Sub-types const VENCRYPT_SUBTYPES = { PLAIN: 256, TLS_NONE: 257, TLS_VNC: 258, TLS_PLAIN: 259, X509_NONE: 260, X509_VNC: 261, X509_PLAIN: 262, TLS_SASL: 263, X509_SASL: 264, } as const; // Key mappings for keyboard events const KEY_MAPPINGS = { // ASCII keys BACKSPACE: 0xff08, TAB: 0xff09, RETURN: 0xff0d, ESCAPE: 0xff1b, DELETE: 0xffff, // Function keys F1: 0xffbe, F2: 0xffbf, F3: 0xffc0, F4: 0xffc1, F5: 0xffc2, F6: 0xffc3, F7: 0xffc4, F8: 0xffc5, F9: 0xffc6, F10: 0xffc7, F11: 0xffc8, F12: 0xffc9, // Arrow keys LEFT: 0xff51, UP: 0xff52, RIGHT: 0xff53, DOWN: 0xff54, // Modifier keys SHIFT_L: 0xffe1, SHIFT_R: 0xffe2, CONTROL_L: 0xffe3, CONTROL_R: 0xffe4, META_L: 0xffe7, META_R: 0xffe8, ALT_L: 0xffe9, ALT_R: 0xffea, // Special keys HOME: 0xff50, END: 0xff57, PAGE_UP: 0xff55, PAGE_DOWN: 0xff56, INSERT: 0xff63, PRINT: 0xff61, MENU: 0xff67, PAUSE: 0xff13, CAPS_LOCK: 0xffe5, NUM_LOCK: 0xff7f, SCROLL_LOCK: 0xff14, } as const; /** * Production-ready VNC Protocol implementation with comprehensive RFB support */ export class VNCProtocol extends BaseProtocol { public readonly type: ConsoleType = 'vnc'; public readonly capabilities: ProtocolCapabilities; public readonly healthStatus: ProtocolHealthStatus; // VNC-specific properties private socket?: Socket | TLSSocket; private vncSessions: Map<string, VNCSession> = new Map(); private defaultOptions: Partial<VNCConnectionOptions>; private config: VNCProtocolConfig; private vncProcesses: Map<string, ChildProcess> = new Map(); private vncViewers: Map< string, { process: ChildProcess; pid: number; port: number } > = new Map(); // Required properties for VNC protocol functionality public options: Partial<VNCConnectionOptions>; public connectionId: string; public session?: VNCSession; // Protocol state private protocolVersion: string = ''; private securityTypes: number[] = []; private selectedSecurity: number = 0; private serverInit?: any; private pixelFormat: any; private framebufferWidth: number = 0; private framebufferHeight: number = 0; // Buffers and state management private receiveBuffer: Buffer = Buffer.alloc(0); private expectedMessageLength: number = 0; private messageHandler?: (data: Buffer) => void; private isConnected: boolean = false; private isAuthenticated: boolean = false; // Performance tracking private performanceMetrics: VNCPerformanceMetrics; private lastFrameTime: number = 0; private frameCount: number = 0; private bytesReceived: number = 0; private bytesSent: number = 0; // Encoding support private supportedEncodings: Map<number, string> = new Map(); private compressionLevel: number = 6; private qualityLevel: number = 6; // File transfer state private activeFileTransfers: Map<string, VNCFileTransfer> = new Map(); private clipboardData: string = ''; // TLS/Encryption state private tlsSocket?: TLSSocket; private encryptionEnabled: boolean = false; // Recording state private recordingStream?: fs.WriteStream; private recordingStartTime?: Date; // Multi-monitor support private monitors: VNCMonitorConfig[] = []; private primaryMonitor: number = 0; // Repeater support private repeaterConnection?: Socket; private repeaterMode: 'mode1' | 'mode2' = 'mode1'; constructor(options?: VNCConnectionOptions) { super('VNCProtocol'); this.defaultOptions = { ...this.getDefaultOptions(), ...(options || {}) }; this.capabilities = { supportsStreaming: true, supportsFileTransfer: true, supportsX11Forwarding: false, supportsPortForwarding: false, supportsAuthentication: true, supportsEncryption: true, supportsCompression: true, supportsMultiplexing: false, supportsKeepAlive: true, supportsReconnection: true, supportsBinaryData: true, supportsCustomEnvironment: false, supportsWorkingDirectory: false, supportsSignals: false, supportsResizing: true, supportsPTY: false, maxConcurrentSessions: 10, defaultTimeout: 30000, supportedEncodings: ['utf-8'], supportedAuthMethods: ['password', 'none', 'tls', 'vencrypt'], platformSupport: { windows: true, linux: true, macos: true, freebsd: true, }, }; this.healthStatus = { isHealthy: true, lastChecked: new Date(), errors: [], warnings: [], metrics: { activeSessions: 0, totalSessions: 0, averageLatency: 0, successRate: 1.0, uptime: 0, }, dependencies: { vnc: { available: true, version: '1.0' }, vncviewer: { available: true, version: '1.0' }, tightvnc: { available: true, version: '1.0' }, tigervnc: { available: true, version: '1.0' }, realvnc: { available: true, version: '1.0' }, ultravnc: { available: true, version: '1.0' }, }, }; this.config = this.createDefaultConfig(); this.initializeEncodingSupport(); this.initializePerformanceMetrics(); // Initialize required properties this.options = this.defaultOptions; this.connectionId = this.generateConnectionId(); } private getDefaultOptions(): Partial<VNCConnectionOptions> { return { host: 'localhost', port: 5900, rfbProtocolVersion: 'auto', sharedConnection: true, viewOnly: false, pixelFormat: { bitsPerPixel: 32, depth: 24, bigEndianFlag: false, trueColorFlag: true, redMax: 255, greenMax: 255, blueMax: 255, redShift: 16, greenShift: 8, blueShift: 0, }, supportedEncodings: [ 'zrle', 'hextile', 'rre', 'raw', 'copyrect', 'cursor', 'desktopsize', ], authMethod: 'vencrypt', timeout: 30000, keepAlive: true, keepAliveInterval: 30000, retryAttempts: 3, retryDelay: 5000, enableKeyboard: true, enableMouse: true, enableClipboard: true, compressionLevel: 6, qualityLevel: 6, enableJPEGCompression: true, jpegQuality: 75, autoResize: true, enableFileTransfer: true, maxFileSize: 100 * 1024 * 1024, // 100MB enableUltraVNCExtensions: true, enableTightVNCExtensions: true, enableRealVNCExtensions: true, enableAppleRemoteDesktop: false, cursorMode: 'local', enableCursorShapeUpdates: true, enableRichCursor: true, recordSession: false, recordingFormat: 'fbs', securityTypes: ['vencrypt', 'tls', 'vnc', 'none'], allowInsecure: false, enableFastPath: true, bufferSize: 65536, maxUpdateRate: 60, enableLazyUpdates: false, debugLevel: 'info', }; } private createDefaultConfig(): VNCProtocolConfig { return { defaultPort: 5900, defaultProtocolVersion: 'RFB 003.008\n', connectionTimeout: 30000, readTimeout: 60000, writeTimeout: 30000, preferredEncodings: [ 'zrle', 'hextile', 'tight', 'rre', 'raw', 'copyrect', ], defaultPixelFormat: { bitsPerPixel: 32, depth: 24, bigEndianFlag: false, trueColorFlag: true, }, allowedSecurityTypes: ['vencrypt', 'tls', 'vnc'], requireEncryption: false, enableCompression: true, compressionLevel: 6, enableJPEG: true, jpegQuality: 75, maxUpdateRate: 60, bufferSize: 65536, enableCursorShapeUpdates: true, enableDesktopResize: true, enableContinuousUpdates: true, enableFileTransfer: true, enableClipboard: true, logLevel: 'info', enableProtocolLogging: false, enablePerformanceLogging: true, enableSessionRecording: false, recordingPath: './vnc-recordings', recordingFormat: 'fbs', enableUltraVNCExtensions: true, enableTightVNCExtensions: true, enableRealVNCExtensions: true, enableTigerVNCExtensions: true, enableTurboVNCExtensions: true, }; } private initializeEncodingSupport(): void { this.supportedEncodings.set(RFB_ENCODINGS.RAW, 'Raw'); this.supportedEncodings.set(RFB_ENCODINGS.COPY_RECT, 'CopyRect'); this.supportedEncodings.set(RFB_ENCODINGS.RRE, 'RRE'); this.supportedEncodings.set(RFB_ENCODINGS.HEXTILE, 'Hextile'); this.supportedEncodings.set(RFB_ENCODINGS.TRLE, 'TRLE'); this.supportedEncodings.set(RFB_ENCODINGS.ZRLE, 'ZRLE'); this.supportedEncodings.set(RFB_ENCODINGS.TIGHT, 'Tight'); this.supportedEncodings.set(RFB_ENCODINGS.ULTRA, 'Ultra'); this.supportedEncodings.set(RFB_ENCODINGS.ZLIBHEX, 'ZlibHex'); this.supportedEncodings.set(RFB_ENCODINGS.JPEG, 'JPEG'); this.supportedEncodings.set(RFB_ENCODINGS.JRLE, 'JRLE'); this.supportedEncodings.set(RFB_ENCODINGS.CURSOR, 'Cursor'); this.supportedEncodings.set(RFB_ENCODINGS.DESKTOP_SIZE, 'DesktopSize'); this.supportedEncodings.set(RFB_ENCODINGS.LAST_RECT, 'LastRect'); this.supportedEncodings.set( RFB_ENCODINGS.PSEUDO_EXTENDED_DESKTOP_SIZE, 'ExtendedDesktopSize' ); this.supportedEncodings.set( RFB_ENCODINGS.PSEUDO_CONTINUOUS_UPDATES, 'ContinuousUpdates' ); this.supportedEncodings.set(RFB_ENCODINGS.PSEUDO_FENCE, 'Fence'); } private initializePerformanceMetrics(): void { this.performanceMetrics = { sessionId: this.connectionId, timestamp: new Date(), frameRate: 0, avgFrameTime: 0, frameSkips: 0, bandwidth: 0, latency: 0, packetLoss: 0, compressionRatio: 1.0, uncompressedBytes: 0, compressedBytes: 0, cpuUsage: 0, memoryUsage: 0, networkIO: 0, pixelChanges: 0, screenUpdateArea: 0, cursorUpdates: 0, }; } /** * Connect to VNC server with comprehensive authentication and encryption support */ public async connect(): Promise<VNCSession> { try { this.logger.info( `Connecting to VNC server at ${this.options.host}:${this.options.port}` ); // Handle VNC Repeater connection if configured if (this.options.repeater?.enabled) { await this.connectViaRepeater(); } else { await this.connectDirect(); } // Initialize session this.session = this.createVNCSession(); // Start session recording if enabled if (this.options.recordSession) { await this.startSessionRecording(); } // Begin RFB protocol handshake await this.performHandshake(); this.logger.info( `Successfully connected to VNC server: ${this.session.serverName || 'Unknown'}` ); this.emit('connected', this.session); return this.session; } catch (error) { this.logger.error('Failed to connect to VNC server:', error); this.emit('error', error); throw error; } } private async connectDirect(): Promise<void> { return new Promise((resolve, reject) => { const connectOptions = { host: this.options.host, port: this.options.port, timeout: this.options.timeout, }; // Handle proxy connection if configured if (this.options.proxy) { // Proxy support would be implemented here this.logger.warn( 'Proxy support not yet implemented, connecting directly' ); } this.socket = createConnection(connectOptions); this.socket.setTimeout(this.options.timeout || 30000); this.socket.on('connect', () => { this.logger.debug('TCP connection established'); this.isConnected = true; if (this.options.keepAlive) { this.socket!.setKeepAlive( true, this.options.keepAliveInterval || 30000 ); } resolve(); }); this.socket.on('data', (data: Buffer) => { this.handleIncomingData(data); }); this.socket.on('error', (error: Error) => { this.logger.error('Socket error:', error); this.emit('error', error); reject(error); }); this.socket.on('close', () => { this.logger.debug('Socket closed'); this.isConnected = false; this.emit('disconnected'); }); this.socket.on('timeout', () => { this.logger.error('Connection timeout'); const error = new Error('Connection timeout'); this.emit('error', error); reject(error); }); }); } private async connectViaRepeater(): Promise<void> { if (!this.options.repeater) { throw new Error('Repeater configuration not provided'); } const { host, port, id, mode } = this.options.repeater; this.repeaterMode = mode || 'mode1'; this.logger.info( `Connecting via VNC repeater: ${host}:${port} (${this.repeaterMode})` ); return new Promise((resolve, reject) => { this.repeaterConnection = createConnection({ host: host || this.options.host, port: port || 5901, timeout: this.options.timeout, }); this.repeaterConnection.on('connect', async () => { try { if (this.repeaterMode === 'mode1') { // Mode 1: Send ID then connect const idBuffer = Buffer.from(id || '000000000000'); await this.writeData(idBuffer); // Wait for repeater response then establish VNC connection this.socket = this.repeaterConnection; resolve(); } else { // Mode 2: More complex handshake await this.handleRepeaterMode2(); resolve(); } } catch (error) { reject(error); } }); this.repeaterConnection.on('error', reject); }); } private async handleRepeaterMode2(): Promise<void> { // Mode 2 repeater handshake implementation // This would involve a more complex protocol exchange this.logger.warn('Repeater Mode 2 not fully implemented yet'); } private createVNCSession(): VNCSession { return { sessionId: this.connectionId, connectionId: this.connectionId, host: this.options.host, port: this.options.port || 5900, protocolVersion: this.protocolVersion, securityType: this.mapSecurityType(this.selectedSecurity), sharedConnection: this.options.sharedConnection || true, viewOnlyMode: this.options.viewOnly || false, framebufferInfo: { width: this.framebufferWidth, height: this.framebufferHeight, pixelFormat: this.pixelFormat || this.options.pixelFormat, }, supportedEncodings: this.options.supportedEncodings || [], serverCapabilities: { cursorShapeUpdates: this.options.enableCursorShapeUpdates || false, richCursor: this.options.enableRichCursor || false, desktopResize: this.options.autoResize || false, continuousUpdates: false, fence: false, fileTransfer: this.options.enableFileTransfer || false, clipboardTransfer: this.options.enableClipboard || false, audio: false, }, status: 'connecting', connectionTime: new Date(), statistics: { bytesReceived: 0, bytesSent: 0, framebufferUpdates: 0, keyboardEvents: 0, mouseEvents: 0, clipboardTransfers: 0, fileTransfers: 0, avgFrameRate: 0, bandwidth: 0, compression: 1.0, latency: 0, }, errorCount: 0, warnings: [], monitors: this.options.monitors || [], metadata: {}, }; } private mapSecurityType(securityTypeNumber: number): VNCSecurityType { switch (securityTypeNumber) { case RFB_SECURITY_TYPES.NONE: return 'none'; case RFB_SECURITY_TYPES.VNC_AUTH: return 'vnc'; case RFB_SECURITY_TYPES.RA2: return 'ra2'; case RFB_SECURITY_TYPES.RA2NE: return 'ra2ne'; case RFB_SECURITY_TYPES.TIGHT: return 'tight'; case RFB_SECURITY_TYPES.ULTRA: return 'ultra'; case RFB_SECURITY_TYPES.TLS: return 'tls'; case RFB_SECURITY_TYPES.VENCRYPT: return 'vencrypt'; case RFB_SECURITY_TYPES.SASL: return 'sasl'; case RFB_SECURITY_TYPES.MD5_HASH: return 'md5hash'; case RFB_SECURITY_TYPES.XVP: return 'xvp'; default: return 'vnc'; } } /** * RFB Protocol Handshake Implementation */ private async performHandshake(): Promise<void> { try { // Step 1: Protocol Version Negotiation await this.negotiateProtocolVersion(); // Step 2: Security Negotiation await this.negotiateSecurity(); // Step 3: Authentication await this.performAuthentication(); // Step 4: Client Initialization await this.performClientInit(); // Step 5: Server Initialization await this.handleServerInit(); // Step 6: Set encodings and pixel format await this.configureSession(); this.isAuthenticated = true; if (this.session) { this.session.status = 'connected'; } } catch (error) { this.logger.error('Handshake failed:', error); throw error; } } private async negotiateProtocolVersion(): Promise<void> { return new Promise((resolve, reject) => { this.messageHandler = (data: Buffer) => { if (data.length >= 12) { const version = data.slice(0, 12).toString(); this.logger.debug(`Server protocol version: ${version.trim()}`); // Determine client protocol version to use let clientVersion: string; if (this.options.rfbProtocolVersion === 'auto') { if (version.includes('003.008')) { clientVersion = RFB_PROTOCOL_VERSION_3_8; } else if (version.includes('003.007')) { clientVersion = RFB_PROTOCOL_VERSION_3_7; } else { clientVersion = RFB_PROTOCOL_VERSION_3_3; } } else { switch (this.options.rfbProtocolVersion) { case '3.8': clientVersion = RFB_PROTOCOL_VERSION_3_8; break; case '3.7': clientVersion = RFB_PROTOCOL_VERSION_3_7; break; case '3.3': clientVersion = RFB_PROTOCOL_VERSION_3_3; break; default: clientVersion = RFB_PROTOCOL_VERSION_3_8; break; } } this.protocolVersion = clientVersion.trim(); this.logger.debug( `Using client protocol version: ${this.protocolVersion}` ); // Send client protocol version this.writeData(Buffer.from(clientVersion)) .then(() => { this.messageHandler = undefined; resolve(); }) .catch(reject); } }; this.expectedMessageLength = 12; }); } private async negotiateSecurity(): Promise<void> { return new Promise((resolve, reject) => { this.messageHandler = async (data: Buffer) => { try { if (this.protocolVersion === 'RFB 003.003') { // RFB 3.3: Server decides security type if (data.length >= 4) { this.selectedSecurity = data.readUInt32BE(0); this.logger.debug( `Server selected security type: ${this.selectedSecurity}` ); this.messageHandler = undefined; resolve(); } } else { // RFB 3.7+: Client chooses from server list const numTypes = data.readUInt8(0); if (numTypes === 0) { // Server error const reasonLength = data.readUInt32BE(1); const reason = data.slice(5, 5 + reasonLength).toString(); reject( new Error(`Server security negotiation failed: ${reason}`) ); return; } this.securityTypes = []; for (let i = 0; i < numTypes; i++) { this.securityTypes.push(data.readUInt8(1 + i)); } this.logger.debug( `Server security types: ${this.securityTypes.join(', ')}` ); // Select preferred security type this.selectedSecurity = this.selectPreferredSecurity(); // Send selected security type const securityBuffer = Buffer.allocUnsafe(1); securityBuffer.writeUInt8(this.selectedSecurity, 0); await this.writeData(securityBuffer); this.messageHandler = undefined; resolve(); } } catch (error) { reject(error); } }; this.expectedMessageLength = this.protocolVersion === 'RFB 003.003' ? 4 : 1; }); } private selectPreferredSecurity(): number { const preferenceOrder = [ RFB_SECURITY_TYPES.VENCRYPT, RFB_SECURITY_TYPES.TLS, RFB_SECURITY_TYPES.VNC_AUTH, RFB_SECURITY_TYPES.NONE, ]; for (const preferred of preferenceOrder) { if (this.securityTypes.includes(preferred)) { // Check if this security type is allowed by configuration if ( preferred === RFB_SECURITY_TYPES.NONE && !this.options.allowInsecure ) { continue; } return preferred; } } throw new Error( `No compatible security type found. Server offers: ${this.securityTypes.join(', ')}` ); } private async performAuthentication(): Promise<void> { switch (this.selectedSecurity) { case RFB_SECURITY_TYPES.NONE: await this.authenticateNone(); break; case RFB_SECURITY_TYPES.VNC_AUTH: await this.authenticateVNC(); break; case RFB_SECURITY_TYPES.TLS: await this.authenticateTLS(); break; case RFB_SECURITY_TYPES.VENCRYPT: await this.authenticateVeNCrypt(); break; default: throw new Error(`Unsupported security type: ${this.selectedSecurity}`); } } private async authenticateNone(): Promise<void> { this.logger.debug('Using no authentication'); // For RFB 3.8+, wait for security result if (this.protocolVersion !== 'RFB 003.003') { await this.waitForSecurityResult(); } } private async authenticateVNC(): Promise<void> { if (!this.options.password) { throw new Error('VNC authentication requires a password'); } return new Promise((resolve, reject) => { this.messageHandler = async (data: Buffer) => { try { if (data.length >= 16) { const challenge = data.slice(0, 16); const response = this.vncAuthChallenge( challenge, this.options.password! ); await this.writeData(response); await this.waitForSecurityResult(); this.messageHandler = undefined; resolve(); } } catch (error) { reject(error); } }; this.expectedMessageLength = 16; }); } private vncAuthChallenge(challenge: Buffer, password: string): Buffer { // VNC uses DES encryption with the password as key const key = Buffer.alloc(8); const passwordBytes = Buffer.from(password.slice(0, 8), 'binary'); passwordBytes.copy(key); // VNC DES key has bits in reverse order for (let i = 0; i < 8; i++) { let byte = key[i]; byte = ((byte & 0xf0) >> 4) | ((byte & 0x0f) << 4); byte = ((byte & 0xcc) >> 2) | ((byte & 0x33) << 2); byte = ((byte & 0xaa) >> 1) | ((byte & 0x55) << 1); key[i] = byte; } const cipher = crypto.createCipheriv('des-ecb', key, null); cipher.setAutoPadding(false); return Buffer.concat([cipher.update(challenge), cipher.final()]); } private async authenticateTLS(): Promise<void> { this.logger.debug('Starting TLS authentication'); await this.upgradToTLS(); // After TLS upgrade, continue with sub-authentication await this.negotiateSecurity(); // Re-negotiate security over TLS } private async authenticateVeNCrypt(): Promise<void> { this.logger.debug('Starting VeNCrypt authentication'); // VeNCrypt version negotiation return new Promise((resolve, reject) => { this.messageHandler = async (data: Buffer) => { try { if (data.length >= 2) { const majorVersion = data.readUInt8(0); const minorVersion = data.readUInt8(1); this.logger.debug( `VeNCrypt server version: ${majorVersion}.${minorVersion}` ); // Send client version (0.2) const versionBuffer = Buffer.from([0, 2]); await this.writeData(versionBuffer); // Continue with VeNCrypt handshake this.messageHandler = this.handleVeNCryptHandshake.bind(this); this.expectedMessageLength = 1; } } catch (error) { reject(error); } }; this.expectedMessageLength = 2; }); } private async handleVeNCryptHandshake(data: Buffer): Promise<void> { const status = data.readUInt8(0); if (status !== 0) { throw new Error('VeNCrypt version negotiation failed'); } // Get list of VeNCrypt subtypes this.messageHandler = async (data: Buffer) => { const numSubtypes = data.readUInt8(0); const subtypes: number[] = []; for (let i = 0; i < numSubtypes; i++) { subtypes.push(data.readUInt32BE(1 + i * 4)); } this.logger.debug(`VeNCrypt subtypes: ${subtypes.join(', ')}`); // Select preferred subtype const selectedSubtype = this.selectVeNCryptSubtype(subtypes); // Send selected subtype const subtypeBuffer = Buffer.allocUnsafe(4); subtypeBuffer.writeUInt32BE(selectedSubtype, 0); await this.writeData(subtypeBuffer); // Handle subtype-specific authentication await this.handleVeNCryptSubtype(selectedSubtype); }; this.expectedMessageLength = 1; // Will be updated when we know the number of subtypes } private selectVeNCryptSubtype(subtypes: number[]): number { const preferenceOrder = [ VENCRYPT_SUBTYPES.X509_VNC, VENCRYPT_SUBTYPES.X509_PLAIN, VENCRYPT_SUBTYPES.TLS_VNC, VENCRYPT_SUBTYPES.TLS_PLAIN, VENCRYPT_SUBTYPES.PLAIN, ]; for (const preferred of preferenceOrder) { if (subtypes.includes(preferred)) { return preferred; } } throw new Error( `No compatible VeNCrypt subtype found. Server offers: ${subtypes.join(', ')}` ); } private async handleVeNCryptSubtype(subtype: number): Promise<void> { switch (subtype) { case VENCRYPT_SUBTYPES.TLS_VNC: case VENCRYPT_SUBTYPES.X509_VNC: await this.upgradToTLS(); await this.authenticateVNC(); break; case VENCRYPT_SUBTYPES.TLS_PLAIN: case VENCRYPT_SUBTYPES.X509_PLAIN: await this.upgradToTLS(); await this.authenticatePlain(); break; case VENCRYPT_SUBTYPES.PLAIN: await this.authenticatePlain(); break; default: throw new Error(`Unsupported VeNCrypt subtype: ${subtype}`); } } private async upgradToTLS(): Promise<void> { return new Promise((resolve, reject) => { const tlsOptions: any = { socket: this.socket, rejectUnauthorized: this.options.tlsOptions?.rejectUnauthorized ?? true, }; if (this.options.tlsOptions?.certificates) { const certs = this.options.tlsOptions.certificates; if (certs.ca) tlsOptions.ca = certs.ca; if (certs.cert) tlsOptions.cert = certs.cert; if (certs.key) tlsOptions.key = certs.key; if (certs.passphrase) tlsOptions.passphrase = certs.passphrase; } this.tlsSocket = tlsConnect(tlsOptions); this.socket = this.tlsSocket; this.encryptionEnabled = true; this.tlsSocket.on('secureConnect', () => { this.logger.debug('TLS connection established'); resolve(); }); this.tlsSocket.on('error', (error) => { this.logger.error('TLS connection failed:', error); reject(error); }); // Re-attach data handler this.tlsSocket.on('data', (data: Buffer) => { this.handleIncomingData(data); }); }); } private async authenticatePlain(): Promise<void> { if (!this.options.username || !this.options.password) { throw new Error('Plain authentication requires username and password'); } const username = Buffer.from(this.options.username, 'utf8'); const password = Buffer.from(this.options.password, 'utf8'); const authBuffer = Buffer.alloc(4 + username.length + 4 + password.length); let offset = 0; authBuffer.writeUInt32BE(username.length, offset); offset += 4; username.copy(authBuffer, offset); offset += username.length; authBuffer.writeUInt32BE(password.length, offset); offset += 4; password.copy(authBuffer, offset); await this.writeData(authBuffer); await this.waitForSecurityResult(); } private async waitForSecurityResult(): Promise<void> { return new Promise((resolve, reject) => { this.messageHandler = (data: Buffer) => { if (data.length >= 4) { const result = data.readUInt32BE(0); if (result === 0) { this.logger.debug('Authentication successful'); this.messageHandler = undefined; resolve(); } else { // Authentication failed let reason = 'Authentication failed'; if (this.protocolVersion !== 'RFB 003.003' && data.length > 4) { const reasonLength = data.readUInt32BE(4); if (data.length >= 8 + reasonLength) { reason = data.slice(8, 8 + reasonLength).toString(); } } reject(new Error(reason)); } } }; this.expectedMessageLength = 4; }); } private async performClientInit(): Promise<void> { // Send ClientInit message const sharedFlag = this.options.sharedConnection ? 1 : 0; const clientInitBuffer = Buffer.from([sharedFlag]); await this.writeData(clientInitBuffer); this.logger.debug(`Sent ClientInit: shared=${sharedFlag}`); } private async handleServerInit(): Promise<void> { return new Promise((resolve, reject) => { this.messageHandler = (data: Buffer) => { try { if (data.length < 24) { this.logger.warn('ServerInit message too short'); return; } // Parse ServerInit message this.framebufferWidth = data.readUInt16BE(0); this.framebufferHeight = data.readUInt16BE(2); const pixelFormat = { bitsPerPixel: data.readUInt8(4), depth: data.readUInt8(5), bigEndianFlag: data.readUInt8(6) === 1, trueColorFlag: data.readUInt8(7) === 1, redMax: data.readUInt16BE(8), greenMax: data.readUInt16BE(10), blueMax: data.readUInt16BE(12), redShift: data.readUInt8(14), greenShift: data.readUInt8(15), blueShift: data.readUInt8(16), }; this.pixelFormat = pixelFormat; // Desktop name const nameLength = data.readUInt32BE(20); let desktopName = ''; if (data.length >= 24 + nameLength) { desktopName = data.slice(24, 24 + nameLength).toString('utf8'); } this.serverInit = { framebufferWidth: this.framebufferWidth, framebufferHeight: this.framebufferHeight, pixelFormat, desktopName, }; this.logger.debug( `ServerInit: ${this.framebufferWidth}x${this.framebufferHeight}, ${desktopName}` ); if (this.session) { this.session.serverName = desktopName; this.session.framebufferInfo = { width: this.framebufferWidth, height: this.framebufferHeight, pixelFormat, }; } this.messageHandler = this.handleServerMessage.bind(this); this.expectedMessageLength = 1; // Server messages start with 1-byte type resolve(); } catch (error) { reject(error); } }; this.expectedMessageLength = 24; // Minimum ServerInit size }); } private async configureSession(): Promise<void> { // Set pixel format if different from server default if ( this.options.pixelFormat && JSON.stringify(this.options.pixelFormat) !== JSON.stringify(this.pixelFormat) ) { await this.setPixelFormat(this.options.pixelFormat); } // Set supported encodings await this.setEncodings(); // Request initial framebuffer update await this.requestFramebufferUpdate( 0, 0, this.framebufferWidth, this.framebufferHeight, false ); this.logger.debug('Session configuration completed'); } private async setPixelFormat(pixelFormat: any): Promise<void> { const buffer = Buffer.allocUnsafe(20); let offset = 0; buffer.writeUInt8(RFB_CLIENT_MESSAGES.SET_PIXEL_FORMAT, offset++); buffer.writeUInt8(0, offset++); // padding buffer.writeUInt8(0, offset++); // padding buffer.writeUInt8(0, offset++); // padding buffer.writeUInt8(pixelFormat.bitsPerPixel || 32, offset++); buffer.writeUInt8(pixelFormat.depth || 24, offset++); buffer.writeUInt8(pixelFormat.bigEndianFlag ? 1 : 0, offset++); buffer.writeUInt8(pixelFormat.trueColorFlag ? 1 : 0, offset++); buffer.writeUInt16BE(pixelFormat.redMax || 255, offset); offset += 2; buffer.writeUInt16BE(pixelFormat.greenMax || 255, offset); offset += 2; buffer.writeUInt16BE(pixelFormat.blueMax || 255, offset); offset += 2; buffer.writeUInt8(pixelFormat.redShift || 16, offset++); buffer.writeUInt8(pixelFormat.greenShift || 8, offset++); buffer.writeUInt8(pixelFormat.blueShift || 0, offset++); buffer.writeUInt8(0, offset++); // padding buffer.writeUInt8(0, offset++); // padding buffer.writeUInt8(0, offset++); // padding await this.writeData(buffer); this.pixelFormat = pixelFormat; this.logger.debug('Set pixel format:', pixelFormat); } private async setEncodings(): Promise<void> { const encodings = this.mapEncodingsToNumbers( this.options.supportedEncodings || [] ); const buffer = Buffer.allocUnsafe(4 + encodings.length * 4); let offset = 0; buffer.writeUInt8(RFB_CLIENT_MESSAGES.SET_ENCODINGS, offset++); buffer.writeUInt8(0, offset++); // padding buffer.writeUInt16BE(encodings.length, offset); offset += 2; for (const encoding of encodings) { buffer.writeInt32BE(encoding, offset); offset += 4; } await this.writeData(buffer); this.logger.debug( `Set encodings: ${this.options.supportedEncodings?.join(', ')}` ); } private mapEncodingsToNumbers(encodings: VNCEncoding[]): number[] { const encodingMap: Record<string, number> = { raw: RFB_ENCODINGS.RAW, copyrect: RFB_ENCODINGS.COPY_RECT, rre: RFB_ENCODINGS.RRE, hextile: RFB_ENCODINGS.HEXTILE, trle: RFB_ENCODINGS.TRLE, zrle: RFB_ENCODINGS.ZRLE, tight: RFB_ENCODINGS.TIGHT, ultra: RFB_ENCODINGS.ULTRA, zlibhex: RFB_ENCODINGS.ZLIBHEX, jpeg: RFB_ENCODINGS.JPEG, jrle: RFB_ENCODINGS.JRLE, cursor: RFB_ENCODINGS.CURSOR, desktopsize: RFB_ENCODINGS.DESKTOP_SIZE, lastrect: RFB_ENCODINGS.LAST_RECT, continuous: RFB_ENCODINGS.PSEUDO_CONTINUOUS_UPDATES, fence: RFB_ENCODINGS.PSEUDO_FENCE, x11cursor: RFB_ENCODINGS.PSEUDO_X_CURSOR, richcursor: RFB_ENCODINGS.CURSOR, wmvi: 0x574d5649, }; return encodings .map((encoding) => encodingMap[encoding]) .filter((num) => num !== undefined); } /** * Handle incoming data from server */ private handleIncomingData(data: Buffer): void { this.receiveBuffer = Buffer.concat([this.receiveBuffer, data]); this.bytesReceived += data.length; // Update performance metrics this.updateNetworkMetrics(); // Process complete messages while ( this.receiveBuffer.length >= this.expectedMessageLength && (this.messageHandler || this.expectedMessageLength > 0) ) { if (this.messageHandler) { const messageData = this.receiveBuffer.slice( 0, this.expectedMessageLength ); this.receiveBuffer = this.receiveBuffer.slice( this.expectedMessageLength ); this.messageHandler(messageData); } else { // Default server message handling this.processServerMessage(); } } } private processServerMessage(): void { if (this.receiveBuffer.length < 1) return; const messageType = this.receiveBuffer.readUInt8(0); switch (messageType) { case RFB_SERVER_MESSAGES.FRAMEBUFFER_UPDATE: this.handleFramebufferUpdate(); break; case RFB_SERVER_MESSAGES.SET_COLOUR_MAP_ENTRIES: this.handleSetColourMapEntries(); break; case RFB_SERVER_MESSAGES.BELL: this.handleBell(); break; case RFB_SERVER_MESSAGES.SERVER_CUT_TEXT: this.handleServerCutText(); break; case RFB_SERVER_MESSAGES.RESIZE_FRAME_BUFFER: this.handleResizeFrameBuffer(); break; default: this.logger.warn(`Unknown server message type: ${messageType}`); // Skip unknown message this.receiveBuffer = this.receiveBuffer.slice(1); break; } } private handleFramebufferUpdate(): void { if (this.receiveBuffer.length < 4) return; const numRectangles = this.receiveBuffer.readUInt16BE(2); if (this.receiveBuffer.length < 4 + numRectangles * 12) return; const rectangles: VNCRectangle[] = []; let offset = 4; for (let i = 0; i < numRectangles; i++) { if (this.receiveBuffer.length < offset + 12) return; const x = this.receiveBuffer.readUInt16BE(offset); const y = this.receiveBuffer.readUInt16BE(offset + 2); const width = this.receiveBuffer.readUInt16BE(offset + 4); const height = this.receiveBuffer.readUInt16BE(offset + 6); const encoding = this.receiveBuffer.readInt32BE(offset + 8); offset += 12; // Calculate expected data size based on encoding let dataSize = 0; let data: Buffer; switch (encoding) { case RFB_ENCODINGS.RAW: dataSize = width * height * (this.pixelFormat.bitsPerPixel / 8); break; case RFB_ENCODINGS.COPY_RECT: dataSize = 4; // source x, y break; case RFB_ENCODINGS.CURSOR: case RFB_ENCODINGS.PSEUDO_X_CURSOR: dataSize = this.calculateCursorDataSize(width, height); break; case RFB_ENCODINGS.DESKTOP_SIZE: case RFB_ENCODINGS.PSEUDO_EXTENDED_DESKTOP_SIZE: dataSize = 0; // No additional data break; default: // For complex encodings, we need to parse the data to determine size dataSize = this.calculateEncodingDataSize( encoding, width, height, offset ); break; } if (this.receiveBuffer.length < offset + dataSize) return; data = this.receiveBuffer.slice(offset, offset + dataSize); offset += dataSize; rectangles.push({ x, y, width, height, encoding: this.mapEncodingNumberToString(encoding), data, }); // Handle pseudo-encodings if (encoding === RFB_ENCODINGS.DESKTOP_SIZE) { this.handleDesktopResize(width, height); } else if ( encoding === RFB_ENCODINGS.CURSOR || encoding === RFB_ENCODINGS.PSEUDO_X_CURSOR ) { this.handleCursorUpdate(x, y, width, height, data); } } // Remove processed data from buffer this.receiveBuffer = this.receiveBuffer.slice(offset); // Create framebuffer update event const update: VNCFramebufferUpdate = { messageType: RFB_SERVER_MESSAGES.FRAMEBUFFER_UPDATE, rectangles, timestamp: new Date(), sequenceNumber: this.frameCount++, }; // Update performance metrics this.updateFrameMetrics(); // Emit framebuffer update event this.emit('framebufferUpdate', update); // Process rectangles for recording if (this.recordingStream) { this.recordFramebufferUpdate(update); } } private calculateCursorDataSize(width: number, height: number): number { const pixelData = width * height * (this.pixelFormat.bitsPerPixel / 8); const maskData = Math.floor((width + 7) / 8) * height; return pixelData + maskData; } private calculateEncodingDataSize( encoding: number, width: number, height: number, offset: number ): number { // This is a simplified implementation // In a full implementation, you would need to parse the encoding-specific data switch (encoding) { case RFB_ENCODINGS.RRE: return this.calculateRREDataSize(width, height, offset); case RFB_ENCODINGS.HEXTILE: return this.calculateHextileDataSize(width, height, offset); case RFB_ENCODINGS.ZRLE: case RFB_ENCODINGS.TRLE: return this.calculateZRLEDataSize(offset); case RFB_ENCODINGS.TIGHT: return this.calculateTightDataSize(offset); default: // For unknown encodings, assume raw data size return width * height * (this.pixelFormat.bitsPerPixel / 8); } } private calculateRREDataSize( width: number, height: number, offset: number ): number { if (this.receiveBuffer.length < offset + 4) return 0; const numSubrects = this.receiveBuffer.readUInt32BE(offset); const bgPixelSize = this.pixelFormat.bitsPerPixel / 8; const subrectSize = bgPixelSize + 8; // pixel + x + y + w + h return 4 + bgPixelSize + numSubrects * subrectSize; } private calculateHextileDataSize( width: number, height: number, offset: number ): number { // Hextile divides rectangle into 16x16 tiles let dataSize = 0; let currentOffset = offset; for (let ty = 0; ty < height; ty += 16) { for (let tx = 0; tx < width; tx += 16) { if (this.receiveBuffer.length < currentOffset + 1) return 0; const subencoding = this.receiveBuffer.readUInt8(currentOffset); currentOffset += 1; dataSize += 1; const tileWidth = Math.min(16, width - tx); const tileHeight = Math.min(16, height - ty); const pixelSize = this.pixelFormat.bitsPerPixel / 8; if (subencoding & 0x01) { // Raw const tileDataSize = tileWidth * tileHeight * pixelSize; currentOffset += tileDataSize; dataSize += tileDataSize; } else { // Parse other hextile subencodings if (subencoding & 0x02) { // Background specified currentOffset += pixelSize; dataSize += pixelSize; } if (subencoding & 0x04) { // Foreground specified currentOffset += pixelSize; dataSize += pixelSize; } if (subencoding & 0x08) { // Any subrects if (this.receiveBuffer.length < currentOffset + 1) return 0; const numSubrects = this.receiveBuffer.readUInt8(currentOffset); currentOffset += 1; dataSize += 1; const subrectSize = subencoding & 0x10 ? 2 : 2 + pixelSize; // Subrects coloured currentOffset += numSubrects * subrectSize; dataSize += numSubrects * subrectSize; } } } } return dataSize; } private calculateZRLEDataSize(offset: number): number { if (this.receiveBuffer.length < offset + 4) return 0; const length = this.receiveBuffer.readUInt32BE(offset); return 4 + length; } private calculateTightDataSize(offset: number): number { // Tight encoding has a complex structure // This is a simplified version if (this.receiveBuffer.length < offset + 1) return 0; const compressionControl = this.receiveBuffer.readUInt8(offset); let dataSize = 1; let currentOffset = offset + 1; // Parse length let length = 0; if (compressionControl & 0x80) { // JPEG if (this.receiveBuffer.length < currentOffset + 3) return 0; length = this.receiveBuffer.readUInt8(currentOffset) | (this.receiveBuffer.readUInt8(currentOffset + 1) << 8) | (this.receiveBuffer.readUInt8(currentOffset + 2) << 16); dataSize += 3; } else { // Basic or fill compression if (this.receiveBuffer.length < currentOffset + 1) return 0; const byte1 = this.receiveBuffer.readUInt8(currentOffset); dataSize += 1; if (byte1 & 0x80) { if (this.receiveBuffer.length < currentOffset + 2) return 0; const byte2 = this.receiveBuffer.readUInt8(currentOffset + 1); dataSize += 1; if (byte2 & 0x80) { if (this.receiveBuffer.length < currentOffset + 3) return 0; const byte3 = this.receiveBuffer.readUInt8(currentOffset + 2); length = (byte1 & 0x7f) | ((byte2 & 0x7f) << 7) | ((byte3 & 0x7f) << 14); dataSize += 1; } else { length = (byte1 & 0x7f) | ((byte2 & 0x7f) << 7); } } else { length = byte1 & 0x7f; } } return dataSize + length; } private mapEncodingNumberToString(encoding: number): VNCEncoding { switch (encoding) { case RFB_ENCODINGS.RAW: return 'raw'; case RFB_ENCODINGS.COPY_RECT: return 'copyrect'; case RFB_ENCODINGS.RRE: return 'rre'; case RFB_ENCODINGS.HEXTILE: return 'hextile'; case RFB_ENCODINGS.TRLE: return 'trle'; case RFB_ENCODINGS.ZRLE: return 'zrle'; case RFB_ENCODINGS.TIGHT: return 'tight'; case RFB_ENCODINGS.ULTRA: return 'ultra'; case RFB_ENCODINGS.ZLIBHEX: return 'zlibhex'; case RFB_ENCODINGS.JPEG: return 'jpeg'; case RFB_ENCODINGS.JRLE: return 'jrle'; case RFB_ENCODINGS.CURSOR: return 'cursor'; case RFB_ENCODINGS.DESKTOP_SIZE: return 'desktopsize'; case RFB_ENCODINGS.LAST_RECT: return 'lastrect'; case RFB_ENCODINGS.PSEUDO_CONTINUOUS_UPDATES: return 'continuous'; case RFB_ENCODINGS.PSEUDO_FENCE: return 'fence'; case RFB_ENCODINGS.PSEUDO_X_CURSOR: return 'x11cursor'; default: return 'raw'; } } private handleDesktopResize(width: number, height: number): void { this.framebufferWidth = width; this.framebufferHeight = height; if (this.session) { this.session.framebufferInfo.width = width; this.session.framebufferInfo.height = height; } this.logger.debug(`Desktop resized to ${width}x${height}`); this.emit('desktopResize', { width, height }); } private handleCursorUpdate( x: number, y: number, width: number, height: number, data: Buffer ): void { this.emit('cursorUpdate', { x, y, width, height, data }); } private handleSetColourMapEntries(): void { // Color map is rarely used in modern VNC implementations this.logger.debug('Received SetColourMapEntries message'); // Skip the message for now this.receiveBuffer = this.receiveBuffer.slice(1); } private handleBell(): void { this.logger.debug('Received Bell message'); this.emit('bell'); if (this.options.bellCommand) { // Execute custom bell command const { spawn } = require('child_process'); spawn(this.options.bellCommand, [], { detached: true, stdio: 'ignore' }); } // Remove bell message from buffer this.receiveBuffer = this.receiveBuffer.slice(1); } private handleServerCutText(): void { if (this.receiveBuffer.length < 8) return; const length = this.receiveBuffer.readUInt32BE(4); if (this.receiveBuffer.length < 8 + length) return; const text = this.receiveBuffer.slice(8, 8 + length).toString('utf8'); this.receiveBuffer = this.receiveBuffer.slice(8 + length); this.clipboardData = text; // Create clipboard sync event const clipboardSync: VNCClipboardSync = { sessionId: this.connectionId, direction: 'to_client', contentType: 'text', content: text, timestamp: new Date(), size: length, }; this.logger.debug(`Received clipboard data: ${text.length} characters`); this.emit('clipboardSync', clipboardSync); if (this.session) { this.session.statistics.clipboardTransfers++; } } private handleResizeFrameBuffer(): void { // Extended desktop size pseudo-encoding this.logger.debug('Received ResizeFrameBuffer message'); // Implementation depends on the specific pseudo-encoding this.receiveBuffer = this.receiveBuffer.slice(1); } /** * Client input methods */ public async sendKeyEvent( key: number | string, down: boolean ): Promise<void> { if (!this.isAuthenticated) { throw new Error('Not authenticated'); } const keyCode = typeof key === 'string' ? this.mapKeyToCode(key) : key; const buffer = Buffer.allocUnsafe(8); buffer.writeUInt8(RFB_CLIENT_MESSAGES.KEY_EVENT, 0); buffer.writeUInt8(down ? 1 : 0, 1); buffer.writeUInt16BE(0, 2); // padding buffer.writeUInt32BE(keyCode, 4); await this.writeData(buffer); if (this.session) { this.session.statistics.keyboardEvents++; } // Create key event const keyEvent: VNCKeyEvent = { messageType: RFB_CLIENT_MESSAGES.KEY_EVENT, downFlag: down, key: keyCode, timestamp: new Date(), }; this.emit('keyEvent', keyEvent); } private mapKeyToCode(key: string): number { // Check if it's a named key const namedKey = (KEY_MAPPINGS as any)[key.toUpperCase()]; if (namedKey) { return namedKey; } // For single characters, use their Unicode code point if (key.length === 1) { return key.charCodeAt(0); } // Default to space return 0x20; } public async sendPointerEvent( x: number, y: number, buttonMask: number ): Promise<void> { if (!this.isAuthenticated) { throw new Error('Not authenticated'); } const buffer = Buffer.allocUnsafe(6); buffer.writeUInt8(RFB_CLIENT_MESSAGES.POINTER_EVENT, 0); buffer.writeUInt8(buttonMask, 1); buffer.writeUInt16BE(x, 2); buffer.writeUInt16BE(y, 4); await this.writeData(buffer); if (this.session) { this.session.statistics.mouseEvents++; } // Create pointer event const pointerEvent: VNCPointerEvent = { messageType: RFB_CLIENT_MESSAGES.POINTER_EVENT, buttonMask, x, y, timestamp: new Date(), }; this.emit('pointerEvent', pointerEvent); } public async sendClientCutText(text: string): Promise<void> { if (!this.isAuthenticated) { throw new Error('Not authenticated'); } const textBuffer = Buffer.from(text, 'utf8'); const buffer = Buffer.allocUnsafe(8 + textBuffer.length); buffer.writeUInt8(RFB_CLIENT_MESSAGES.CLIENT_CUT_TEXT, 0); buffer.writeUInt8(0, 1); // padding buffer.writeUInt8(0, 2); // padding buffer.writeUInt8(0, 3); // padding buffer.writeUInt32BE(textBuffer.length, 4); textBuffer.copy(buffer, 8); await this.writeData(buffer); this.clipboardData = text; // Create clipboard sync event const clipboardSync: VNCClipboardSync = { sessionId: this.connectionId, direction: 'to_server', contentType: 'text', content: text, timestamp: new Date(), size: textBuffer.length, }; this.emit('clipboardSync', clipboardSync); if (this.session) { this.session.statistics.clipboardTransfers++; } } public async requestFramebufferUpdate( x: number, y: number, width: number, height: number, incremental: boolean ): Promise<void> { if (!this.isAuthenticated) { throw new Error('Not authenticated'); } const buffer = Buffer.allocUnsafe(10); buffer.writeUInt8(RFB_CLIENT_MESSAGES.FRAMEBUFFER_UPDATE_REQUEST, 0); buffer.writeUInt8(incremental ? 1 : 0, 1); buffer.writeUInt16BE(x, 2); buffer.writeUInt16BE(y, 4); buffer.writeUInt16BE(width, 6); buffer.writeUInt16BE(height, 8); await this.writeData(buffer); } /** * File transfer methods */ public async uploadFile( localPath: string, remotePath: string ): Promise<VNCFileTransfer> { if (!this.options.enableFileTransfer) { throw new Error('File transfer is disabled'); } const transferId = `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const fileTransfer: VNCFileTransfer = { transferId, sessionId: this.connectionId, direction: 'upload', localPath, remotePath, fileSize: 0, transferredBytes: 0, progress: 0, speed: 0, status: 'queued', startTime: new Date(), }; this.activeFileTransfers.set(transferId, fileTransfer); // Start file transfer (implementation would depend on VNC server extension) this.logger.info(`Starting file upload: ${localPath} -> ${remotePath}`); return fileTransfer; } public async downloadFile( remotePath: string, localPath: string ): Promise<VNCFileTransfer> { if (!this.options.enableFileTransfer) { throw new Error('File transfer is disabled'); } const transferId = `download-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const fileTransfer: VNCFileTransfer = { transferId, sessionId: this.connectionId, direction: 'download', localPath, remotePath, fileSize: 0, transferredBytes: 0, progress: 0, speed: 0, status: 'queued', startTime: new Date(), }; this.activeFileTransfers.set(transferId, fileTransfer); // Start file transfer (implementation would depend on VNC server extension) this.logger.info(`Starting file download: ${remotePath} -> ${localPath}`); return fileTransfer; } /** * Session recording methods */ private async startSessionRecording(): Promise<void> { if (!this.options.recordSession || !this.options.recordingPath) { return; } const recordingDir = path.dirname(this.options.recordingPath); if (!fs.existsSync(recordingDir)) { fs.mkdirSync(recordingDir, { recursive: true }); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `vnc-session-${timestamp}.${this.options.recordingFormat || 'fbs'}`; const filepath = path.join(recordingDir, filename); this.recordingStream = fs.createWriteStream(filepath); this.recordingStartTime = new Date(); if (this.session) { this.session.recording = { active: true, startTime: this.recordingStartTime, filePath: filepath, format: this.options.recordingFormat || 'fbs', fileSize: 0, }; } // Write FBS header if (this.options.recordingFormat === 'fbs') { this.writeFBSHeader(); } this.logger.info(`Session recording started: ${filepath}`); } private writeFBSHeader(): void { if (!this.recordingStream || !this.serverInit) return; // FBS format header const header = Buffer.alloc(12); header.write('FBS 001.000\n', 0, 12); this.recordingStream.write(header); // Write server initialization data const serverInitBuffer = Buffer.alloc( 24 + this.serverInit.desktopName.length ); serverInitBuffer.writeUInt16BE(this.serverInit.framebufferWidth, 0); serverInitBuffer.writeUInt16BE(this.serverInit.framebufferHeight, 2); const pf = this.serverInit.pixelFormat; serverInitBuffer.writeUInt8(pf.bitsPerPixel, 4); serverInitBuffer.writeUInt8(pf.depth, 5); serverInitBuffer.writeUInt8(pf.bigEndianFlag ? 1 : 0, 6); serverInitBuffer.writeUInt8(pf.trueColorFlag ? 1 : 0, 7); serverInitBuffer.writeUInt16BE(pf.redMax, 8); serverInitBuffer.writeUInt16BE(pf.greenMax, 10); serverInitBuffer.writeUInt16BE(pf.blueMax, 12); serverInitBuffer.writeUInt8(pf.redShift, 14); serverInitBuffer.writeUInt8(pf.greenShift, 15); serverInitBuffer.writeUInt8(pf.blueShift, 16); serverInitBuffer.writeUInt32BE(this.serverInit.desktopName.length, 20); Buffer.from(this.serverInit.desktopName, 'utf8').copy(serverInitBuffer, 24); this.recordingStream.write(serverInitBuffer); } private recordFramebufferUpdate(update: VNCFramebufferUpdate): void { if (!this.recordingStream) return; // Calculate timestamp since recording start const timestamp = Date.now() - (this.recordingStartTime?.getTime() || 0); // Write timestamp (4 bytes) const timestampBuffer = Buffer.allocUnsafe(4); timestampBuffer.writeUInt32BE(timestamp, 0); this.recordingStream.write(timestampBuffer); // Write message type and data const messageBuffer = Buffer.allocUnsafe(4); messageBuffer.writeUInt8(RFB_SERVER_MESSAGES.FRAMEBUFFER_UPDATE, 0); messageBuffer.writeUInt8(0, 1); // padding messageBuffer.writeUInt16BE(update.rectangles.length, 2); this.recordingStream.write(messageBuffer); // Write rectangles for (const rect of update.rectangles) { const rectBuffer = Buffer.allocUnsafe(12); rectBuffer.writeUInt16BE(rect.x, 0); rectBuffer.writeUInt16BE(rect.y, 2); rectBuffer.writeUInt16BE(rect.width, 4); rectBuffer.writeUInt16BE(rect.height, 6); const encodingNumber = this.mapEncodingStringToNumber(rect.encoding); rectBuffer.writeInt32BE(encodingNumber, 8); this.recordingStream.write(rectBuffer); this.recordingStream.write(rect.data); } if (this.session?.recording) { this.session.recording.fileSize = this.recordingStream.bytesWritten || 0; } } private mapEncodingStringToNumber(encoding: VNCEncoding): number { switch (encoding) { case 'raw': return RFB_ENCODINGS.RAW; case 'copyrect': return RFB_ENCODINGS.COPY_RECT; case 'rre': return RFB_ENCODINGS.RRE; case 'hextile': return RFB_ENCODINGS.HEXTILE; case 'trle': return RFB_ENCODINGS.TRLE; case 'zrle': return RFB_ENCODINGS.ZRLE; case 'tight': return RFB_ENCODINGS.TIGHT; case 'ultra': return RFB_ENCODINGS.ULTRA; case 'zlibhex': return RFB_ENCODINGS.ZLIBHEX; case 'jpeg': return RFB_ENCODINGS.JPEG; case 'jrle': return RFB_ENCODINGS.JRLE; case 'cursor': return RFB_ENCODINGS.CURSOR; case 'desktopsize': return RFB_ENCODINGS.DESKTOP_SIZE; case 'lastrect': return RFB_ENCODINGS.LAST_RECT; case 'continuous': return RFB_ENCODINGS.PSEUDO_CONTINUOUS_UPDATES; case 'fence': return RFB_ENCODINGS.PSEUDO_FENCE; case 'x11cursor': return RFB_ENCODINGS.PSEUDO_X_CURSOR; case 'richcursor': return RFB_ENCODINGS.CURSOR; default: return RFB_ENCODINGS.RAW; } } /** * Performance and metrics methods */ private updateNetworkMetrics(): void { const now = Date.now(); const timeDiff = now - this.performanceMetrics.timestamp.getTime(); if (timeDiff > 1000) { // Update every second this.performanceMetrics.bandwidth = (this.bytesReceived * 8) / (timeDiff / 1000); // bits per second this.performanceMetrics.networkIO = this.bytesReceived + this.bytesSent; this.performanceMetrics.timestamp = new Date(); } } private updateFrameMetrics(): void { const now = Date.now(); const frameDuration = now - this.lastFrameTime; if (this.lastFrameTime > 0) { this.performanceMetrics.avgFrameTime = frameDuration; this.performanceMetrics.frameRate = 1000 / frameDuration; } this.lastFrameTime = now; this.performanceMetrics.pixelChanges++; // Simplified metric } public getPerformanceMetrics(): VNCPerformanceMetrics { return { ...this.performanceMetrics }; } public getServerCapabilities(): VNCCapabilities { return { protocolVersions: ['3.3', '3.7', '3.8'], maxResolution: { width: 16384, height: 16384 }, supportedEncodings: Array.from(this.supportedEncodings.keys()).map((k) => this.mapEncodingNumberToString(k) ), supportedSecurityTypes: this.securityTypes.map((s) => this.mapSecurityType(s) ), supportedPixelFormats: { bitsPerPixel: [8, 16, 32], depths: [8, 16, 24], colorModes: ['truecolor', 'colormap'], }, cursorShapeUpdates: this.options.enableCursorShapeUpdates || false, desktopResize: this.options.autoResize || false, continuousUpdates: false, fileTransfer: this.options.enableFileTransfer || false, clipboardTransfer: this.options.enableClipboard || false, audio: false, tightVNCExtensions: this.options.enableTightVNCExtensions || false, ultraVNCExtensions: this.options.enableUltraVNCExtensions || false, realVNCExtensions: this.options.enableRealVNCExtensions || false, appleRemoteDesktop: this.options.enableAppleRemoteDesktop || false, tigervncExtensions: this.config.enableTigerVNCExtensions, turbovncExtensions: this.config.enableTurboVNCExtensions, compressionSupport: this.config.enableCompression, jpegSupport: this.config.enableJPEG, multiMonitorSupport: (this.options.monitors?.length || 0) > 1, tlsSupport: this.encryptionEnabled, vencryptSupport: this.selectedSecurity === RFB_SECURITY_TYPES.VENCRYPT, saslSupport: false, x509Support: false, }; } /** * Utility methods */ private async writeData(data: Buffer): Promise<void> { return new Promise((resolve, reject) => { if (!this.socket) { reject(new Error('Socket not connected')); return; } this.socket.write(data, (error) => { if (error) { reject(error); } else { this.bytesSent += data.length; resolve(); } }); }); } /** * Disconnect from VNC server */ public async disconnect(): Promise<void> { this.logger.info('Disconnecting from VNC server'); if (this.recordingStream) { this.recordingStream.end(); this.recordingStream = undefined; if (this.session?.recording) { this.session.recording.active = false; } } if (this.socket) { this.socket.destroy(); this.socket = undefined; } if (this.repeaterConnection) { this.repeaterConnection.destroy(); this.repeaterConnection = undefined; } this.isConnected = false; this.isAuthenticated = false; if (this.session) { this.session.status = 'disconnected'; } this.emit('disconnected'); } /** * Handle server message - main message processing entry point */ private handleServerMessage(data: Buffer): void { // This method is set as the messageHandler after authentication this.processServerMessage(); } // BaseProtocol required methods async initialize(): Promise<void> { if (this.isInitialized) return; this.logger.info('Initializing VNC Protocol'); try { // Check VNC tool availability await this.checkVNCAvailability(); this.isInitialized = true; this.logger.info('VNC Protocol initialized successfully'); } catch (error) { this.logger.error('Failed to initialize VNC Protocol:', error); throw error; } } async createSession(options: SessionOptions): Promise<ConsoleSession> { if (!this.isInitialized) { await this.initialize(); } const sessionId = this.generateSessionId(); const vncOptions = { ...this.defaultOptions, ...(options.vncOptions || {}), }; // Handle different VNC connection modes let command: string; let args: string[]; if (vncOptions.mode === 'viewer') { // VNC Viewer mode - connect to existing VNC server const viewerResult = this.buildVNCViewerCommand(vncOptions); command = viewerResult.command; args = viewerResult.args; } else if (vncOptions.mode === 'server') { // VNC Server mode - start VNC server const serverResult = this.buildVNCServerCommand(vncOptions); command = serverResult.command; args = serverResult.args; } else { // Direct VNC protocol connection const directResult = this.buildVNCDirectCommand(vncOptions); command = directResult.command; args = directResult.args; } const session = await this.createSessionWithTypeDetection(sessionId, { ...options, command, args, }); // Create VNC-specific session tracking const vncSession: VNCSession = { sessionId, connectionId: sessionId, host: vncOptions.host || 'localhost', port: vncOptions.port || 5900, protocolVersion: vncOptions.rfbProtocolVersion || '3.8', securityType: vncOptions.authMethod || 'vencrypt', sharedConnection: vncOptions.sharedConnection || true, viewOnlyMode: vncOptions.viewOnly || false, framebufferInfo: { width: vncOptions.initialWidth || 1024, height: vncOptions.initialHeight || 768, pixelFormat: { bitsPerPixel: vncOptions.pixelFormat?.bitsPerPixel || 32, depth: vncOptions.pixelFormat?.depth || 24, bigEndianFlag: vncOptions.pixelFormat?.bigEndianFlag || false, trueColorFlag: vncOptions.pixelFormat?.trueColorFlag || true, redMax: vncOptions.pixelFormat?.redMax || 255, greenMax: vncOptions.pixelFormat?.greenMax || 255, blueMax: vncOptions.pixelFormat?.blueMax || 255, redShift: vncOptions.pixelFormat?.redShift || 16, greenShift: vncOptions.pixelFormat?.greenShift || 8, blueShift: vncOptions.pixelFormat?.blueShift || 0, }, }, supportedEncodings: vncOptions.supportedEncodings || [ 'zrle', 'hextile', 'raw', ], serverCapabilities: { cursorShapeUpdates: vncOptions.enableCursorShapeUpdates || false, richCursor: vncOptions.enableRichCursor || false, desktopResize: vncOptions.autoResize || false, continuousUpdates: false, fence: false, fileTransfer: vncOptions.enableFileTransfer || false, clipboardTransfer: vncOptions.enableClipboard || false, audio: false, }, status: 'connecting', connectionTime: new Date(), statistics: { bytesReceived: 0, bytesSent: 0, framebufferUpdates: 0, keyboardEvents: 0, mouseEvents: 0, clipboardTransfers: 0, fileTransfers: 0, avgFrameRate: 0, bandwidth: 0, compression: 1.0, latency: 0, }, errorCount: 0, warnings: [], monitors: vncOptions.monitors || [], metadata: { mode: vncOptions.mode }, }; this.vncSessions.set(sessionId, vncSession); this.logger.info( `VNC session created: ${sessionId} (${vncOptions.mode || 'direct'})` ); return session; } async executeCommand( sessionId: string, command: string, args?: string[] ): Promise<void> { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const vncSession = this.vncSessions.get(sessionId); const process = this.vncProcesses.get(sessionId); if (process && !process.killed) { // Send commands to VNC process stdin if available if (process.stdin && process.stdin.writable) { const commandLine = args ? `${command} ${args.join(' ')}` : command; process.stdin.write(`${commandLine}\n`); if (vncSession) { vncSession.statistics.keyboardEvents++; } } } this.emit('commandExecuted', { sessionId, command, args, timestamp: new Date(), }); } async sendInput(sessionId: string, input: string): Promise<void> { const session = this.sessions.get(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const vncSession = this.vncSessions.get(sessionId); const process = this.vncProcesses.get(sessionId); if (process && !process.killed && process.stdin && process.stdin.writable) { process.stdin.write(input); if (vncSession) { vncSession.statistics.keyboardEvents++; } } // Also handle VNC-specific key events for direct connections if (vncSession?.status === 'connected') { for (const char of input) { const keycode = char.charCodeAt(0); await this.sendKeyEvent(keycode, true); // key down await this.sendKeyEvent(keycode, false); // key up } } } async closeSession(sessionId: string): Promise<void> { const session = this.sessions.get(sessionId); if (!session) { this.logger.warn(`Attempted to close non-existent session: ${sessionId}`); return; } try { // Close VNC-specific resources const vncSession = this.vncSessions.get(sessionId); if (vncSession) { vncSession.status = 'disconnected'; } // Terminate VNC process const process = this.vncProcesses.get(sessionId); if (process && !process.killed) { process.kill('SIGTERM'); // Force kill after 5 seconds setTimeout(() => { if (!process.killed) { process.kill('SIGKILL'); } }, 5000); } // Clean up viewer processes const viewer = this.vncViewers.get(sessionId); if (viewer && !viewer.process.killed) { viewer.process.kill('SIGTERM'); setTimeout(() => { if (!viewer.process.killed) { viewer.process.kill('SIGKILL'); } }, 5000); } // Clean up maps this.vncSessions.delete(sessionId); this.vncProcesses.delete(sessionId); this.vncViewers.delete(sessionId); this.sessions.delete(sessionId); this.outputBuffers.delete(sessionId); session.status = 'terminated'; session.endedAt = new Date(); this.logger.info(`VNC session closed: ${sessionId}`); this.emit('sessionClosed', sessionId); } catch (error) { this.logger.error(`Error closing VNC session ${sessionId}:`, error); throw error; } } async dispose(): Promise<void> { this.logger.info('Disposing VNC Protocol'); // Close all active sessions const sessionIds = Array.from(this.sessions.keys()); await Promise.all(sessionIds.map((id) => this.closeSession(id))); // Disconnect any active VNC connections if (this.socket) { this.socket.destroy(); this.socket = undefined; } if (this.repeaterConnection) { this.repeaterConnection.destroy(); this.repeaterConnection = undefined; } this.removeAllListeners(); this.isInitialized = false; this.logger.info('VNC Protocol disposed'); } /** * VNC-specific helper methods for BaseProtocol integration */ private generateSessionId(): string { return `vnc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } private generateConnectionId(): string { return `vnc-conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } private async checkVNCAvailability(): Promise<void> { // Check for VNC tools availability const tools = [ 'vncviewer', 'tigervnc', 'tightvncviewer', 'realvnc', 'ultravnc', ]; let availableTools = 0; for (const tool of tools) { try { await new Promise<void>((resolve, reject) => { const process = spawn(tool, ['--version'], { stdio: 'pipe' }); process.on('close', (code) => { if (code === 0 || code === 1) { // Some tools return 1 for --version availableTools++; this.healthStatus.dependencies[tool] = { available: true, version: 'detected', }; } resolve(); }); process.on('error', () => resolve()); // Ignore errors, tool not available setTimeout(() => { process.kill(); resolve(); }, 3000); }); } catch { // Tool not available } } if (availableTools === 0) { this.logger.warn( 'No VNC tools detected, some functionality may be limited' ); } else { this.logger.debug(`Detected ${availableTools} VNC tools`); } } private buildVNCViewerCommand(options: Partial<VNCConnectionOptions>): { command: string; args: string[]; } { // Determine best VNC viewer const viewers = [ { cmd: 'vncviewer', name: 'TigerVNC Viewer' }, { cmd: 'tigervnc', name: 'TigerVNC' }, { cmd: 'tightvncviewer', name: 'TightVNC Viewer' }, { cmd: 'realvnc', name: 'RealVNC Viewer' }, { cmd: 'ultravnc', name: 'UltraVNC' }, ]; const command = options.vncViewer || 'vncviewer'; const args: string[] = []; // Connection target const target = `${options.host || 'localhost'}:${options.port || 5900}`; args.push(target); // Common VNC viewer options if (options.viewOnly) { args.push('-ViewOnly'); } if (options.sharedConnection) { args.push('-Shared'); } if (options.password) { args.push('-passwd', options.password); } if (options.qualityLevel !== undefined) { args.push('-quality', options.qualityLevel.toString()); } if (options.compressionLevel !== undefined) { args.push('-compresslevel', options.compressionLevel.toString()); } if (options.enableJPEGCompression) { args.push('-jpeg'); } if (options.cursorMode) { args.push('-cursor', options.cursorMode); } // Full screen mode if (options.fullScreen) { args.push('-fullscreen'); } // Scaling if (options.scaleFactor) { args.push('-scale', options.scaleFactor.toString()); } return { command, args }; } private buildVNCServerCommand(options: Partial<VNCConnectionOptions>): { command: string; args: string[]; } { // Determine VNC server to use const servers = ['x11vnc', 'tigervnc', 'tightvncserver', 'vncserver']; const command = options.vncServer || 'x11vnc'; const args: string[] = []; if (command === 'x11vnc') { // x11vnc server options args.push('-display', options.display || ':0'); args.push('-rfbport', (options.port || 5900).toString()); if (options.password) { args.push('-passwd', options.password); } if (options.viewOnly) { args.push('-viewonly'); } if (options.sharedConnection) { args.push('-shared'); } if (options.enableFileTransfer) { args.push('-ultrafilexfer'); } if (options.enableClipboard) { args.push('-clipboard'); } // Security options if (options.allowInsecure) { args.push('-nopw'); } // Performance options if (options.compressionLevel !== undefined) { args.push('-compress', options.compressionLevel.toString()); } // Daemon mode if (options.daemonMode) { args.push('-forever', '-loop'); } } else if (command.includes('tigervnc') || command === 'vncserver') { // TigerVNC/standard vncserver options if (options.geometry) { args.push('-geometry', options.geometry); } else { args.push('-geometry', '1024x768'); } if (options.depth) { args.push('-depth', options.depth.toString()); } if (options.port) { args.push('-rfbport', options.port.toString()); } // Display number (e.g., :1, :2) const displayNum = options.display || ':1'; args.push(displayNum); } return { command, args }; } private buildVNCDirectCommand(options: Partial<VNCConnectionOptions>): { command: string; args: string[]; } { // For direct VNC protocol connections, we can use netcat or custom connection tools const command = 'nc'; // netcat for basic TCP connection const args = [ options.host || 'localhost', (options.port || 5900).toString(), ]; if (options.timeout) { args.unshift('-w', (options.timeout / 1000).toString()); } return { command, args }; } /** * Enhanced createSessionWithTypeDetection wrapper for VNC-specific session creation */ protected async doCreateSession( sessionId: string, options: SessionOptions, sessionState: SessionState ): Promise<ConsoleSession> { const vncOptions = { ...this.defaultOptions, ...(options.vncOptions || {}), }; let command: string; let args: string[]; if (vncOptions.mode === 'viewer') { const result = this.buildVNCViewerCommand(vncOptions); command = result.command; args = result.args; } else if (vncOptions.mode === 'server') { const result = this.buildVNCServerCommand(vncOptions); command = result.command; args = result.args; } else { const result = this.buildVNCDirectCommand(vncOptions); command = result.command; args = result.args; } const spawnOptions: SpawnOptionsWithoutStdio = { cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.environment }, stdio: ['pipe', 'pipe', 'pipe'], }; this.logger.debug(`Starting VNC process: ${command} ${args.join(' ')}`); const childProcess = spawn(command, args, spawnOptions); this.vncProcesses.set(sessionId, childProcess); // Handle process events childProcess.on('spawn', () => { this.logger.debug( `VNC process spawned for session ${sessionId} (PID: ${childProcess.pid})` ); }); childProcess.on('error', (error) => { this.logger.error(`VNC process error for session ${sessionId}:`, error); this.emit('processError', { sessionId, error }); }); childProcess.on('exit', (code, signal) => { this.logger.debug( `VNC process exited for session ${sessionId}: code=${code}, signal=${signal}` ); this.vncProcesses.delete(sessionId); const vncSession = this.vncSessions.get(sessionId); if (vncSession) { vncSession.status = 'disconnected'; } }); // Handle stdout/stderr if (childProcess.stdout) { childProcess.stdout.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stdout', data: data.toString(), timestamp: new Date(), stream: 'stdout', }; const outputBuffer = this.outputBuffers.get(sessionId) || []; outputBuffer.push(output); this.outputBuffers.set(sessionId, outputBuffer); this.emit('output', output); }); } if (childProcess.stderr) { childProcess.stderr.on('data', (data) => { const output: ConsoleOutput = { sessionId, type: 'stderr', data: data.toString(), timestamp: new Date(), stream: 'stderr', }; const outputBuffer = this.outputBuffers.get(sessionId) || []; outputBuffer.push(output); this.outputBuffers.set(sessionId, outputBuffer); this.emit('output', output); }); } // Create and return ConsoleSession const session: ConsoleSession = { id: sessionId, command, args, cwd: options.cwd || process.cwd(), env: { ...process.env, ...options.environment }, createdAt: new Date(), pid: childProcess.pid, status: 'running', type: 'vnc', vncOptions: vncOptions as VNCConnectionOptions, executionState: 'executing', activeCommands: new Map(), }; return session; } // Compatibility properties for legacy ProtocolFactory interface public get supportedAuthMethods(): string[] { return this.capabilities.supportedAuthMethods || []; } public get maxConcurrentSessions(): number { return this.capabilities.maxConcurrentSessions || 10; } public get defaultTimeout(): number { return this.capabilities.defaultTimeout || 30000; } public async getOutput( sessionId: string, since?: Date ): Promise<ConsoleOutput[]> { const outputs = this.outputBuffers.get(sessionId) || []; const filteredOutputs = since ? outputs.filter((output) => output.timestamp > since) : outputs; return filteredOutputs; } public async getHealthStatus(): Promise<ProtocolHealthStatus> { this.healthStatus.lastChecked = new Date(); this.healthStatus.metrics.activeSessions = this.sessions.size; this.healthStatus.isHealthy = this.isInitialized; // Check for any failed sessions let hasErrors = false; for (const [sessionId, session] of this.sessions) { if (session.status === 'failed' || session.status === 'crashed') { hasErrors = true; break; } } if ( hasErrors && !this.healthStatus.errors.includes('Some VNC sessions have errors') ) { this.healthStatus.errors.push('Some VNC sessions have errors'); } return { ...this.healthStatus }; } } // Export VNC-specific error types export class VNCProtocolError extends Error { constructor( message: string, public code?: string ) { super(message); this.name = 'VNCProtocolError'; } } export class VNCAuthenticationError extends VNCProtocolError { constructor(message: string) { super(message, 'VNC_AUTH_ERROR'); this.name = 'VNCAuthenticationError'; } } export class VNCConnectionError extends VNCProtocolError { constructor(message: string) { super(message, 'VNC_CONN_ERROR'); this.name = 'VNCConnectionError'; } } export class VNCEncryptionError extends VNCProtocolError { constructor(message: string) { super(message, 'VNC_ENCRYPT_ERROR'); this.name = 'VNCEncryptionError'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ooples/mcp-console-automation'

If you have feedback or need assistance with the MCP directory API, please join our Discord server