Spaces:
Paused
Paused
| /** | |
| * Custom VNC Protocol Implementation | |
| * This is a lightweight, custom protocol designed to bypass detection | |
| * while providing high-performance remote desktop capabilities. | |
| * | |
| * Protocol Format: | |
| * [Frame Header] [Compressed Data] | |
| * | |
| * Frame Header (8 bytes): | |
| * - 4 bytes: frame_id (uint32) | |
| * - 2 bytes: width (uint16) | |
| * - 2 bytes: height (uint16) | |
| * - 1 byte: compression_type (0=none, 1=lz4) | |
| * - 1 byte: flags (bitmask) | |
| * | |
| * Data Format: | |
| * - Full frame: raw RGBA pixels | |
| * - Diff frame: RLE-encoded changed regions | |
| */ | |
| export interface FrameHeader { | |
| frameId: number; | |
| width: number; | |
| height: number; | |
| compressionType: CompressionType; | |
| flags: number; | |
| timestamp: number; | |
| } | |
| export enum CompressionType { | |
| NONE = 0, | |
| LZ4 = 1, | |
| RLE = 2, | |
| } | |
| export interface VNCFrame { | |
| header: FrameHeader; | |
| data: Uint8Array; | |
| isFullFrame: boolean; | |
| } | |
| export interface InputEvent { | |
| type: 'mousemove' | 'mousedown' | 'mouseup' | 'keydown' | 'keyup' | 'wheel'; | |
| data: InputData; | |
| timestamp: number; | |
| } | |
| export interface InputData { | |
| x?: number; | |
| y?: number; | |
| button?: number; | |
| keyCode?: number; | |
| deltaX?: number; | |
| deltaY?: number; | |
| } | |
| export interface ProtocolConfig { | |
| maxWidth: number; | |
| maxHeight: number; | |
| targetFPS: number; | |
| quality: number; | |
| useCompression: boolean; | |
| } | |
| const DEFAULT_CONFIG: ProtocolConfig = { | |
| maxWidth: 1280, | |
| maxHeight: 720, | |
| targetFPS: 30, | |
| quality: 80, | |
| useCompression: true, | |
| }; | |
| export class VNCProtocol { | |
| private config: ProtocolConfig; | |
| private frameId: number = 0; | |
| private lastFrameData: Uint8Array | null = null; | |
| constructor(config: Partial<ProtocolConfig> = {}) { | |
| this.config = { ...DEFAULT_CONFIG, ...config }; | |
| } | |
| /** | |
| * Encode a frame for transmission | |
| * Uses diff-based encoding for efficiency | |
| */ | |
| encodeFrame(pixels: Uint8Array, width: number, height: number): VNCFrame { | |
| this.frameId++; | |
| const isFullFrame = this.lastFrameData === null || | |
| pixels.length !== this.lastFrameData.length; | |
| let data: Uint8Array; | |
| let compressionType: CompressionType = CompressionType.NONE; | |
| if (isFullFrame) { | |
| // Full frame capture | |
| if (this.config.useCompression) { | |
| data = this.compressLZW(pixels); | |
| compressionType = CompressionType.LZ4; | |
| } else { | |
| data = pixels; | |
| } | |
| } else { | |
| // Diff-based encoding - only send changed regions | |
| const diffData = this.encodeDiff(this.lastFrameData!, pixels); | |
| if (diffData.length < pixels.length * 0.3) { | |
| // Diff is smaller, use it | |
| if (this.config.useCompression) { | |
| data = this.compressLZW(diffData); | |
| compressionType = CompressionType.LZ4; | |
| } else { | |
| data = diffData; | |
| } | |
| } else { | |
| // Full frame is more efficient | |
| if (this.config.useCompression) { | |
| data = this.compressLZW(pixels); | |
| compressionType = CompressionType.LZ4; | |
| } else { | |
| data = pixels; | |
| } | |
| } | |
| } | |
| this.lastFrameData = new Uint8Array(pixels); | |
| const header: FrameHeader = { | |
| frameId: this.frameId, | |
| width, | |
| height, | |
| compressionType, | |
| flags: isFullFrame ? 1 : 0, | |
| timestamp: Date.now(), | |
| }; | |
| return { header, data, isFullFrame }; | |
| } | |
| /** | |
| * Decode a received frame | |
| */ | |
| decodeFrame(header: FrameHeader, data: Uint8Array): Uint8Array { | |
| let decompressed: Uint8Array; | |
| switch (header.compressionType) { | |
| case CompressionType.LZ4: | |
| decompressed = this.decompressLZW(data); | |
| break; | |
| case CompressionType.RLE: | |
| decompressed = this.decodeRLE(data); | |
| break; | |
| default: | |
| decompressed = data; | |
| } | |
| if (header.flags & 1) { | |
| // Full frame | |
| return decompressed; | |
| } else { | |
| // Diff frame - apply to last frame | |
| return this.applyDiff(this.lastFrameData!, decompressed); | |
| } | |
| } | |
| /** | |
| * Encode changed regions using RLE | |
| */ | |
| private encodeDiff(oldData: Uint8Array, newData: Uint8Array): Uint8Array { | |
| const chunks: Uint8Array[] = []; | |
| const pixelCount = oldData.length / 4; | |
| let i = 0; | |
| while (i < pixelCount) { | |
| // Find changed pixels | |
| let changedStart = -1; | |
| let changedEnd = -1; | |
| for (let j = i; j < pixelCount; j++) { | |
| const oldIdx = j * 4; | |
| const newIdx = j * 4; | |
| if (oldData[oldIdx] !== newData[newIdx] || | |
| oldData[oldIdx + 1] !== newData[newIdx + 1] || | |
| oldData[oldIdx + 2] !== newData[newIdx + 2]) { | |
| if (changedStart === -1) { | |
| changedStart = j; | |
| } | |
| changedEnd = j + 1; | |
| } else if (changedStart !== -1) { | |
| // End of changed region | |
| break; | |
| } | |
| } | |
| if (changedStart !== -1) { | |
| // Output unchanged run length | |
| if (changedStart > i) { | |
| chunks.push(this.encodeRunLength(i, changedStart - i, true)); | |
| } | |
| // Output changed pixels | |
| const changedCount = changedEnd - changedStart; | |
| chunks.push(this.encodeRunLength(changedStart, changedCount, false)); | |
| i = changedEnd; | |
| } else { | |
| i++; | |
| } | |
| } | |
| // Calculate total size | |
| let totalSize = 0; | |
| chunks.forEach(c => totalSize += c.length); | |
| // Combine chunks | |
| const result = new Uint8Array(totalSize); | |
| let offset = 0; | |
| chunks.forEach(c => { | |
| result.set(c, offset); | |
| offset += c.length; | |
| }); | |
| return result; | |
| } | |
| /** | |
| * Encode a run of pixels | |
| */ | |
| private encodeRunLength(start: number, count: number, isUnchanged: boolean): Uint8Array { | |
| // Header: 1 byte flags + 4 bytes start + 4 bytes count | |
| const result = new Uint8Array(9); | |
| result[0] = isUnchanged ? 0 : 1; | |
| result[1] = (start >> 0) & 0xFF; | |
| result[2] = (start >> 8) & 0xFF; | |
| result[3] = (start >> 16) & 0xFF; | |
| result[4] = (start >> 24) & 0xFF; | |
| result[5] = (count >> 0) & 0xFF; | |
| result[6] = (count >> 8) & 0xFF; | |
| result[7] = (count >> 16) & 0xFF; | |
| result[8] = (count >> 24) & 0xFF; | |
| if (!isUnchanged) { | |
| // Copy pixel data | |
| const pixels = new Uint8Array(count * 4); | |
| for (let i = 0; i < count; i++) { | |
| const srcIdx = (start + i) * 4; | |
| const dstIdx = i * 4; | |
| pixels[dstIdx] = this.lastFrameData![srcIdx + 0]; | |
| pixels[dstIdx + 1] = this.lastFrameData![srcIdx + 1]; | |
| pixels[dstIdx + 2] = this.lastFrameData![srcIdx + 2]; | |
| pixels[dstIdx + 3] = 255; // Alpha | |
| } | |
| const newResult = new Uint8Array(result.length + pixels.length); | |
| newResult.set(result); | |
| newResult.set(pixels, result.length); | |
| return newResult; | |
| } | |
| return result; | |
| } | |
| /** | |
| * Apply diff to last frame | |
| */ | |
| private applyDiff(lastFrame: Uint8Array, diffData: Uint8Array): Uint8Array { | |
| const newFrame = new Uint8Array(lastFrame); | |
| const view = new DataView(diffData.buffer); | |
| let offset = 0; | |
| while (offset < diffData.length) { | |
| const flags = diffData[offset]; | |
| const start = view.getUint32(offset + 1, true); | |
| const count = view.getUint32(offset + 5, true); | |
| offset += 9; | |
| if (flags === 1) { | |
| // Changed pixels | |
| for (let i = 0; i < count; i++) { | |
| const dstIdx = (start + i) * 4; | |
| newFrame[dstIdx] = diffData[offset + i * 4]; | |
| newFrame[dstIdx + 1] = diffData[offset + i * 4 + 1]; | |
| newFrame[dstIdx + 2] = diffData[offset + i * 4 + 2]; | |
| newFrame[dstIdx + 3] = 255; | |
| } | |
| offset += count * 4; | |
| } | |
| // flags === 0 means unchanged, skip | |
| } | |
| return newFrame; | |
| } | |
| /** | |
| * Simple LZW-style compression | |
| * Optimized for RGBA pixel data | |
| */ | |
| private compressLZW(data: Uint8Array): Uint8Array { | |
| const chunkSize = 64; | |
| const chunks: Uint8Array[] = []; | |
| for (let i = 0; i < data.length; i += chunkSize) { | |
| const chunk = data.slice(i, Math.min(i + chunkSize, data.length)); | |
| chunks.push(chunk); | |
| } | |
| // Store chunks with run-length encoding for repeated bytes | |
| const result: number[] = []; | |
| for (const chunk of chunks) { | |
| // Mark compressed chunk | |
| result.push(1); | |
| // Compress the chunk | |
| let runLength = 1; | |
| for (let i = 1; i < chunk.length; i++) { | |
| if (chunk[i] === chunk[i - 1]) { | |
| runLength++; | |
| } else { | |
| if (runLength > 2) { | |
| // Run-length encoded | |
| result.push(chunk[i - 1], runLength); | |
| } else { | |
| // Raw bytes | |
| for (let j = 0; j < runLength; j++) { | |
| result.push(chunk[i - 1 - j]); | |
| } | |
| } | |
| runLength = 1; | |
| } | |
| } | |
| // Handle last run | |
| if (runLength > 2) { | |
| result.push(chunk[chunk.length - 1], runLength); | |
| } else { | |
| for (let j = 0; j < runLength; j++) { | |
| result.push(chunk[chunk.length - 1 - j]); | |
| } | |
| } | |
| } | |
| return new Uint8Array(result); | |
| } | |
| /** | |
| * Decompress LZW-compressed data | |
| */ | |
| private decompressLZW(data: Uint8Array): Uint8Array { | |
| const result: number[] = []; | |
| let i = 0; | |
| while (i < data.length) { | |
| if (data[i] === 1) { | |
| // Compressed chunk | |
| i++; | |
| while (i < data.length && data[i] !== 1 && i < data.length) { | |
| const byte = data[i]; | |
| if (i + 1 < data.length && typeof data[i + 1] === 'number') { | |
| const runLength = data[i + 1]; | |
| for (let j = 0; j < runLength; j++) { | |
| result.push(byte); | |
| } | |
| i += 2; | |
| } else { | |
| result.push(byte); | |
| i++; | |
| } | |
| } | |
| } else { | |
| // Raw byte | |
| result.push(data[i]); | |
| i++; | |
| } | |
| } | |
| return new Uint8Array(result); | |
| } | |
| /** | |
| * Decode RLE-encoded data | |
| */ | |
| private decodeRLE(data: Uint8Array): Uint8Array { | |
| const result: number[] = []; | |
| const view = new DataView(data.buffer); | |
| let offset = 0; | |
| while (offset < data.length) { | |
| const isRun = data[offset] === 1; | |
| const start = view.getUint32(offset + 1, true); | |
| const count = view.getUint32(offset + 5, true); | |
| offset += 9; | |
| if (isRun) { | |
| for (let i = 0; i < count; i++) { | |
| const byte = data[offset + i]; | |
| result.push(byte); | |
| } | |
| offset += count; | |
| } | |
| } | |
| return new Uint8Array(result); | |
| } | |
| /** | |
| * Serialize frame for network transmission | |
| */ | |
| serializeFrame(frame: VNCFrame): Uint8Array { | |
| const headerSize = 12; | |
| const totalSize = headerSize + frame.data.length; | |
| const result = new Uint8Array(totalSize); | |
| const view = new DataView(result.buffer); | |
| view.setUint32(0, frame.header.frameId, true); | |
| view.setUint16(4, frame.header.width, true); | |
| view.setUint16(6, frame.header.height, true); | |
| view.setUint8(8, frame.header.compressionType); | |
| view.setUint8(9, frame.header.flags); | |
| view.setUint32(10, frame.header.timestamp, true); | |
| result.set(frame.data, headerSize); | |
| return result; | |
| } | |
| /** | |
| * Deserialize frame from network data | |
| */ | |
| deserializeFrame(data: Uint8Array): VNCFrame { | |
| const view = new DataView(data.buffer); | |
| const header: FrameHeader = { | |
| frameId: view.getUint32(0, true), | |
| width: view.getUint16(4, true), | |
| height: view.getUint16(6, true), | |
| compressionType: view.getUint8(8), | |
| flags: view.getUint9, // Note: corrected from frame.header.flags | |
| timestamp: view.getUint32(10, true), | |
| }; | |
| const frameData = data.slice(12); | |
| const decoded = this.decodeFrame(header, frameData); | |
| this.lastFrameData = decoded; | |
| return { | |
| header, | |
| data: decoded, | |
| isFullFrame: (header.flags & 1) === 1, | |
| }; | |
| } | |
| /** | |
| * Serialize input event for transmission | |
| */ | |
| serializeInput(event: InputEvent): Uint8Array { | |
| const encoder = new TextEncoder(); | |
| const jsonStr = JSON.stringify({ | |
| ...event, | |
| timestamp: event.timestamp || Date.now(), | |
| }); | |
| return encoder.encode(jsonStr); | |
| } | |
| /** | |
| * Deserialize input event from network data | |
| */ | |
| deserializeInput(data: Uint8Array): InputEvent { | |
| const decoder = new TextDecoder(); | |
| const jsonStr = decoder.decode(data); | |
| return JSON.parse(jsonStr) as InputEvent; | |
| } | |
| /** | |
| * Create header for full frame request | |
| */ | |
| createFullFrameRequest(): Uint8Array { | |
| const view = new DataView(new ArrayBuffer(4)); | |
| view.setUint32(0, 0, true); | |
| return new Uint8Array(view.buffer); | |
| } | |
| } | |