projects/shelled/rustdesk-web-client/js/connection.js
Z User 7b758ebe01 Add standalone RustDesk web client (reverse-engineered)
Complete browser-based remote desktop client built from protocol analysis:
- Protobuf message definitions (75+ message types, 10 enums)
- NaCl encryption (Ed25519 + Curve25519 ECDH + XSalsa20-Poly1305)
- WebSocket signaling (hbbs) + relay (hbbr) connection lifecycle
- VP8/VP9/AV1 video decoding via ogvjs WASM + yuv-canvas WebGL
- Opus audio decoding via libopus WASM + Web Audio API
- Full mouse/keyboard input forwarding to protobuf events
- Connection dialog, status bar, toolbar, log panel UI

Also tracked web_deps (codec libraries) in rustdesk-as-ref/
2026-04-06 21:11:21 +00:00

1018 lines
34 KiB
JavaScript

/**
* 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; },
};
})();