/** * RustDesk Standalone Web Client - Connection Manager * * Manages WebSocket connections to hbbs (signaling) and hbbr (relay), * handles the full connection lifecycle including registration, * punch hole, relay setup, and key exchange. */ const RDConnection = (() => { // Connection states const STATE = { DISCONNECTED: 'disconnected', CONNECTING: 'connecting', REGISTERING: 'registering', PUNCHING: 'punching', RELAYING: 'relaying', KEY_EXCHANGE: 'key_exchange', AUTHENTICATING: 'authenticating', CONNECTED: 'connected', FAILED: 'failed', CLOSED: 'closed' }; let _state = STATE.DISCONNECTED; let _hbbsWs = null; // WebSocket to hbbs (signaling) let _sessionWs = null; // WebSocket to hbbr/peer (session) let _serverAddr = ''; // Server address (host or host:port) let _useTls = false; // Whether to use wss:// let _serverKey = ''; // Server's Ed25519 public key (for encrypted hbbs) let _myId = ''; // Our registered client ID let _myName = ''; // Our display name let _myUuid = ''; // Our UUID let _targetPeerId = ''; // Remote peer ID we want to connect to let _peerPk = null; // Peer's Ed25519 public key from PunchHoleResponse let _relayUuid = ''; // UUID for relay connection let _hbbsEncrypted = false; // Whether hbbs connection is encrypted let _hbbsSymmetricKey = null; // Symmetric key for hbbs encryption let _hbbsSendSeq = 0; let _hbbsRecvSeq = 0; let _peerPlatform = ''; let _peerVersion = ''; let _passwordCallback = null; // Callback for password input let _2faCallback = null; // Callback for 2FA input let _onPeerInfo = null; // Callback for peer info received let _onVideoFrame = null; // Callback for video frame received let _onAudioFrame = null; // Callback for audio frame received let _onCursorData = null; // Callback for cursor data received let _onCursorPosition = null; // Callback for cursor position received let _onMiscEvent = null; // Callback for misc events received let _onClipboard = null; // Callback for clipboard events let _onClose = null; // Callback for session close let _onStateChange = null; // Callback for state changes let _onError = null; // Callback for errors let _msgBuffer = []; // Buffer for incomplete messages let _serial = 0; // Registration serial number let _keepAliveInterval = null; let _latencyInterval = null; /** * Generate a random 9-digit client ID for web client */ function generateClientId() { const chars = '23456789abcdefghijkmnpqrstuvwxyz'; let id = ''; for (let i = 0; i < 9; i++) { id += chars[Math.floor(Math.random() * chars.length)]; } return id; } /** * Generate a UUID v4 */ function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); } /** * Build WebSocket URL */ function buildWsUrl(host, path) { const protocol = _useTls ? 'wss' : 'ws'; // Detect if host is IP address const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes('://'); if (isIp) { // For IP: add +3 to port for WebSocket, or use default ports // ws://ip:port format (no path) let [hostname, port] = host.split(':'); if (!port) port = '21115'; // Default hbbs TCP port port = parseInt(port) + 3; // WebSocket port offset return `${protocol}://${hostname}:${port}`; } else { // For domain: use path-based routing const base = host.includes('://') ? host : `${protocol}://${host}`; return `${base}${path}`; } } /** * Initialize and connect to the server */ async function connect(serverAddr, options = {}) { _serverAddr = serverAddr; _useTls = options.tls !== false; _serverKey = options.serverKey || ''; _myId = options.clientId || generateClientId(); _myName = options.clientName || `Web-${_myId.substring(0, 4)}`; _myUuid = options.uuid || generateUUID(); _passwordCallback = options.onPassword || null; _2faCallback = options.on2FA || null; _onPeerInfo = options.onPeerInfo || null; _onVideoFrame = options.onVideoFrame || null; _onAudioFrame = options.onAudioFrame || null; _onCursorData = options.onCursorData || null; _onCursorPosition = options.onCursorPosition || null; _onMiscEvent = options.onMiscEvent || null; _onClipboard = options.onClipboard || null; _onClose = options.onClose || null; _onStateChange = options.onStateChange || null; _onError = options.onError || null; // Generate keypairs RDCrypto.generateSignKeyPair(); RDCrypto.generateBoxKeyPair(); setState(STATE.CONNECTING); console.log('[RDConn] Connecting to', serverAddr); try { await connectHbbs(); } catch (e) { console.error('[RDConn] Connection failed:', e); setState(STATE.FAILED); if (_onError) _onError(e.message); } } /** * Connect to hbbs signaling server via WebSocket */ function connectHbbs() { return new Promise((resolve, reject) => { const url = buildWsUrl(_serverAddr, '/ws/id'); console.log('[RDConn] Connecting to hbbs at', url); _hbbsWs = new WebSocket(url); _hbbsWs.binaryType = 'arraybuffer'; _hbbsWs.onopen = () => { console.log('[RDConn] Connected to hbbs'); // First message from server should be KeyExchange (if encrypted) // Wait for it before registering }; _hbbsWs.onmessage = (event) => { handleHbbsMessage(new Uint8Array(event.data)).then(resolve).catch(reject); }; _hbbsWs.onerror = (e) => { console.error('[RDConn] hbbs WebSocket error:', e); reject(new Error('Failed to connect to signaling server')); }; _hbbsWs.onclose = (e) => { console.log('[RDConn] hbbs WebSocket closed:', e.code, e.reason); if (_state !== STATE.CONNECTED && _state !== STATE.CLOSED) { setState(STATE.FAILED); if (_onError) _onError('Signaling connection closed'); } }; // Timeout for connection setTimeout(() => { if (_hbbsWs && _hbbsWs.readyState !== WebSocket.OPEN) { reject(new Error('Connection timeout')); } }, 10000); }); } /** * Handle incoming messages from hbbs */ async function handleHbbsMessage(data) { let msgData = data; // Decrypt if hbbs connection is encrypted if (RDCrypto.isHbbsEncrypted()) { const decrypted = RDCrypto.hbbsDecrypt(data); if (!decrypted) { console.error('[RDConn] Failed to decrypt hbbs message'); return; } msgData = decrypted; } const rvMsg = RDProto.decodeRendezvousMsg(msgData); const field = getRvField(rvMsg); switch (field) { case 'key_exchange': await handleServerKeyExchange(rvMsg.key_exchange); break; case 'register_peer_response': if (rvMsg.register_peer_response.request_pk) { sendRegisterPk(); } break; case 'register_pk_response': console.log('[RDConn] Registration result:', rvMsg.register_pk_response.result); setState(STATE.PUNCHING); sendPunchHole(); break; case 'punch_hole_response': await handlePunchHoleResponse(rvMsg.punch_hole_response); break; case 'relay_response': await handleRelayResponse(rvMsg.relay_response); break; case 'online_response': handleOnlineResponse(rvMsg.online_response); break; case 'configure_update': console.log('[RDConn] Server config update received'); break; default: console.log('[RDConn] Unhandled hbbs message:', field, rvMsg); } } /** * Handle server key exchange (encrypted hbbs connection) */ async function handleServerKeyExchange(keyExchange) { if (keyExchange.keys.length < 1) { console.log('[RDConn] No server key provided, connection is unencrypted'); sendRegisterPeer(); return; } if (!_serverKey) { console.log('[RDConn] No server key configured, skipping verification'); sendRegisterPeer(); return; } // Parse server's long-term Ed25519 public key from hex const serverPk = hexToBytes(_serverKey); // Verify the server's ephemeral key signature const serverEphemeralPk = RDCrypto.verifyServerKey(keyExchange.keys[0], serverPk); if (!serverEphemeralPk) { console.error('[RDConn] Server key verification failed'); sendRegisterPeer(); return; } // Setup hbbs encryption using a separate keypair (not the session keypair) RDCrypto.generateHbbsKeyPair(); RDCrypto.setupHbbsEncryption(serverEphemeralPk); // Send our ephemeral pk back to server sendHbbs(RDProto.encodeRendezvousMsg({ key_exchange: { keys: [RDCrypto.hbbsBoxPublicKey] } })); console.log('[RDConn] Server key exchange complete, hbbs connection encrypted'); // Proceed with registration sendRegisterPeer(); } /** * Send RegisterPeer to hbbs */ function sendRegisterPeer() { setState(STATE.REGISTERING); _serial = Date.now() % 1000000; sendHbbs(RDProto.encodeRendezvousMsg({ register_peer: { id: _myId, serial: _serial } })); console.log('[RDConn] Sent RegisterPeer, id:', _myId); } /** * Send RegisterPk to hbbs */ function sendRegisterPk() { const pk = RDCrypto.getSignPublicKey(); sendHbbs(RDProto.encodeRendezvousMsg({ register_pk: { id: _myId, uuid: _myUuid, pk: pk } })); console.log('[RDConn] Sent RegisterPk'); } /** * Send PunchHoleRequest to hbbs */ function sendPunchHole() { sendHbbs(RDProto.encodeRendezvousMsg({ punch_hole_request: { id: _targetPeerId, nat_type: 0, // UNKNOWN_NAT conn_type: 0, // DEFAULT_CONN version: '1.3.6', force_relay: true // Web clients always use relay } })); console.log('[RDConn] Sent PunchHoleRequest for', _targetPeerId); } /** * Handle PunchHoleResponse from hbbs */ async function handlePunchHoleResponse(response) { console.log('[RDConn] PunchHoleResponse received'); // Check for failure if (response.failure) { const failures = { 0: 'ID_NOT_EXIST', 2: 'OFFLINE', 3: 'LICENSE_MISMATCH', 4: 'LICENSE_OVERUSE' }; const reason = failures[response.failure] || `UNKNOWN(${response.failure})`; console.error('[RDConn] Punch hole failed:', reason, response.other_failure); setState(STATE.FAILED); if (_onError) _onError(`Connection failed: ${reason}. ${response.other_failure || ''}`); return; } // Store peer's Ed25519 public key if (response.pk) { _peerPk = response.pk; RDCrypto.setPeerPublicKey(response.pk); } // Web clients always use relay setState(STATE.RELAYING); await connectRelay(); } /** * Connect to relay (hbbr) via WebSocket */ function connectRelay() { return new Promise((resolve, reject) => { const url = buildWsUrl(_serverAddr, '/ws/relay'); console.log('[RDConn] Connecting to relay at', url); _sessionWs = new WebSocket(url); _sessionWs.binaryType = 'arraybuffer'; _sessionWs.onopen = () => { console.log('[RDConn] Connected to relay'); // Send RequestRelay sendRelay(RDProto.encodeRendezvousMsg({ request_relay: { id: _targetPeerId, uuid: _myUuid, secure: !!_peerPk, conn_type: 0 // DEFAULT_CONN } })); }; _sessionWs.onmessage = (event) => { handleRelayMessage(new Uint8Array(event.data)); }; _sessionWs.onerror = (e) => { console.error('[RDConn] Relay WebSocket error:', e); setState(STATE.FAILED); if (_onError) _onError('Relay connection failed'); }; _sessionWs.onclose = (e) => { console.log('[RDConn] Relay WebSocket closed:', e.code, e.reason); if (_state === STATE.CONNECTED) { cleanup(); if (_onClose) _onClose(e.reason || 'Session closed'); } }; // Store the resolve for later _relayResolve = resolve; _relayReject = reject; }); } /** * Handle incoming messages from relay */ function handleRelayMessage(data) { // First few messages from relay are RendezvousMessage // After key exchange, they become Message (session protocol) let msgData = data; let isSessionMsg = RDCrypto.isEncrypted(); if (isSessionMsg) { // Try to decrypt as session message msgData = RDCrypto.decrypt(data); if (!msgData) { console.error('[RDConn] Failed to decrypt session message'); return; } handleSessionMessage(msgData); } else { // Still in rendezvous protocol try { const rvMsg = RDProto.decodeRendezvousMsg(msgData); handleRelayRendezvous(rvMsg); } catch (e) { // Maybe it's a session message without encryption try { handleSessionMessage(msgData); } catch (e2) { console.error('[RDConn] Failed to parse message:', e, e2); } } } } /** * Handle RendezvousMessage from relay */ function handleRelayRendezvous(rvMsg) { const field = getRvField(rvMsg); switch (field) { case 'relay_response': handleRelayResponse(rvMsg.relay_response); break; case 'punch_hole': // Server is telling us about an incoming connection console.log('[RDConn] PunchHole received (unexpected for web client)'); break; default: console.log('[RDConn] Unhandled relay rendezvous:', field); } } /** * Handle RelayResponse from hbbr */ async function handleRelayResponse(response) { console.log('[RDConn] RelayResponse received'); if (response.refuse_reason) { console.error('[RDConn] Relay refused:', response.refuse_reason); setState(STATE.FAILED); if (_onError) _onError(`Relay refused: ${response.refuse_reason}`); return; } // If we got the peer's pk from relay response, use it if (response.pk) { _peerPk = response.pk; RDCrypto.setPeerPublicKey(response.pk); } if (response.uuid) { _relayUuid = response.uuid; console.log('[RDConn] Relay UUID:', response.uuid); } setState(STATE.KEY_EXCHANGE); console.log('[RDConn] Waiting for key exchange from peer...'); // The actual session messages will come through the same relay WebSocket if (_relayResolve) _relayResolve(); } /** * Handle session protocol Message from peer */ function handleSessionMessage(data) { const msg = RDProto.decodeMsg(data); const field = getMsgField(msg); switch (field) { case 'signed_id': handleSignedId(msg.signed_id); break; case 'hash': handleHashChallenge(msg.hash); break; case 'login_response': handleLoginResponse(msg.login_response); break; case 'video_frame': if (_onVideoFrame) _onVideoFrame(msg.video_frame); break; case 'audio_frame': if (_onAudioFrame) _onAudioFrame(msg.audio_frame); break; case 'cursor_data': if (_onCursorData) _onCursorData(msg.cursor_data); break; case 'cursor_position': if (_onCursorPosition) _onCursorPosition(msg.cursor_position); break; case 'cursor_id': console.log('[RDConn] Cursor ID:', msg.cursor_id.toString()); break; case 'misc': handleMiscEvent(msg.misc); break; case 'clipboard': if (_onClipboard) _onClipboard(msg.clipboard); break; case 'test_delay': handleTestDelay(msg.test_delay); break; case 'message_box': handleMessageBox(msg.message_box); break; case 'peer_info': if (_onPeerInfo) _onPeerInfo(msg.peer_info); break; default: console.log('[RDConn] Session message:', field); } } /** * Handle SignedId (key exchange step 1) */ function handleSignedId(signedId) { console.log('[RDConn] Received SignedId, performing key exchange...'); try { // Verify Ed25519 signature and extract IdPk const idPk = RDCrypto.verifySignedId(signedId.id); // Create our PublicKey response const keyResponse = RDCrypto.createKeyExchangeResponse(idPk.pk); // Send PublicKey message sendSession(RDProto.encodeMsg({ public_key: keyResponse })); console.log('[RDConn] Key exchange complete, waiting for auth challenge...'); setState(STATE.AUTHENTICATING); } catch (e) { console.error('[RDConn] Key exchange failed:', e); setState(STATE.FAILED); if (_onError) _onError(`Key exchange failed: ${e.message}`); } } /** * Handle Hash challenge (authentication step 1) */ function handleHashChallenge(hash) { console.log('[RDConn] Received hash challenge, requesting password...'); if (_passwordCallback) { _passwordCallback(hash, async (password) => { if (password === null || password === undefined) { // User cancelled sendSession(RDProto.encodeMsg({})); close(); return; } const hashedPw = await RDCrypto.hashPassword(hash.salt, password); sendSession(RDProto.encodeMsg( RDProto.createPasswordLogin(_myId, _myName, hashedPw, 'Web', '1.3.6') )); }); } else { console.log('[RDConn] No password callback set, connecting without password'); sendSession(RDProto.encodeMsg( RDProto.createLoginRequest(_myId, _myName, new Uint8Array(0), 'Web', '1.3.6') )); } } /** * Handle LoginResponse (authentication result) */ function handleLoginResponse(response) { if (response.error) { console.error('[RDConn] Login failed:', response.error); setState(STATE.FAILED); if (_onError) _onError(`Authentication failed: ${response.error}`); return; } if (response.peer_info) { _peerPlatform = response.peer_info.platform; _peerVersion = response.peer_info.version; console.log('[RDConn] Login successful! Peer:', response.peer_info.hostname, 'Platform:', _peerPlatform, 'Version:', _peerVersion); if (_onPeerInfo) _onPeerInfo(response.peer_info); setState(STATE.CONNECTED); // Start keepalive startKeepalive(); startLatencyTest(); } } /** * Handle Misc event */ function handleMiscEvent(misc) { const field = getMiscField(misc); switch (field) { case 'close_reason': console.log('[RDConn] Close reason:', misc.close_reason); cleanup(); if (_onClose) _onClose(misc.close_reason); break; case 'permission_info': console.log('[RDConn] Permission:', misc.permission_info.permission, misc.permission_info.enabled ? 'granted' : 'revoked'); break; case 'switch_display': console.log('[RDConn] Switch display:', misc.switch_display.display, misc.switch_display.width, 'x', misc.switch_display.height); break; case 'audio_format': console.log('[RDConn] Audio format:', misc.audio_format.sample_rate, 'Hz,', misc.audio_format.channels, 'channels'); break; case 'supported_encoding': console.log('[RDConn] Server encoding support:', 'h264:', misc.supported_encoding.h264, 'h265:', misc.supported_encoding.h265, 'vp8:', misc.supported_encoding.vp8, 'av1:', misc.supported_encoding.av1); break; case 'chat_message': console.log('[RDConn] Chat:', misc.chat_message.text); break; case 'elevation_request': console.log('[RDConn] Elevation request'); break; case 'refresh_video': console.log('[RDConn] Refresh video requested'); break; case 'back_notification': console.log('[RDConn] Back notification:', misc.back_notification); break; default: // Pass to app callback if (_onMiscEvent) _onMiscEvent(field, misc); } } /** * Handle MessageBox from peer */ function handleMessageBox(msgBox) { console.log('[RDConn] MessageBox:', msgBox.msgtype, msgBox.title, msgBox.text); if (_onMiscEvent) _onMiscEvent('message_box', msgBox); } /** * Handle TestDelay (latency measurement) */ function handleTestDelay(testDelay) { if (testDelay.from_client) return; // Don't echo our own const latency = testDelay.last_delay; console.log('[RDConn] Latency:', latency, 'ms'); } /** * Send data to hbbs signaling server */ function sendHbbs(data) { if (_hbbsWs && _hbbsWs.readyState === WebSocket.OPEN) { if (RDCrypto.isHbbsEncrypted()) { data = RDCrypto.hbbsEncrypt(data); } _hbbsWs.send(data); } } /** * Send data through session (relay) WebSocket */ function sendSession(data) { if (_sessionWs && _sessionWs.readyState === WebSocket.OPEN) { let sendData = data; if (RDCrypto.isEncrypted()) { sendData = RDCrypto.encrypt(data); } _sessionWs.send(sendData); } } /** * Connect to a specific peer */ function connectToPeer(peerId) { _targetPeerId = peerId; if (_state === STATE.PUNCHING || _state === STATE.REGISTERING || _state === STATE.CONNECTING) { // Already connected to hbbs, just send punch hole sendPunchHole(); } else if (_state === STATE.CONNECTED) { // Already in a session, close it first close(); setTimeout(() => connect(_serverAddr), 500); // After reconnect, we'll need to punch hole again // The password callback etc. should still be set } else { // Reconnect to server connect(_serverAddr, { clientId: _myId, clientName: _myName, uuid: _myUuid, tls: _useTls, serverKey: _serverKey, onPassword: _passwordCallback, on2FA: _2faCallback, onPeerInfo: _onPeerInfo, onVideoFrame: _onVideoFrame, onAudioFrame: _onAudioFrame, onCursorData: _onCursorData, onCursorPosition: _onCursorPosition, onMiscEvent: _onMiscEvent, onClipboard: _onClipboard, onClose: _onClose, onStateChange: _onStateChange, onError: _onError, }); } } /** * Send a 2FA code */ function send2FACode(code) { if (_state === STATE.AUTHENTICATING) { sendSession(RDProto.encodeMsg({ auth_2fa: { code: code } })); } } /** * Send a mouse event to the peer */ function sendMouseEvent(mask, x, y, modifiers = []) { sendSession(RDProto.encodeMsg(RDProto.createMouseEvent(mask, x, y, modifiers))); } /** * Send a key event to the peer */ function sendKeyEvent(controlKey, down, press = false, modifiers = []) { sendSession(RDProto.encodeMsg(RDProto.createKeyEvent(controlKey, down, press, modifiers))); } /** * Send a character key event to the peer */ function sendCharEvent(chr, down, press = false, modifiers = []) { sendSession(RDProto.encodeMsg(RDProto.createCharKeyEvent(chr, down, press, modifiers))); } /** * Send clipboard text to peer */ function sendClipboard(text) { sendSession(RDProto.encodeMsg(RDProto.createClipboard(text))); } /** * Send Ctrl+Alt+Del */ function sendCtrlAltDel() { sendSession(RDProto.encodeMsg(RDProto.createKeyEvent(100, true, true))); setTimeout(() => sendSession(RDProto.encodeMsg(RDProto.createKeyEvent(100, false, false))), 50); } /** * Request display switch */ function sendSwitchDisplay(display) { sendSession(RDProto.encodeMsg({ misc: { switch_display: { display: display } } })); } /** * Lock remote screen */ function sendLockScreen() { sendSession(RDProto.encodeMsg(RDProto.createKeyEvent(101, true, true))); } /** * Start keepalive interval */ function startKeepalive() { stopKeepalive(); _keepAliveInterval = setInterval(() => { if (_state === STATE.CONNECTED) { // Send empty message as keepalive sendSession(new Uint8Array(0)); } }, 30000); } /** * Stop keepalive */ function stopKeepalive() { if (_keepAliveInterval) { clearInterval(_keepAliveInterval); _keepAliveInterval = null; } } /** * Start latency test interval */ function startLatencyTest() { stopLatencyTest(); _latencyInterval = setInterval(() => { if (_state === STATE.CONNECTED) { sendSession(RDProto.encodeMsg(RDProto.createTestDelay(true))); } }, 10000); } /** * Stop latency test */ function stopLatencyTest() { if (_latencyInterval) { clearInterval(_latencyInterval); _latencyInterval = null; } } /** * Clean up connection resources */ function cleanup() { stopKeepalive(); stopLatencyTest(); RDCrypto.reset(); RDCrypto.resetHbbs(); } /** * Close the connection */ function close() { cleanup(); if (_hbbsWs) { try { _hbbsWs.close(); } catch (e) {} _hbbsWs = null; } if (_sessionWs) { try { _sessionWs.close(); } catch (e) {} _sessionWs = null; } setState(STATE.CLOSED); } /** * Set connection state */ function setState(newState) { const oldState = _state; _state = newState; console.log('[RDConn] State:', oldState, '->', newState); if (_onStateChange) _onStateChange(newState, oldState); } /** * Get the active field from a RendezvousMessage */ function getRvField(msg) { const fields = ['register_peer', 'register_peer_response', 'punch_hole_request', 'punch_hole', 'punch_hole_sent', 'punch_hole_response', 'fetch_local_addr', 'local_addr', 'configure_update', 'register_pk', 'register_pk_response', 'software_update', 'request_relay', 'relay_response', 'test_nat_request', 'test_nat_response', 'peer_discovery', 'online_request', 'online_response', 'key_exchange', 'hc', 'http_proxy_request', 'http_proxy_response']; for (const f of fields) { if (msg[f] != null && !(typeof msg[f] === 'object' && Object.keys(msg[f]).length === 0 && !ArrayBuffer.isView(msg[f]))) { return f; } } // Check for bytes/number fields if (msg.hc !== undefined && msg.hc !== null) return 'hc'; return null; } /** * Get the active field from a Message */ function getMsgField(msg) { const fields = ['signed_id', 'public_key', 'test_delay', 'video_frame', 'login_request', 'login_response', 'hash', 'mouse_event', 'audio_frame', 'cursor_data', 'cursor_position', 'key_event', 'clipboard', 'file_action', 'file_response', 'misc', 'cliprdr', 'message_box', 'peer_info', 'pointer_device_event', 'auth_2fa', 'multi_clipboards', 'screenshot_request', 'screenshot_response', 'terminal_action', 'terminal_response', 'switch_sides_response', 'voice_call_request', 'voice_call_response']; for (const f of fields) { if (msg[f] != null) { if (f === 'cursor_id' && msg[f] === 0n) continue; // Skip default bigint if (typeof msg[f] === 'object' && !ArrayBuffer.isView(msg[f]) && !(msg[f] instanceof Uint8Array) && !(msg[f] instanceof ArrayBuffer) && Object.keys(msg[f]).length === 0) continue; if (typeof msg[f] === 'number' && msg[f] === 0 && f === 'cursor_id') continue; return f; } } // Check cursor_id specifically (bigint) if (msg.cursor_id && msg.cursor_id !== 0n) return 'cursor_id'; return null; } /** * Get the active field from a Misc message */ function getMiscField(misc) { const fields = ['chat_message', 'switch_display', 'permission_info', 'option', 'audio_format', 'close_reason', 'refresh_video', 'video_received', 'back_notification', 'restart_remote_device', 'uac', 'foreground_window_elevated', 'stop_service', 'elevation_request', 'elevation_response', 'portable_service_running', 'switch_sides_request', 'switch_back', 'change_resolution', 'plugin_request', 'plugin_failure', 'full_speed_fps', 'auto_adjust_fps', 'client_record_status', 'capture_displays', 'refresh_video_display', 'toggle_virtual_display', 'toggle_privacy_mode', 'supported_encoding', 'selected_sid', 'change_display_resolution', 'message_query', 'follow_current_display']; for (const f of fields) { if (misc[f] != null && misc[f] !== false && misc[f] !== '') { return f; } } return null; } /** * Hex string to Uint8Array */ function hexToBytes(hex) { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16); } return bytes; } return { STATE, connect, connectToPeer, close, sendMouseEvent, sendKeyEvent, sendCharEvent, sendClipboard, sendCtrlAltDel, sendSwitchDisplay, sendLockScreen, send2FACode, get state() { return _state; }, get myId() { return _myId; }, get myName() { return _myName; }, get peerPlatform() { return _peerPlatform; }, get isConnected() { return _state === STATE.CONNECTED; }, }; })();