import Websock from "./websock"; import * as message from "./message.js"; import * as rendezvous from "./rendezvous.js"; import { loadVp9, loadOpus } from "./codec"; import * as sha256 from "fast-sha256"; import * as globals from "./globals"; import { decompress, mapKey, sleep } from "./common"; const PORT = 21116; const HOST = "rs-sg.rustdesk.com"; const SCHEMA = "ws://"; type MsgboxCallback = (type: string, title: string, text: string) => void; type DrawCallback = (data: Uint8Array) => void; export default class Connection { _msgs: any[]; _ws: Websock | undefined; _interval: any; _id: string; _hash: message.Hash | undefined; _msgbox: MsgboxCallback; _draw: DrawCallback; _peerInfo: message.PeerInfo | undefined; _firstFrame: Boolean | undefined; _videoDecoder: any; _audioDecoder: any; _password: Uint8Array | undefined; _options: any; _videoTestSpeed: number[]; constructor() { this._msgbox = globals.msgbox; this._draw = globals.draw; this._msgs = []; this._id = ""; this._videoTestSpeed = [0, 0]; } async start(id: string) { if (!this._options) { this._options = globals.getPeers()[id] || {}; } if (!this._password) { const p = this.getOption("password"); if (p) { try { this._password = Uint8Array.from(JSON.parse("[" + p + "]")); } catch (e) { console.error(e); } } } this._interval = setInterval(() => { while (this._msgs.length) { this._ws?.sendMessage(this._msgs[0]); this._msgs.splice(0, 1); } }, 1); this.loadVideoDecoder(); this.loadAudioDecoder(); const uri = getDefaultUri(); const ws = new Websock(uri, true); this._ws = ws; this._id = id; console.log( new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id ); await ws.open(); console.log(new Date() + ": Connected to rendezvoous server"); const conn_type = rendezvous.ConnType.DEFAULT_CONN; const nat_type = rendezvous.NatType.SYMMETRIC; const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ id, licence_key: localStorage.getItem("key") || undefined, conn_type, nat_type, }); ws.sendRendezvous({ punch_hole_request }); const msg = (await ws.next()) as rendezvous.RendezvousMessage; ws.close(); console.log(new Date() + ": Got relay response"); const phr = msg.punch_hole_response; const rr = msg.relay_response; if (phr) { if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) { switch (phr?.failure) { case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST: this.msgbox("error", "Error", "ID does not exist"); break; case rendezvous.PunchHoleResponse_Failure.OFFLINE: this.msgbox("error", "Error", "Remote desktop is offline"); break; case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH: this.msgbox("error", "Error", "Key mismatch"); break; default: if (phr?.other_failure) { this.msgbox("error", "Error", phr?.other_failure); } } } } else if (rr) { await this.connectRelay(rr); } } async connectRelay(rr: rendezvous.RelayResponse) { const pk = rr.pk; let uri = rr.relay_server; if (uri) { uri = getrUriFromRs(uri); } else { uri = getDefaultUri(true); } const uuid = rr.uuid; console.log(new Date() + ": Connecting to relay server: " + uri); const ws = new Websock(uri, false, this.handleVideoFrame.bind(this)); await ws.open(); console.log(new Date() + ": Connected to relay server"); this._ws = ws; const request_relay = rendezvous.RequestRelay.fromPartial({ licence_key: localStorage.getItem("key") || undefined, uuid, }); ws.sendRendezvous({ request_relay }); const secure = (await this.secure(pk)) || false; globals.pushEvent("connection_ready", { secure, direct: false }); await this.msgLoop(); } async secure(pk: Uint8Array | undefined) { if (pk) { const RS_PK = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; try { pk = await globals.verify(pk, RS_PK).catch(); if (pk?.length != 32) { pk = undefined; } } catch (e) { console.error(e); pk = undefined; } if (!pk) console.error( "Handshake failed: invalid public key from rendezvous server" ); } if (!pk) { // send an empty message out in case server is setting up secure and waiting for first message this._ws?.sendMessage({}); return; } const msg = (await this._ws?.next()) as message.Message; let signedId: any = msg?.signed_id; if (!signedId) { console.error("Handshake failed: invalid message type"); this._ws?.sendMessage({}); return; } try { signedId = await globals.verify(signedId.id, Uint8Array.from(pk!)); } catch (e) { console.error(e); // fall back to non-secure connection in case pk mismatch console.error("pk mismatch, fall back to non-secure"); const public_key = message.PublicKey.fromPartial({}); this._ws?.sendMessage({ public_key }); return; } signedId = new TextDecoder().decode(signedId!); const tmp = signedId.split("\0"); const id = tmp[0]; let theirPk = tmp[1]; if (id != this._id!) { console.error("Handshake failed: sign failure"); this._ws?.sendMessage({}); return; } theirPk = globals.decodeBase64(theirPk); if (theirPk.length != 32) { console.error( "Handshake failed: invalid public box key length from peer" ); this._ws?.sendMessage({}); return; } const [mySk, asymmetric_value] = globals.genBoxKeyPair(); const secret_key = globals.genSecretKey(); const symmetric_value = globals.seal(secret_key, theirPk, mySk); const public_key = message.PublicKey.fromPartial({ asymmetric_value, symmetric_value, }); this._ws?.sendMessage({ public_key }); this._ws?.setSecretKey(secret_key); console.log("secured"); return true; } async msgLoop() { while (true) { const msg = (await this._ws?.next()) as message.Message; if (msg?.hash) { this._hash = msg?.hash; if (!this._password) this.msgbox("input-password", "Password Required", ""); this.login(); } else if (msg?.test_delay) { const test_delay = msg?.test_delay; console.log(test_delay); if (!test_delay.from_client) { this._ws?.sendMessage({ test_delay }); } } else if (msg?.login_response) { const r = msg?.login_response; if (r.error) { this.msgbox("error", "Error", r.error); } else if (r.peer_info) { this.handlePeerInfo(r.peer_info); } } else if (msg?.video_frame) { this.handleVideoFrame(msg?.video_frame!); } else if (msg?.clipboard) { const cb = msg?.clipboard; if (cb.compress) { const c = await decompress(cb.content); if (!c) continue; cb.content = c; } globals.pushEvent("clipboard", cb); } else if (msg?.cursor_data) { const cd = msg?.cursor_data; const c = await decompress(cd.colors); if (!c) continue; cd.colors = c; globals.pushEvent("cursor_data", cd); } else if (msg?.cursor_id) { globals.pushEvent("cursor_id", { id: msg?.cursor_id }); } else if (msg?.cursor_position) { globals.pushEvent("cursor_position", msg?.cursor_position); } else if (msg?.misc) { this.handleMisc(msg?.misc); } else if (msg?.audio_frame) { // } } } msgbox(type_: string, title: string, text: string) { this._msgbox?.(type_, title, text); } draw(frame: any) { this._draw?.(frame); globals.draw(frame); } close() { this._msgs = []; clearInterval(this._interval); this._ws?.close(); this._videoDecoder?.close(); this._audioDecoder?.close(); } refresh() { const misc = message.Misc.fromPartial({ refresh_video: true }); this._ws?.sendMessage({ misc }); } setMsgbox(callback: MsgboxCallback) { this._msgbox = callback; } setDraw(callback: DrawCallback) { this._draw = callback; } login(password: string | undefined = undefined) { if (password) { const salt = this._hash?.salt; let p = hash([password, salt!]); this._password = p; const challenge = this._hash?.challenge; p = hash([p, challenge!]); this.msgbox("connecting", "Connecting...", "Logging in..."); this._sendLoginMessage(p); } else { let p = this._password; if (p) { const challenge = this._hash?.challenge; p = hash([p, challenge!]); } this._sendLoginMessage(p); } } async reconnect() { this.close(); await this.start(this._id); } _sendLoginMessage(password: Uint8Array | undefined = undefined) { const login_request = message.LoginRequest.fromPartial({ username: this._id!, my_id: "web", // to-do my_name: "web", // to-do password, option: this.getOptionMessage(), video_ack_required: true, }); this._ws?.sendMessage({ login_request }); } getOptionMessage(): message.OptionMessage | undefined { let n = 0; const msg = message.OptionMessage.fromPartial({}); const q = this.getImageQualityEnum(this.getImageQuality(), true); const yes = message.OptionMessage_BoolOption.Yes; if (q != undefined) { msg.image_quality = q; n += 1; } if (this._options["show-remote-cursor"]) { msg.show_remote_cursor = yes; n += 1; } if (this._options["lock-after-session-end"]) { msg.lock_after_session_end = yes; n += 1; } if (this._options["privacy-mode"]) { msg.privacy_mode = yes; n += 1; } if (this._options["disable-audio"]) { msg.disable_audio = yes; n += 1; } if (this._options["disable-clipboard"]) { msg.disable_clipboard = yes; n += 1; } return n > 0 ? msg : undefined; } sendVideoReceived() { const misc = message.Misc.fromPartial({ video_received: true }); this._ws?.sendMessage({ misc }); } handleVideoFrame(vf: message.VideoFrame) { if (!this._firstFrame) { this.msgbox("", "", ""); this._firstFrame = true; } if (vf.vp9s) { const dec = this._videoDecoder; var tm = new Date().getTime(); var i = 0; const n = vf.vp9s?.frames.length; vf.vp9s.frames.forEach((f) => { dec.processFrame(f.data.slice(0).buffer, (ok: any) => { i++; if (i == n) this.sendVideoReceived(); if (ok && dec.frameBuffer && n == i) { this.draw(dec.frameBuffer); const now = new Date().getTime(); var elapsed = now - tm; this._videoTestSpeed[1] += elapsed; this._videoTestSpeed[0] += 1; if (this._videoTestSpeed[0] >= 30) { console.log( "video decoder: " + parseInt( "" + this._videoTestSpeed[1] / this._videoTestSpeed[0] ) ); this._videoTestSpeed = [0, 0]; } } }); }); } } handlePeerInfo(pi: message.PeerInfo) { this._peerInfo = pi; if (pi.displays.length == 0) { this.msgbox("error", "Remote Error", "No Display"); return; } this.msgbox("success", "Successful", "Connected, waiting for image..."); globals.pushEvent("peer_info", pi); const p = this.shouldAutoLogin(); if (p) this.inputOsPassword(p); const username = this.getOption("info")?.username; if (username && !pi.username) pi.username = username; this.setOption("info", pi); if (this.getRemember()) { if (this._password?.length) { const p = this._password.toString(); if (p != this.getOption("password")) { this.setOption("password", p); console.log("remember password of " + this._id); } } } else { this.setOption("password", undefined); } } shouldAutoLogin(): string { const l = this.getOption("lock-after-session-end"); const a = !!this.getOption("auto-login"); const p = this.getOption("os-password"); if (p && l && a) { return p; } return ""; } handleMisc(misc: message.Misc) { if (misc.audio_format) { // } else if (misc.chat_message) { globals.pushEvent("chat", misc.chat_message.text); } else if (misc.permission_info) { const p = misc.permission_info; console.info("Change permission " + p.permission + " -> " + p.enabled); let name; switch (p.permission) { case message.PermissionInfo_Permission.Keyboard: name = "keyboard"; break; case message.PermissionInfo_Permission.Clipboard: name = "clipboard"; break; case message.PermissionInfo_Permission.Audio: name = "audio"; break; default: return; } globals.pushEvent("permission", { [name]: p.enabled }); } else if (misc.switch_display) { this.loadVideoDecoder(); globals.pushEvent("switch_display", misc.switch_display); } else if (misc.close_reason) { this.msgbox("error", "Connection Error", misc.close_reason); } } getRemember(): Boolean { return this._options["remember"] || false; } setRemember(v: Boolean) { this.setOption("remember", v); } getOption(name: string): any { return this._options[name]; } setOption(name: string, value: any) { if (value == undefined) { delete this._options[name]; } else { this._options[name] = value; } this._options["tm"] = new Date().getTime(); const peers = globals.getPeers(); peers[this._id] = this._options; localStorage.setItem("peers", JSON.stringify(peers)); } inputKey( name: string, down: boolean, press: boolean, alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean ) { const key_event = mapKey(name, globals.isDesktop()); if (!key_event) return; if (alt && name == "VK_MENU") { alt = false; } if (ctrl && name == "VK_CONTROL") { ctrl = false; } if (shift && name == "VK_SHIFT") { shift = false; } if (command && name == "Meta") { command = false; } key_event.down = down; key_event.press = press; key_event.modifiers = this.getMod(alt, ctrl, shift, command); this._ws?.sendMessage({ key_event }); } ctrlAltDel() { const key_event = message.KeyEvent.fromPartial({ down: true }); if (this._peerInfo?.platform == "Windows") { key_event.control_key = message.ControlKey.CtrlAltDel; } else { key_event.control_key = message.ControlKey.Delete; key_event.modifiers = this.getMod(true, true, false, false); } this._ws?.sendMessage({ key_event }); } inputString(seq: string) { const key_event = message.KeyEvent.fromPartial({ seq }); this._ws?.sendMessage({ key_event }); } switchDisplay(display: number) { const switch_display = message.SwitchDisplay.fromPartial({ display }); const misc = message.Misc.fromPartial({ switch_display }); this._ws?.sendMessage({ misc }); } async inputOsPassword(seq: string) { this.inputMouse(); await sleep(50); this.inputMouse(0, 3, 3); await sleep(50); this.inputMouse(1 | (1 << 3)); this.inputMouse(2 | (1 << 3)); await sleep(1200); const key_event = message.KeyEvent.fromPartial({ press: true, seq }); this._ws?.sendMessage({ key_event }); } lockScreen() { const key_event = message.KeyEvent.fromPartial({ down: true, control_key: message.ControlKey.LockScreen, }); this._ws?.sendMessage({ key_event }); } getMod(alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean) { const mod: message.ControlKey[] = []; if (alt) mod.push(message.ControlKey.Alt); if (ctrl) mod.push(message.ControlKey.Control); if (shift) mod.push(message.ControlKey.Shift); if (command) mod.push(message.ControlKey.Meta); return mod; } inputMouse( mask: number = 0, x: number = 0, y: number = 0, alt: Boolean = false, ctrl: Boolean = false, shift: Boolean = false, command: Boolean = false ) { const mouse_event = message.MouseEvent.fromPartial({ mask, x, y, modifiers: this.getMod(alt, ctrl, shift, command), }); this._ws?.sendMessage({ mouse_event }); } toggleOption(name: string) { const v = !this._options[name]; const option = message.OptionMessage.fromPartial({}); const v2 = v ? message.OptionMessage_BoolOption.Yes : message.OptionMessage_BoolOption.No; switch (name) { case "show-remote-cursor": option.show_remote_cursor = v2; break; case "disable-audio": option.disable_audio = v2; break; case "disable-clipboard": option.disable_clipboard = v2; break; case "lock-after-session-end": option.lock_after_session_end = v2; break; case "privacy-mode": option.privacy_mode = v2; break; case "block-input": option.block_input = message.OptionMessage_BoolOption.Yes; break; case "unblock-input": option.block_input = message.OptionMessage_BoolOption.No; break; default: return; } if (name.indexOf("block-input") < 0) this.setOption(name, v); const misc = message.Misc.fromPartial({ option }); this._ws?.sendMessage({ misc }); } getImageQuality() { return this.getOption("image-quality"); } getImageQualityEnum( value: string, ignoreDefault: Boolean ): message.ImageQuality | undefined { switch (value) { case "low": return message.ImageQuality.Low; case "best": return message.ImageQuality.Best; case "balanced": return ignoreDefault ? undefined : message.ImageQuality.Balanced; default: return undefined; } } setImageQuality(value: string) { this.setOption("image-quality", value); const image_quality = this.getImageQualityEnum(value, false); if (image_quality == undefined) return; const option = message.OptionMessage.fromPartial({ image_quality }); const misc = message.Misc.fromPartial({ option }); this._ws?.sendMessage({ misc }); } loadVideoDecoder() { this._videoDecoder?.close(); loadVp9((decoder: any) => { this._videoDecoder = decoder; console.log("vp9 loaded"); console.log(decoder); }); } loadAudioDecoder() { this._audioDecoder?.close(); loadOpus((decoder: any) => { this._audioDecoder = decoder; console.log("opus loaded"); }); } } // @ts-ignore async function testDelay() { const ws = new Websock(getDefaultUri(false), true); await ws.open(); console.log(ws.latency()); } function getDefaultUri(isRelay: Boolean = false): string { const host = localStorage.getItem("custom-rendezvous-server"); return SCHEMA + (host || HOST) + ":" + (PORT + (isRelay ? 3 : 2)); } function getrUriFromRs(uri: string): string { if (uri.indexOf(":") > 0) { const tmp = uri.split(":"); const port = parseInt(tmp[1]); uri = tmp[0] + ":" + (port + 2); } else { uri += ":" + (PORT + 3); } return SCHEMA + uri; } function hash(datas: (string | Uint8Array)[]): Uint8Array { const hasher = new sha256.Hash(); datas.forEach((data) => { if (typeof data == "string") { data = new TextEncoder().encode(data); } return hasher.update(data); }); return hasher.digest(); }