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
JavaScript
"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
});