UNPKG

react-vnc-lib

Version:

A modern, lightweight VNC client library for React, Next.js, Node.js, and Bun with TypeScript support

2,120 lines (2,114 loc) 49.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { VNCClient: () => VNCClient, VNCProtocolUtils: () => VNCProtocolUtils, VNCViewer: () => VNCViewer, default: () => VNCClient, useVNC: () => useVNC }); module.exports = __toCommonJS(index_exports); // src/utils/protocol.ts var VNCProtocolUtils = class { /** * Convert string to Uint8Array */ static stringToUint8Array(str) { return new TextEncoder().encode(str); } /** * Convert Uint8Array to string */ static uint8ArrayToString(arr) { return new TextDecoder().decode(arr); } /** * Read uint8 from buffer at offset */ static readUint8(buffer, offset) { return new DataView(buffer).getUint8(offset); } /** * Read uint16 big endian from buffer at offset */ static readUint16BE(buffer, offset) { return new DataView(buffer).getUint16(offset, false); } /** * Read uint32 big endian from buffer at offset */ static readUint32BE(buffer, offset) { return new DataView(buffer).getUint32(offset, false); } /** * Write uint8 to buffer at offset */ static writeUint8(buffer, offset, value) { new DataView(buffer).setUint8(offset, value); } /** * Write uint16 big endian to buffer at offset */ static writeUint16BE(buffer, offset, value) { new DataView(buffer).setUint16(offset, value, false); } /** * Write uint32 big endian to buffer at offset */ static writeUint32BE(buffer, offset, value) { new DataView(buffer).setUint32(offset, value, false); } /** * Parse VNC server init message */ static parseServerInit(data) { const view = new DataView(data); const width = view.getUint16(0, false); const height = view.getUint16(2, false); const pixelFormat = { bitsPerPixel: view.getUint8(4), depth: view.getUint8(5), bigEndian: view.getUint8(6) === 1, trueColor: view.getUint8(7) === 1, redMax: view.getUint16(8, false), greenMax: view.getUint16(10, false), blueMax: view.getUint16(12, false), redShift: view.getUint8(14), greenShift: view.getUint8(15), blueShift: view.getUint8(16) }; const nameLength = view.getUint32(20, false); const nameBytes = new Uint8Array(data, 24, nameLength); const name = this.uint8ArrayToString(nameBytes); return { width, height, pixelFormat, name }; } /** * Create client init message */ static createClientInit(shared = true) { const buffer = new ArrayBuffer(1); this.writeUint8(buffer, 0, shared ? 1 : 0); return buffer; } /** * Create set pixel format message */ static createSetPixelFormat(pixelFormat) { const buffer = new ArrayBuffer(20); const view = new DataView(buffer); view.setUint8(0, 0); view.setUint8(1, 0); view.setUint8(2, 0); view.setUint8(3, 0); view.setUint8(4, pixelFormat.bitsPerPixel); view.setUint8(5, pixelFormat.depth); view.setUint8(6, pixelFormat.bigEndian ? 1 : 0); view.setUint8(7, pixelFormat.trueColor ? 1 : 0); view.setUint16(8, pixelFormat.redMax, false); view.setUint16(10, pixelFormat.greenMax, false); view.setUint16(12, pixelFormat.blueMax, false); view.setUint8(14, pixelFormat.redShift); view.setUint8(15, pixelFormat.greenShift); view.setUint8(16, pixelFormat.blueShift); return buffer; } /** * Create framebuffer update request */ static createFramebufferUpdateRequest(incremental, x, y, width, height) { const buffer = new ArrayBuffer(10); const view = new DataView(buffer); view.setUint8(0, 3); view.setUint8(1, incremental ? 1 : 0); view.setUint16(2, x, false); view.setUint16(4, y, false); view.setUint16(6, width, false); view.setUint16(8, height, false); return buffer; } /** * Create key event message */ static createKeyEvent(down, key) { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setUint8(0, 4); view.setUint8(1, down ? 1 : 0); view.setUint16(2, 0, false); view.setUint32(4, key, false); return buffer; } /** * Create pointer event message */ static createPointerEvent(buttonMask, x, y) { const buffer = new ArrayBuffer(6); const view = new DataView(buffer); view.setUint8(0, 5); view.setUint8(1, buttonMask); view.setUint16(2, x, false); view.setUint16(4, y, false); return buffer; } /** * Get default pixel format (32-bit RGBA) */ static getDefaultPixelFormat() { return { bitsPerPixel: 32, depth: 24, bigEndian: false, trueColor: true, redMax: 255, greenMax: 255, blueMax: 255, redShift: 0, greenShift: 8, blueShift: 16 }; } }; // src/core/VNCClient.ts var VNCClient = class { constructor(options) { this.ws = null; this.eventHandlers = /* @__PURE__ */ new Map(); this.serverInit = null; this.connectionTimeout = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 3; this.vncState = "version"; this.options = { url: options.url, username: options.username || "", password: options.password || "", viewOnly: options.viewOnly || false, quality: options.quality || 6, compression: options.compression || 2, autoResize: options.autoResize || true, scale: options.scale || 1, timeout: options.timeout || 1e4, debug: options.debug || false }; this.state = { connected: false, connecting: false, error: null, serverName: null, width: 0, height: 0 }; this.pixelFormat = VNCProtocolUtils.getDefaultPixelFormat(); } /** * Connect to VNC server */ async connect() { if (this.state.connecting) { this.log("Connection attempt already in progress, ignoring duplicate request"); return; } if (this.state.connected) { this.log("Already connected, ignoring duplicate connection request"); return; } this.cleanupExistingConnection(); this.setState({ connecting: true, error: null }); this.vncState = "version"; this.emit("connecting"); try { if (!this.isValidWebSocketUrl(this.options.url)) { throw new Error("Invalid WebSocket URL"); } this.log("Attempting to connect to:", this.options.url); this.ws = new WebSocket(this.options.url); this.ws.binaryType = "arraybuffer"; this.setupWebSocketHandlers(); this.setupConnectionTimeout(); return new Promise((resolve, reject) => { const onConnected = () => { this.off("connected", onConnected); this.off("error", onError); this.reconnectAttempts = 0; resolve(); }; const onError = (event) => { this.off("connected", onConnected); this.off("error", onError); reject(new Error(event.data?.message || "Connection failed")); }; this.on("connected", onConnected); this.on("error", onError); }); } catch (error) { this.setState({ connecting: false, error: error.message }); this.emit("error", { message: error.message }); throw error; } } /** * Validate WebSocket URL */ isValidWebSocketUrl(url) { try { const wsUrl = new URL(url); return wsUrl.protocol === "ws:" || wsUrl.protocol === "wss:"; } catch { return false; } } /** * Disconnect from VNC server */ disconnect() { this.log("Disconnecting..."); this.reconnectAttempts = 0; this.vncState = "version"; if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = null; } if (this.ws) { this.ws.onclose = null; this.ws.onerror = null; this.ws.close(1e3, "Manual disconnect"); this.ws = null; } this.setState({ connected: false, connecting: false, error: null, serverName: null, width: 0, height: 0 }); this.emit("disconnected"); } /** * Reset reconnection attempts (useful for manual retry) */ resetReconnectionAttempts() { this.reconnectAttempts = 0; this.log("Reconnection attempts reset"); } /** * Send key event */ sendKeyEvent(event) { if (!this.state.connected || this.options.viewOnly) return; const keyCode = this.keyToVNCKey(event.key, event.code); const message = VNCProtocolUtils.createKeyEvent(event.down, keyCode); this.sendMessage(message); } /** * Send pointer event */ sendPointerEvent(event) { if (!this.state.connected || this.options.viewOnly) return; const message = VNCProtocolUtils.createPointerEvent( event.buttons, Math.floor(event.x / this.options.scale), Math.floor(event.y / this.options.scale) ); this.sendMessage(message); } /** * Request framebuffer update */ requestFramebufferUpdate(incremental = true) { if (!this.state.connected) return; const message = VNCProtocolUtils.createFramebufferUpdateRequest( incremental, 0, 0, this.state.width, this.state.height ); this.sendMessage(message); } /** * Get current connection state */ getState() { return { ...this.state }; } /** * Add event listener */ on(event, handler) { if (!this.eventHandlers.has(event)) { this.eventHandlers.set(event, []); } this.eventHandlers.get(event).push(handler); } /** * Remove event listener */ off(event, handler) { const handlers = this.eventHandlers.get(event); if (handlers) { const index = handlers.indexOf(handler); if (index !== -1) { handlers.splice(index, 1); } } } /** * Emit event */ emit(type, data) { const handlers = this.eventHandlers.get(type); if (handlers) { const event = { type, data }; handlers.forEach((handler) => handler(event)); } if (this.options.debug) { console.log(`[VNC] Event: ${type}`, data); } } /** * Update internal state */ setState(newState) { this.state = { ...this.state, ...newState }; } /** * Setup WebSocket event handlers */ setupWebSocketHandlers() { if (!this.ws) return; this.ws.onopen = () => { this.log("WebSocket opened successfully"); this.handleConnectionOpen(); }; this.ws.onmessage = (event) => { this.handleServerMessage(event.data); }; this.ws.onclose = (event) => { this.log("WebSocket closed", event.code, event.reason); this.handleConnectionClose(event); }; this.ws.onerror = (event) => { this.log("WebSocket error", event); this.handleConnectionError(event); }; } /** * Handle successful WebSocket connection */ handleConnectionOpen() { this.log("WebSocket connection opened, waiting for server version"); this.vncState = "version"; } /** * Handle WebSocket connection close */ handleConnectionClose(event) { const wasConnected = this.state.connected; this.setState({ connected: false, connecting: false }); let errorMessage = ""; switch (event.code) { case 1e3: this.log("Connection closed normally"); break; case 1006: errorMessage = "Connection lost unexpectedly. Server may be unreachable or token expired."; break; case 1002: errorMessage = "Protocol error. Server rejected the connection."; break; case 1003: errorMessage = "Server rejected connection due to invalid data."; break; case 1008: errorMessage = "Connection rejected by policy (CORS, authentication, etc.)"; break; case 1011: errorMessage = "Server encountered an unexpected error."; break; default: errorMessage = `Connection closed with code ${event.code}: ${event.reason || "Unknown reason"}`; } if (errorMessage) { this.log("Connection error:", errorMessage); this.setState({ error: errorMessage }); this.emit("error", { message: errorMessage }); } this.emit("disconnected"); if (wasConnected && this.shouldAttemptReconnect(event.code)) { this.log(`Connection lost (code ${event.code}), attempting reconnection...`); this.attemptReconnect(); } else if (this.reconnectAttempts > 0) { this.log(`Reconnection attempt ${this.reconnectAttempts} failed with code ${event.code}. ${event.code === 1003 ? "This may indicate protocol state issues or server rejection." : ""}`); if (event.code === 1003 || event.code === 1002) { this.log("Resetting reconnection attempts due to protocol error"); this.reconnectAttempts = this.maxReconnectAttempts; } } } /** * Handle WebSocket errors */ handleConnectionError(event) { this.log("WebSocket error details:", event); let message = "WebSocket connection error"; if (event instanceof ErrorEvent) { message = `WebSocket error: ${event.message}`; } else if (event.type === "error") { message = "WebSocket connection failed. Check network connectivity and server availability."; } this.setState({ connecting: false, connected: false, error: message }); this.emit("error", { message }); } /** * Check if we should attempt reconnection */ shouldAttemptReconnect(closeCode) { const retryableCodes = [1006]; return retryableCodes.includes(closeCode) && this.reconnectAttempts < this.maxReconnectAttempts; } /** * Attempt to reconnect */ async attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.log("Max reconnection attempts reached"); return; } this.reconnectAttempts++; const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts - 1), 1e4); this.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); setTimeout(() => { if (!this.state.connected && !this.state.connecting) { this.log("Starting reconnection attempt..."); this.connect().catch((error) => { this.log("Reconnection failed:", error.message); }); } else { this.log("Skipping reconnection - already connected or connecting"); } }, delay); } /** * Setup connection timeout */ setupConnectionTimeout() { this.connectionTimeout = setTimeout(() => { if (this.state.connecting) { this.disconnect(); const message = "Connection timeout"; this.setState({ error: message }); this.emit("error", { message }); } }, this.options.timeout); } /** * Handle VNC protocol version handshake */ handleProtocolVersion() { this.log("Responding to server with VNC protocol version"); const version = "RFB 003.008\n"; this.sendRawMessage(version); } /** * Handle server messages based on VNC protocol state */ handleServerMessage(data) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { this.log("Ignoring message - WebSocket is not open"); return; } this.log(`Received message in state '${this.vncState}', length: ${data.byteLength}`); try { switch (this.vncState) { case "version": this.handleVersionResponse(data); break; case "security": this.handleSecurityResponse(data); break; case "auth": this.handleAuthResponse(data); break; case "init": this.handleServerInit(data); break; case "connected": this.handleProtocolMessage(data); break; default: this.log("Unexpected VNC state:", this.vncState); } } catch (error) { this.log("Error handling server message:", error); this.emit("error", { message: `Protocol error: ${error.message}` }); } } /** * Handle version response from server */ handleVersionResponse(data) { if (data.byteLength >= 12) { const response = new TextDecoder().decode(data); this.log("Received server version:", response.trim()); this.handleProtocolVersion(); this.vncState = "security"; } } /** * Handle security types from server */ handleSecurityResponse(data) { if (data.byteLength >= 2) { const view = new DataView(data); const numSecTypes = view.getUint8(0); if (numSecTypes === 0) { const reasonLength = view.getUint32(1, false); if (data.byteLength >= 5 + reasonLength) { const reason = new TextDecoder().decode(new Uint8Array(data, 5, reasonLength)); throw new Error(`Security handshake failed: ${reason}`); } throw new Error("Security handshake failed: Unknown reason"); } const secTypes = []; for (let i = 0; i < numSecTypes; i++) { if (data.byteLength > 1 + i) { secTypes.push(view.getUint8(1 + i)); } } this.log("Available security types:", secTypes); let chosenSecType = 1; if (this.options.password && secTypes.includes(2)) { chosenSecType = 2; this.log("Using VNC authentication"); } else if (secTypes.includes(1)) { chosenSecType = 1; this.log("Using no authentication"); } else { throw new Error("No supported security type available"); } const response = new ArrayBuffer(1); new DataView(response).setUint8(0, chosenSecType); this.sendMessage(response); if (chosenSecType === 2) { this.vncState = "auth"; } else { this.vncState = "init"; this.sendClientInit(); } } } /** * Handle VNC authentication challenge */ handleAuthResponse(data) { if (data.byteLength === 16) { this.log("Received VNC auth challenge"); if (!this.options.password) { throw new Error("VNC authentication required but no password provided"); } this.log("Using password for VNC auth, length:", this.options.password.length); const challenge = new Uint8Array(data); this.log("Challenge received (hex):", Array.from(challenge).map((b) => b.toString(16).padStart(2, "0")).join(" ")); const encrypted = this.vncEncrypt(this.options.password, challenge); this.log("Encrypted response (hex):", Array.from(encrypted).map((b) => b.toString(16).padStart(2, "0")).join(" ")); this.sendMessage(encrypted.buffer); return; } else if (data.byteLength === 4) { const result = new DataView(data).getUint32(0, false); if (result === 0) { this.log("VNC authentication successful"); this.vncState = "init"; this.sendClientInit(); } else { throw new Error("VNC authentication failed"); } } else if (data.byteLength >= 8) { const view = new DataView(data); const result = view.getUint32(0, false); if (result === 0) { this.log("VNC authentication successful"); this.vncState = "init"; this.sendClientInit(); } else { const reasonLength = view.getUint32(4, false); let reason = "Authentication failed"; if (data.byteLength >= 8 + reasonLength) { const reasonBytes = new Uint8Array(data, 8, reasonLength); reason = new TextDecoder().decode(reasonBytes).replace(/\0+$/, ""); } this.log("VNC authentication failed:", reason); if (this.ws) { this.ws.close(1e3, "Authentication failed"); } throw new Error(`VNC authentication failed: ${reason}`); } } else { this.log("Unexpected auth response length:", data.byteLength); throw new Error(`Unexpected authentication response length: ${data.byteLength}`); } } /** * Send client init message */ sendClientInit() { this.log("Sending client init"); const clientInit = VNCProtocolUtils.createClientInit(true); this.sendMessage(clientInit); } /** * Handle server init message */ handleServerInit(data) { if (data.byteLength >= 24) { this.serverInit = VNCProtocolUtils.parseServerInit(data); this.setState({ connected: true, connecting: false, serverName: this.serverInit.name, width: this.serverInit.width, height: this.serverInit.height }); if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = null; } this.vncState = "connected"; this.log("VNC connection established:", this.serverInit); this.emit("connected"); this.requestFramebufferUpdate(false); } } /** * Handle VNC protocol messages after connection established */ handleProtocolMessage(data) { this.log("Received protocol message, length:", data.byteLength); } /** * VNC DES encryption for authentication * Based on RFC 6143 with bit reversal fix (Errata ID 4951) and proven VNC implementations * Implementation derived from established VNC clients that work with all major VNC servers */ vncEncrypt(password, challenge) { const key = new Uint8Array(8); const passwordBytes = new TextEncoder().encode(password); for (let i = 0; i < 8; i++) { key[i] = i < passwordBytes.length ? passwordBytes[i] : 0; } this.log("Password bytes (before bit reversal):", Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join(" ")); for (let i = 0; i < 8; i++) { let byte = key[i]; byte = (byte & 1) << 7 | (byte & 2) << 5 | (byte & 4) << 3 | (byte & 8) << 1 | (byte & 16) >> 1 | (byte & 32) >> 3 | (byte & 64) >> 5 | (byte & 128) >> 7; key[i] = byte; } this.log("Password bytes (after bit reversal):", Array.from(key).map((b) => b.toString(16).padStart(2, "0")).join(" ")); const result = new Uint8Array(16); const block1 = this.vncDesEncrypt(new Uint8Array(challenge.subarray(0, 8)), key); result.set(block1, 0); const block2 = this.vncDesEncrypt(new Uint8Array(challenge.subarray(8, 16)), key); result.set(block2, 8); return result; } /** * Proper VNC DES encryption implementation * Based on proven VNC implementations and RFC 6143 compliance * This implements the actual DES algorithm that VNC servers expect */ vncDesEncrypt(block, key) { const sBoxes = [ // S1 [ 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13 ], // S2 [ 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9 ], // S3 [ 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12 ], // S4 [ 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14 ], // S5 [ 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3 ], // S6 [ 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13 ], // S7 [ 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12 ], // S8 [ 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11 ] ]; const ip = [ 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8, 57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7 ]; const fp = [ 40, 8, 48, 16, 56, 24, 64, 32, 39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25 ]; const e = [ 32, 1, 2, 3, 4, 5, 4, 5, 6, 7, 8, 9, 8, 9, 10, 11, 12, 13, 12, 13, 14, 15, 16, 17, 16, 17, 18, 19, 20, 21, 20, 21, 22, 23, 24, 25, 24, 25, 26, 27, 28, 29, 28, 29, 30, 31, 32, 1 ]; const p = [ 16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10, 2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25 ]; const pc1 = [ 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4 ]; const pc2 = [ 14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2, 41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32 ]; const shifts = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]; const bytesToBits = (bytes) => { const bits = []; for (const byte of bytes) { for (let i = 7; i >= 0; i--) { bits.push(byte >> i & 1); } } return bits; }; const bitsToBytes = (bits) => { const bytes = new Uint8Array(Math.ceil(bits.length / 8)); for (let i = 0; i < bits.length; i++) { if (bits[i]) { bytes[Math.floor(i / 8)] |= 1 << 7 - i % 8; } } return bytes; }; const permute = (input, table) => { return table.map((pos) => input[pos - 1]); }; const leftShift = (input, shifts2) => { return [...input.slice(shifts2), ...input.slice(0, shifts2)]; }; const blockBits = bytesToBits(block); const keyBits = bytesToBits(key); const ipResult = permute(blockBits, ip); let left = ipResult.slice(0, 32); let right = ipResult.slice(32, 64); const keyPermuted = permute(keyBits, pc1); let c = keyPermuted.slice(0, 28); let d = keyPermuted.slice(28, 56); for (let round = 0; round < 16; round++) { c = leftShift(c, shifts[round]); d = leftShift(d, shifts[round]); const roundKey = permute([...c, ...d], pc2); const expanded = permute(right, e); const xored = expanded.map((bit, i) => bit ^ roundKey[i]); let sBoxOutput = []; for (let s = 0; s < 8; s++) { const chunk = xored.slice(s * 6, (s + 1) * 6); const row = chunk[0] << 1 | chunk[5]; const col = chunk[1] << 3 | chunk[2] << 2 | chunk[3] << 1 | chunk[4]; const sValue = sBoxes[s][row * 16 + col]; for (let i = 3; i >= 0; i--) { sBoxOutput.push(sValue >> i & 1); } } const fResult = permute(sBoxOutput, p); const newRight = left.map((bit, i) => bit ^ fResult[i]); left = right; right = newRight; } const preOutput = [...right, ...left]; const finalResult = permute(preOutput, fp); return bitsToBytes(finalResult); } /** * Send binary message */ sendMessage(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(data); } } /** * Send raw string message as binary data */ sendRawMessage(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { const encoder = new TextEncoder(); const binaryData = encoder.encode(data); this.ws.send(binaryData.buffer); } } /** * Convert keyboard event to VNC key code */ keyToVNCKey(key, code) { const keyMap = { "Backspace": 65288, "Tab": 65289, "Enter": 65293, "Escape": 65307, "Delete": 65535, "ArrowLeft": 65361, "ArrowUp": 65362, "ArrowRight": 65363, "ArrowDown": 65364, " ": 32 }; if (keyMap[key]) { return keyMap[key]; } if (key.length === 1) { return key.charCodeAt(0); } return 0; } /** * Debug logging */ log(...args) { if (this.options.debug) { console.log("[VNC]", ...args); } } /** * Clean up existing connection state without triggering disconnect events */ cleanupExistingConnection() { if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = null; } if (this.ws) { this.ws.onopen = null; this.ws.onmessage = null; this.ws.onclose = null; this.ws.onerror = null; if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { this.ws.close(1e3, "Reconnecting"); } this.ws = null; } this.serverInit = null; this.vncState = "version"; } }; // src/hooks/useVNC.ts var import_react = require("react"); function useVNC(options) { const clientRef = (0, import_react.useRef)(null); const canvasRef = (0, import_react.useRef)(null); const isMountedRef = (0, import_react.useRef)(true); const [state, setState] = (0, import_react.useState)({ connected: false, connecting: false, error: null, serverName: null, width: 0, height: 0 }); const [error, setError] = (0, import_react.useState)(null); const [loading, setLoading] = (0, import_react.useState)(false); (0, import_react.useEffect)(() => { isMountedRef.current = true; const client = new VNCClient(options); clientRef.current = client; const handleEvent = (event) => { if (!isMountedRef.current) return; switch (event.type) { case "connecting": setLoading(true); setError(null); break; case "connected": setLoading(false); setState(client.getState()); break; case "disconnected": setLoading(false); setState(client.getState()); break; case "error": setLoading(false); setError(event.data?.message || "Unknown error"); setState(client.getState()); break; case "framebuffer-update": renderToCanvas(event.data); break; case "resize": setState(client.getState()); resizeCanvas(); break; } }; client.on("connecting", handleEvent); client.on("connected", handleEvent); client.on("disconnected", handleEvent); client.on("error", handleEvent); client.on("framebuffer-update", handleEvent); client.on("resize", handleEvent); if (options.autoConnect) { const connectTimer = setTimeout(() => { if (isMountedRef.current) { connect(); } }, 50); return () => { clearTimeout(connectTimer); isMountedRef.current = false; setTimeout(() => { if (!isMountedRef.current && options.autoDisconnect !== false) { client.disconnect(); } }, 150); }; } return () => { isMountedRef.current = false; setTimeout(() => { if (!isMountedRef.current && options.autoDisconnect !== false) { client.disconnect(); } }, 150); }; }, [options.url, options.autoConnect, options.autoDisconnect]); const connect = (0, import_react.useCallback)(async () => { if (!clientRef.current || !isMountedRef.current) return; if (clientRef.current.getState().connecting || clientRef.current.getState().connected) { return; } try { await clientRef.current.connect(); } catch (err) { if (isMountedRef.current) { setError(err.message); } } }, []); const disconnect = (0, import_react.useCallback)(() => { if (!clientRef.current) return; clientRef.current.disconnect(); }, []); const sendKeyEvent = (0, import_react.useCallback)((event) => { if (!clientRef.current) return; clientRef.current.sendKeyEvent(event); }, []); const sendPointerEvent = (0, import_react.useCallback)((event) => { if (!clientRef.current) return; clientRef.current.sendPointerEvent(event); }, []); const requestUpdate = (0, import_react.useCallback)((incremental = true) => { if (!clientRef.current) return; clientRef.current.requestFramebufferUpdate(incremental); }, []); const renderToCanvas = (0, import_react.useCallback)((imageData) => { const canvas = canvasRef.current; if (!canvas || !imageData) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.putImageData(imageData, 0, 0); }, []); const resizeCanvas = (0, import_react.useCallback)(() => { const canvas = canvasRef.current; if (!canvas || !clientRef.current) return; const clientState = clientRef.current.getState(); if (clientState.width > 0 && clientState.height > 0) { canvas.width = clientState.width; canvas.height = clientState.height; if (options.scale && options.scale !== 1) { canvas.style.width = `${clientState.width * options.scale}px`; canvas.style.height = `${clientState.height * options.scale}px`; } } }, [options.scale]); (0, import_react.useEffect)(() => { if (state.connected && state.width > 0 && state.height > 0) { resizeCanvas(); } }, [state.connected, state.width, state.height, resizeCanvas]); return { client: clientRef.current, state, connect, disconnect, sendKeyEvent, sendPointerEvent, requestUpdate, canvasRef, error, loading }; } // src/components/VNCViewer.tsx var import_react2 = require("react"); var import_jsx_runtime = require("react/jsx-runtime"); var VNCViewer = ({ className = "", style = {}, showStatus = true, showLoading = true, loadingComponent, errorComponent, disableKeyboard = false, disableMouse = false, autoFocus = true, connectButtonText = "Connect", disconnectButtonText = "Disconnect", showCursor = true, ...vncOptions }) => { const { state, connect, disconnect, sendKeyEvent, sendPointerEvent, canvasRef, error, loading } = useVNC(vncOptions); const [isFocused, setIsFocused] = (0, import_react2.useState)(false); const [cursorVisible, setCursorVisible] = (0, import_react2.useState)(true); (0, import_react2.useEffect)(() => { if (!showCursor || !isFocused || !state.connected || disableKeyboard) { setCursorVisible(false); return; } const interval = setInterval(() => { setCursorVisible((prev) => !prev); }, 530); return () => clearInterval(interval); }, [showCursor, isFocused, state.connected, disableKeyboard]); const handleKeyDown = (0, import_react2.useCallback)((event) => { if (disableKeyboard || !state.connected) return; event.preventDefault(); const vncEvent = { key: event.key, code: event.code, down: true, altKey: event.altKey, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, metaKey: event.metaKey }; sendKeyEvent(vncEvent); }, [disableKeyboard, state.connected, sendKeyEvent]); const handleKeyUp = (0, import_react2.useCallback)((event) => { if (disableKeyboard || !state.connected) return; event.preventDefault(); const vncEvent = { key: event.key, code: event.code, down: false, altKey: event.altKey, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, metaKey: event.metaKey }; sendKeyEvent(vncEvent); }, [disableKeyboard, state.connected, sendKeyEvent]); const handleMouseDown = (0, import_react2.useCallback)((event) => { if (disableMouse || !state.connected) return; const rect = event.target.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const vncEvent = { x: Math.floor(x / (vncOptions.scale || 1)), y: Math.floor(y / (vncOptions.scale || 1)), buttons: event.buttons }; sendPointerEvent(vncEvent); }, [disableMouse, state.connected, sendPointerEvent, vncOptions.scale]); const handleMouseUp = (0, import_react2.useCallback)((event) => { if (disableMouse || !state.connected) return; const rect = event.target.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const vncEvent = { x: Math.floor(x / (vncOptions.scale || 1)), y: Math.floor(y / (vncOptions.scale || 1)), buttons: 0 }; sendPointerEvent(vncEvent); }, [disableMouse, state.connected, sendPointerEvent, vncOptions.scale]); const handleMouseMove = (0, import_react2.useCallback)((event) => { if (disableMouse || !state.connected) return; const rect = event.target.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const vncEvent = { x: Math.floor(x / (vncOptions.scale || 1)), y: Math.floor(y / (vncOptions.scale || 1)), buttons: event.buttons }; sendPointerEvent(vncEvent); }, [disableMouse, state.connected, sendPointerEvent, vncOptions.scale]); const handleCanvasFocus = (0, import_react2.useCallback)(() => { setIsFocused(true); }, []); const handleCanvasBlur = (0, import_react2.useCallback)(() => { setIsFocused(false); }, []); (0, import_react2.useEffect)(() => { if (autoFocus && state.connected && canvasRef.current) { canvasRef.current.focus(); } }, [autoFocus, state.connected]); const defaultStyle = { width: "100%", height: "100%", display: "flex", flexDirection: "column", position: "relative", backgroundColor: "#000", color: "#fff", fontFamily: "Arial, sans-serif", fontSize: "14px", ...style }; if (loading && showLoading) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `vnc-viewer vnc-viewer--loading ${className}`, style: defaultStyle, children: loadingComponent || /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vnc-loading", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vnc-loading__text", children: "Connecting to VNC server..." }) }) }); } if (error) { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `vnc-viewer vnc-viewer--error ${className}`, style: defaultStyle, children: errorComponent || /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vnc-error", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vnc-error__text", children: [ "Connection Error: ", error ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { className: "vnc-error__button", onClick: connect, disabled: state.connecting, children: state.connecting ? "Connecting..." : connectButtonText } ) ] }) }); } return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `vnc-viewer vnc-viewer--connected ${className}`, style: defaultStyle, children: [ showStatus && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vnc-status", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vnc-status__info", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vnc-status__text", children: [ "Status: ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `vnc-status__indicator vnc-status__indicator--${state.connected ? "connected" : "disconnected"}`, children: state.connected ? "Connected" : "Disconnected" }), state.serverName && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vnc-status__server", children: [ " - ", state.serverName ] }), state.width > 0 && state.height > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "vnc-status__resolution", children: [ " (", state.width, "x", state.height, ")" ] }) ] }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "vnc-controls", children: !state.connected ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { className: "vnc-controls__button vnc-controls__button--connect", onClick: connect, disabled: state.connecting, children: state.connecting ? "Connecting..." : connectButtonText } ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "button", { className: "vnc-controls__button vnc-controls__button--disconnect", onClick: disconnect, children: disconnectButtonText } ) }) ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "vnc-canvas-container", children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "canvas", { ref: canvasRef, className: `vnc-canvas ${isFocused ? "vnc-canvas--focused" : ""}`, tabIndex: 0, onKeyDown: handleKeyDown, onKeyUp: handleKeyUp, onMouseDown: handleMouseDown, onMouseUp: handleMouseUp, onMouseMove: handleMouseMove, onFocus: handleCanvasFocus, onBlur: handleCanvasBlur, style: { outline: "none", cursor: disableMouse ? "default" : "pointer", border: "1px solid #333", background: "#000", display: "block", maxWidth: "100%", maxHeight: "100%", flex: 1, position: "relative" } } ), showCursor && isFocused && state.connected && !disableKeyboard && /* @__PURE__ */ (0, import_jsx_runtime.jsx)( "div", { className: `vnc-cursor ${cursorVisible ? "vnc-cursor--visible" : "vnc-cursor--hidden"}`, style: { position: "absolute", top: "10px", right: "10px", width: "2px", height: "16px", backgroundColor: "#00ff00", pointerEvents: "none", zIndex: 10 } } ) ] }) ] }); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { VNCClient, VNCProtocolUtils, VNCViewer, useVNC });