123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774 |
- import Websock from "./websock";
- import * as message from "./message.js";
- import * as rendezvous from "./rendezvous.js";
- import { loadVp9 } from "./codec";
- import * as sha256 from "fast-sha256";
- import * as globals from "./globals";
- import { decompress, mapKey, sleep } from "./common";
- const PORT = 21116;
- const HOSTS = [
- "rs-sg.rustdesk.com",
- "rs-cn.rustdesk.com",
- "rs-us.rustdesk.com",
- ];
- let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
- const SCHEMA = "ws://";
- type MsgboxCallback = (type: string, title: string, text: string) => void;
- type DrawCallback = (data: Uint8Array) => void;
- //const cursorCanvas = document.createElement("canvas");
- 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;
- _password: Uint8Array | undefined;
- _options: any;
- _videoTestSpeed: number[];
- //_cursors: { [name: number]: any };
- constructor() {
- this._msgbox = globals.msgbox;
- this._draw = globals.draw;
- this._msgs = [];
- this._id = "";
- this._videoTestSpeed = [0, 0];
- //this._cursors = {};
- }
- async start(id: string) {
- try {
- await this._start(id);
- } catch (e: any) {
- this.msgbox(
- "error",
- "Connection Error",
- e.type == "close" ? "Reset by the peer" : String(e)
- );
- }
- }
- 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();
- const uri = getDefaultUri();
- const ws = new Websock(uri, true);
- this._ws = ws;
- this._id = id;
- console.log(
- new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id
- );
- await ws.open();
- console.log(new Date() + ": Connected to rendezvous 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,
- token: localStorage.getItem("access_token") || undefined,
- });
- 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?.other_failure) {
- this.msgbox("error", "Error", phr?.other_failure);
- return;
- }
- 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;
- case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE:
- this.msgbox("error", "Error", "Key overuse");
- break;
- }
- }
- } else if (rr) {
- if (!rr.version) {
- this.msgbox("error", "Error", "Remote version is low, not support web");
- return;
- }
- await this.connectRelay(rr);
- }
- }
- async connectRelay(rr: rendezvous.RelayResponse) {
- const pk = rr.pk;
- let uri = rr.relay_server;
- if (uri) {
- uri = getrUriFromRs(uri, true, 2);
- } else {
- uri = getDefaultUri(true);
- }
- const uuid = rr.uuid;
- console.log(new Date() + ": Connecting to relay server: " + uri);
- const ws = new Websock(uri, false);
- 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, localStorage.getItem("key") || RS_PK);
- if (pk) {
- const idpk = message.IdPk.decode(pk);
- if (idpk.id == this._id) {
- pk = idpk.pk;
- }
- }
- 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
- const public_key = message.PublicKey.fromPartial({});
- this._ws?.sendMessage({ public_key });
- 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");
- const public_key = message.PublicKey.fromPartial({});
- this._ws?.sendMessage({ public_key });
- 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;
- }
- const idpk = message.IdPk.decode(signedId);
- const id = idpk.id;
- const theirPk = idpk.pk;
- if (id != this._id!) {
- console.error("Handshake failed: sign failure");
- const public_key = message.PublicKey.fromPartial({});
- this._ws?.sendMessage({ public_key });
- return;
- }
- if (theirPk.length != 32) {
- console.error(
- "Handshake failed: invalid public box key length from peer"
- );
- const public_key = message.PublicKey.fromPartial({});
- this._ws?.sendMessage({ public_key });
- 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) {
- if (r.error == "Wrong Password") {
- this._password = undefined;
- this.msgbox(
- "re-input-password",
- r.error,
- "Do you want to enter again?"
- );
- } else {
- this.msgbox("error", "Login 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;
- }
- try {
- globals.copyToClipboard(new TextDecoder().decode(cb.content));
- } catch (e) {
- console.error(e);
- }
- // 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);
- /*
- let ctx = cursorCanvas.getContext("2d");
- cursorCanvas.width = cd.width;
- cursorCanvas.height = cd.height;
- let imgData = new ImageData(
- new Uint8ClampedArray(c),
- cd.width,
- cd.height
- );
- ctx?.clearRect(0, 0, cd.width, cd.height);
- ctx?.putImageData(imgData, 0, 0);
- let url = cursorCanvas.toDataURL();
- const img = document.createElement("img");
- img.src = url;
- this._cursors[cd.id] = img;
- //cursorCanvas.width /= 2.;
- //cursorCanvas.height /= 2.;
- //ctx?.drawImage(img, cursorCanvas.width, cursorCanvas.height);
- url = cursorCanvas.toDataURL();
- document.body.style.cursor =
- "url(" + url + ")" + cd.hotx + " " + cd.hoty + ", default";
- console.log(document.body.style.cursor);
- */
- } 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) {
- if (!this.handleMisc(msg?.misc)) break;
- } else if (msg?.audio_frame) {
- globals.playAudio(msg?.audio_frame.data);
- }
- }
- }
- 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();
- }
- 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) {
- globals.initAudio(
- misc.audio_format.channels,
- misc.audio_format.sample_rate
- );
- } else if (misc.chat_message) {
- globals.pushEvent("chat", { text: 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);
- this.close();
- return false;
- }
- return true;
- }
- 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" || name == "RAlt")) {
- alt = false;
- }
- if (ctrl && (name == "VK_CONTROL" || name == "RControl")) {
- ctrl = false;
- }
- if (shift && (name == "VK_SHIFT" || name == "RShift")) {
- shift = false;
- }
- if (command && (name == "Meta" || name == "RWin")) {
- 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);
- });
- }
- }
- function testDelay() {
- var nearest = "";
- HOSTS.forEach((host) => {
- const now = new Date().getTime();
- new Websock(getrUriFromRs(host), true).open().then(() => {
- console.log("latency of " + host + ": " + (new Date().getTime() - now));
- if (!nearest) {
- HOST = host;
- localStorage.setItem("rendezvous-server", host);
- }
- });
- });
- }
- testDelay();
- function getDefaultUri(isRelay: Boolean = false): string {
- const host = localStorage.getItem("custom-rendezvous-server");
- return getrUriFromRs(host || HOST, isRelay);
- }
- function getrUriFromRs(
- uri: string,
- isRelay: Boolean = false,
- roffset: number = 0
- ): string {
- if (uri.indexOf(":") > 0) {
- const tmp = uri.split(":");
- const port = parseInt(tmp[1]);
- uri = tmp[0] + ":" + (port + (isRelay ? roffset || 3 : 2));
- } else {
- uri += ":" + (PORT + (isRelay ? 3 : 2));
- }
- 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();
- }
|