diff --git a/web/js/.gitattributes b/web/js/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/web/js/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/web/js/.gitignore b/web/js/.gitignore new file mode 100644 index 000000000..620c5689f --- /dev/null +++ b/web/js/.gitignore @@ -0,0 +1,8 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local +*log +ogvjs +.vscode diff --git a/web/js/gen_js_from_hbb.py b/web/js/gen_js_from_hbb.py new file mode 100755 index 000000000..7e9d78cbe --- /dev/null +++ b/web/js/gen_js_from_hbb.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import re +import os +import glob +from tabnanny import check + +def main(): + print('export const LANGS = {') + for fn in glob.glob('../hbb/src/lang/*'): + lang = os.path.basename(fn)[:-3] + if lang == 'template': continue + print(' %s: {'%lang) + for ln in open(fn): + ln = ln.strip() + if ln.startswith('("'): + toks = ln.split('", "') + assert(len(toks) == 2) + a = toks[0][2:] + b = toks[1][:-3] + print(' "%s": "%s",'%(a, b)) + print(' },') + print('}') + check_if_retry = ['', False] + KEY_MAP = ['', False] + for ln in open('../hbb/src/client.rs'): + ln = ln.strip() + if 'check_if_retry' in ln: + check_if_retry[1] = True + continue + if ln.startswith('}') and check_if_retry[1]: + check_if_retry[1] = False + continue + if check_if_retry[1]: + ln = removeComment(ln) + check_if_retry[0] += ln + '\n' + if 'KEY_MAP' in ln: + KEY_MAP[1] = True + continue + if '.collect' in ln and KEY_MAP[1]: + KEY_MAP[1] = False + continue + if KEY_MAP[1] and ln.startswith('('): + ln = removeComment(ln) + toks = ln.split('", Key::') + assert(len(toks) == 2) + a = toks[0][2:] + b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '') + KEY_MAP[0] += ' "%s": "%s",\n'%(a, b) + print() + print('export function checkIfRetry(msgtype: string, title: string, text: string) {') + print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0')) + print(';}') + print() + print('export const KEY_MAP: any = {') + print(KEY_MAP[0]) + print('}') + for ln in open('../hbb/Cargo.toml'): + if ln.startswith('version ='): + print('export const ' + ln) + + +def removeComment(ln): + return re.sub('\s+\/\/.*$', '', ln) + +main() diff --git a/web/js/index.html b/web/js/index.html new file mode 100644 index 000000000..0ae0a2410 --- /dev/null +++ b/web/js/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Vite App + + +
+ + + diff --git a/web/js/package.json b/web/js/package.json new file mode 100644 index 000000000..15e0e75b8 --- /dev/null +++ b/web/js/package.json @@ -0,0 +1,22 @@ +{ + "name": "web_hbb", + "version": "1.0.0", + "scripts": { + "dev": "vite", + "build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^4.4.4", + "vite": "^2.7.2" + }, + "dependencies": { + "fast-sha256": "^1.3.0", + "libsodium": "^0.7.9", + "libsodium-wrappers": "^0.7.9", + "pcm-player": "^0.0.11", + "ts-proto": "^1.101.0", + "wasm-feature-detect": "^1.2.11", + "zstddec": "^0.0.2" + } +} diff --git a/web/js/src/codec.js b/web/js/src/codec.js new file mode 100644 index 000000000..dc579b5f3 --- /dev/null +++ b/web/js/src/codec.js @@ -0,0 +1,43 @@ +// example: https://github.com/rgov/js-theora-decoder/blob/main/index.html +// https://github.com/brion/ogv.js/releases, yarn add has no simd +// dev: copy decoder files from node/ogv/dist/* to project dir +// dist: .... to dist +/* + OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js', + OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js', + OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js', + OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js', + OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js', + OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js', + OGVDecoderVideoVP8MTW: 'ogv-decoder-video-vp8-mt-wasm.js', + OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js', + OGVDecoderVideoVP9SIMDW: 'ogv-decoder-video-vp9-simd-wasm.js', + OGVDecoderVideoVP9MTW: 'ogv-decoder-video-vp9-mt-wasm.js', + OGVDecoderVideoVP9SIMDMTW: 'ogv-decoder-video-vp9-simd-mt-wasm.js', + OGVDecoderVideoAV1W: 'ogv-decoder-video-av1-wasm.js', + OGVDecoderVideoAV1SIMDW: 'ogv-decoder-video-av1-simd-wasm.js', + OGVDecoderVideoAV1MTW: 'ogv-decoder-video-av1-mt-wasm.js', + OGVDecoderVideoAV1SIMDMTW: 'ogv-decoder-video-av1-simd-mt-wasm.js', +*/ +import { simd } from "wasm-feature-detect"; + +export async function loadVp9(callback) { + // Multithreading is used only if `options.threading` is true. + // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, + // currently available in Firefox and Chrome with experimental flags enabled. + // 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer + const isSIMD = await simd(); + console.log('isSIMD: ' + isSIMD); + window.OGVLoader.loadClass( + isSIMD ? "OGVDecoderVideoVP9SIMDW" : "OGVDecoderVideoVP9W", + (videoCodecClass) => { + window.videoCodecClass = videoCodecClass; + videoCodecClass({ videoFormat: {} }).then((decoder) => { + decoder.init(() => { + callback(decoder); + }) + }) + }, + { worker: true, threading: true } + ); +} \ No newline at end of file diff --git a/web/js/src/common.ts b/web/js/src/common.ts new file mode 100644 index 000000000..8da049a4d --- /dev/null +++ b/web/js/src/common.ts @@ -0,0 +1,77 @@ +import * as zstd from "zstddec"; +import { KeyEvent, controlKeyFromJSON, ControlKey } from "./message"; +import { KEY_MAP, LANGS } from "./gen_js_from_hbb"; + +let decompressor: zstd.ZSTDDecoder; + +export async function initZstd() { + const tmp = new zstd.ZSTDDecoder(); + await tmp.init(); + console.log("zstd ready"); + decompressor = tmp; +} + +export async function decompress(compressedArray: Uint8Array) { + const MAX = 1024 * 1024 * 64; + const MIN = 1024 * 1024; + let n = 30 * compressedArray.length; + if (n > MAX) { + n = MAX; + } + if (n < MIN) { + n = MIN; + } + try { + if (!decompressor) { + await initZstd(); + } + return decompressor.decode(compressedArray, n); + } catch (e) { + console.error("decompress failed: " + e); + return undefined; + } +} + +const LANG = getLang(); + +export function translate(locale: string, text: string): string { + const lang = LANG || locale.substring(locale.length - 2).toLowerCase(); + let en = LANGS.en as any; + let dict = (LANGS as any)[lang]; + if (!dict) dict = en; + let res = dict[text]; + if (!res && lang != "en") res = en[text]; + return res || text; +} + +const zCode = "z".charCodeAt(0); +const aCode = "a".charCodeAt(0); + +export function mapKey(name: string, isDesktop: Boolean) { + const tmp = KEY_MAP[name] || name; + if (tmp.length == 1) { + const chr = tmp.charCodeAt(0); + if (!isDesktop && (chr > zCode || chr < aCode)) + return KeyEvent.fromPartial({ unicode: chr }); + else return KeyEvent.fromPartial({ chr }); + } + const control_key = controlKeyFromJSON(tmp); + if (control_key == ControlKey.UNRECOGNIZED) { + console.error("Unknown control key " + tmp); + } + return KeyEvent.fromPartial({ control_key }); +} + +export async function sleep(ms: number) { + await new Promise((r) => setTimeout(r, ms)); +} + +function getLang(): string { + try { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + return urlParams.get("lang") || ""; + } catch (e) { + return ""; + } +} diff --git a/web/js/src/connection.ts b/web/js/src/connection.ts new file mode 100644 index 000000000..2846d9078 --- /dev/null +++ b/web/js/src/connection.ts @@ -0,0 +1,773 @@ +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() + ": 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, + 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(); +} diff --git a/web/js/src/globals.js b/web/js/src/globals.js new file mode 100644 index 000000000..a9eb941a5 --- /dev/null +++ b/web/js/src/globals.js @@ -0,0 +1,404 @@ +import Connection from "./connection"; +import _sodium from "libsodium-wrappers"; +import { CursorData } from "./message"; +import { loadVp9 } from "./codec"; +import { checkIfRetry, version } from "./gen_js_from_hbb"; +import { initZstd, translate } from "./common"; +import PCMPlayer from "pcm-player"; + +var currentFrame = undefined; +var events = []; + +window.curConn = undefined; +window.getRgba = () => { + const tmp = currentFrame; + currentFrame = undefined; + return tmp || null; +} +window.isMobile = () => { + return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) + || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4)); +} + +export function isDesktop() { + return !isMobile(); +} + +export function msgbox(type, title, text) { + if (!events) return; + if (!type || (type == 'error' && !text)) return; + const text2 = text.toLowerCase(); + var hasRetry = checkIfRetry(type, title, text) ? 'true' : ''; + events.push({ name: 'msgbox', type, title, text, hasRetry }); +} + +function jsonfyForDart(payload) { + var tmp = {}; + for (const [key, value] of Object.entries(payload)) { + if (!key) continue; + tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value); + } + return tmp; +} + +export function pushEvent(name, payload) { + if (!events) return; + payload = jsonfyForDart(payload); + payload.name = name; + events.push(payload); +} + +let yuvWorker; +let yuvCanvas; +let gl; +let pixels; +let flipPixels; +let oldSize; +if (YUVCanvas.WebGLFrameSink.isAvailable()) { + var canvas = document.createElement('canvas'); + yuvCanvas = YUVCanvas.attach(canvas, { webGL: true }); + gl = canvas.getContext("webgl"); +} else { + yuvWorker = new Worker("./yuv.js"); +} +let testSpeed = [0, 0]; + +export function draw(frame) { + if (yuvWorker) { + // frame's (y/u/v).bytes already detached, can not transferrable any more. + yuvWorker.postMessage(frame); + } else { + var tm0 = new Date().getTime(); + yuvCanvas.drawFrame(frame); + var width = canvas.width; + var height = canvas.height; + var size = width * height * 4; + if (size != oldSize) { + pixels = new Uint8Array(size); + flipPixels = new Uint8Array(size); + oldSize = size; + } + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + const row = width * 4; + const end = (height - 1) * row; + for (let i = 0; i < size; i += row) { + flipPixels.set(pixels.subarray(i, i + row), end - i); + } + currentFrame = flipPixels; + testSpeed[1] += new Date().getTime() - tm0; + testSpeed[0] += 1; + if (testSpeed[0] > 30) { + console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0])); + testSpeed = [0, 0]; + } + } + /* + var testCanvas = document.getElementById("test-yuv-decoder-canvas"); + if (testCanvas && currentFrame) { + var ctx = testCanvas.getContext("2d"); + testCanvas.width = frame.format.displayWidth; + testCanvas.height = frame.format.displayHeight; + var img = ctx.createImageData(testCanvas.width, testCanvas.height); + img.data.set(currentFrame); + ctx.putImageData(img, 0, 0); + } + */ +} + +export function sendOffCanvas(c) { + let canvas = c.transferControlToOffscreen(); + yuvWorker.postMessage({ canvas }, [canvas]); +} + +export function setConn(conn) { + window.curConn = conn; +} + +export function getConn() { + return window.curConn; +} + +export async function startConn(id) { + currentFrame = undefined; + events = []; + setByName('remote_id', id); + await curConn.start(id); +} + +export function close() { + getConn()?.close(); + setConn(undefined); + currentFrame = undefined; + events = undefined; +} + +export function newConn() { + window.curConn?.close(); + const conn = new Connection(); + setConn(conn); + return conn; +} + +let sodium; +export async function verify(signed, pk) { + if (!sodium) { + await _sodium.ready; + sodium = _sodium; + } + if (typeof pk == 'string') { + pk = decodeBase64(pk); + } + return sodium.crypto_sign_open(signed, pk); +} + +export function decodeBase64(pk) { + return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL); +} + +export function genBoxKeyPair() { + const pair = sodium.crypto_box_keypair(); + const sk = pair.privateKey; + const pk = pair.publicKey; + return [sk, pk]; +} + +export function genSecretKey() { + return sodium.crypto_secretbox_keygen(); +} + +export function seal(unsigned, theirPk, ourSk) { + const nonce = Uint8Array.from(Array(24).fill(0)); + return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk); +} + +function makeOnce(value) { + var byteArray = Array(24).fill(0); + + for (var index = 0; index < byteArray.length && value > 0; index++) { + var byte = value & 0xff; + byteArray[index] = byte; + value = (value - byte) / 256; + } + + return Uint8Array.from(byteArray); +}; + +export function encrypt(unsigned, nonce, key) { + return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key); +} + +export function decrypt(signed, nonce, key) { + return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key); +} + +window.setByName = (name, value) => { + switch (name) { + case 'remote_id': + localStorage.setItem('remote-id', value); + break; + case 'connect': + newConn(); + startConn(value); + break; + case 'login': + value = JSON.parse(value); + curConn.setRemember(value.remember == 'true'); + curConn.login(value.password); + break; + case 'close': + close(); + break; + case 'refresh': + curConn.refresh(); + break; + case 'reconnect': + curConn.reconnect(); + break; + case 'toggle_option': + curConn.toggleOption(value); + break; + case 'image_quality': + curConn.setImageQuality(value); + break; + case 'lock_screen': + curConn.lockScreen(); + break; + case 'ctrl_alt_del': + curConn.ctrlAltDel(); + break; + case 'switch_display': + curConn.switchDisplay(value); + break; + case 'remove': + const peers = getPeers(); + delete peers[value]; + localStorage.setItem('peers', JSON.stringify(peers)); + break; + case 'input_key': + value = JSON.parse(value); + curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); + break; + case 'input_string': + curConn.inputString(value); + break; + case 'send_mouse': + let mask = 0; + value = JSON.parse(value); + switch (value.type) { + case 'down': + mask = 1; + break; + case 'up': + mask = 2; + break; + case 'wheel': + mask = 3; + break; + } + switch (value.buttons) { + case 'left': + mask |= 1 << 3; + break; + case 'right': + mask |= 2 << 3; + break; + case 'wheel': + mask |= 4 << 3; + } + curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); + break; + case 'option': + value = JSON.parse(value); + localStorage.setItem(value.name, value.value); + break; + case 'peer_option': + value = JSON.parse(value); + curConn.setOption(value.name, value.value); + break; + case 'input_os_password': + curConn.inputOsPassword(value); + break; + default: + break; + } +} + +window.getByName = (name, arg) => { + let v = _getByName(name, arg); + if (typeof v == 'string' || v instanceof String) return v; + if (v == undefined || v == null) return ''; + return JSON.stringify(v); +} + +function getPeersForDart() { + const peers = []; + for (const [id, value] of Object.entries(getPeers())) { + if (!id) continue; + const tm = value['tm']; + const info = value['info']; + if (!tm || !info) continue; + peers.push([tm, id, info]); + } + return peers.sort().reverse().map(x => x.slice(1)); +} + +function _getByName(name, arg) { + switch (name) { + case 'peers': + return getPeersForDart(); + case 'remote_id': + return localStorage.getItem('remote-id'); + case 'remember': + return curConn.getRemember(); + case 'event': + if (events && events.length) { + const e = events[0]; + events.splice(0, 1); + return JSON.stringify(e); + } + break; + case 'toggle_option': + return curConn.getOption(arg) || false; + case 'option': + return localStorage.getItem(arg); + case 'image_quality': + return curConn.getImageQuality(); + case 'translate': + arg = JSON.parse(arg); + return translate(arg.locale, arg.text); + case 'peer_option': + return curConn.getOption(arg); + case 'test_if_valid_server': + break; + case 'version': + return version; + } + return ''; +} + +let opusWorker = new Worker("./libopus.js"); +let pcmPlayer; + +export function initAudio(channels, sampleRate) { + pcmPlayer = newAudioPlayer(channels, sampleRate); + opusWorker.postMessage({ channels, sampleRate }); +} + +export function playAudio(packet) { + opusWorker.postMessage(packet, [packet.buffer]); +} + +window.init = async () => { + if (yuvWorker) { + yuvWorker.onmessage = (e) => { + currentFrame = e.data; + } + } + opusWorker.onmessage = (e) => { + pcmPlayer.feed(e.data); + } + loadVp9(() => { }); + await initZstd(); + console.log('init done'); +} + +export function getPeers() { + try { + return JSON.parse(localStorage.getItem('peers')) || {}; + } catch (e) { + return {}; + } +} + +function newAudioPlayer(channels, sampleRate) { + return new PCMPlayer({ + channels, + sampleRate, + flushingTime: 2000 + }); +} + +export function copyToClipboard(text) { + if (window.clipboardData && window.clipboardData.setData) { + // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. + return window.clipboardData.setData("Text", text); + + } + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { + var textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. + document.body.appendChild(textarea); + textarea.select(); + try { + return document.execCommand("copy"); // Security exception may be thrown by some browsers. + } + catch (ex) { + console.warn("Copy to clipboard failed.", ex); + // return prompt("Copy to clipboard: Ctrl+C, Enter", text); + } + finally { + document.body.removeChild(textarea); + } + } +} \ No newline at end of file diff --git a/web/js/src/main.ts b/web/js/src/main.ts new file mode 100644 index 000000000..2be877f58 --- /dev/null +++ b/web/js/src/main.ts @@ -0,0 +1,2 @@ +import "./globals"; +import "./ui"; \ No newline at end of file diff --git a/web/js/src/style.css b/web/js/src/style.css new file mode 100644 index 000000000..852de7aa2 --- /dev/null +++ b/web/js/src/style.css @@ -0,0 +1,8 @@ +#app { + font-family: Avenir, Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-align: center; + color: #2c3e50; + margin-top: 60px; +} diff --git a/web/js/src/ui.js b/web/js/src/ui.js new file mode 100644 index 000000000..446334022 --- /dev/null +++ b/web/js/src/ui.js @@ -0,0 +1,108 @@ +import "./style.css"; +import "./connection"; +import * as globals from "./globals"; + +const app = document.querySelector('#app'); + +if (app) { + app.innerHTML = ` +
+ + + + +
Host:
Key:
Id:
+ + + +`; + + let player; + window.init(); + + document.body.onload = () => { + const host = document.querySelector('#host'); + host.value = localStorage.getItem('custom-rendezvous-server'); + const id = document.querySelector('#id'); + id.value = localStorage.getItem('id'); + const key = document.querySelector('#key'); + key.value = localStorage.getItem('key'); + player = YUVCanvas.attach(document.getElementById('player')); + // globals.sendOffCanvas(document.getElementById('player')); + }; + + window.connect = () => { + const host = document.querySelector('#host'); + localStorage.setItem('custom-rendezvous-server', host.value); + const id = document.querySelector('#id'); + localStorage.setItem('id', id.value); + const key = document.querySelector('#key'); + localStorage.setItem('key', key.value); + const func = async () => { + const conn = globals.newConn(); + conn.setMsgbox(msgbox); + conn.setDraw((f) => { + /* + if (!(document.getElementById('player').width > 0)) { + document.getElementById('player').width = f.format.displayWidth; + document.getElementById('player').height = f.format.displayHeight; + } + */ + globals.draw(f); + player.drawFrame(f); + }); + document.querySelector('div#status').style.display = 'block'; + document.querySelector('div#connect').style.display = 'none'; + document.querySelector('div#text').innerHTML = 'Connecting ...'; + await conn.start(id.value); + }; + func(); + } + + function msgbox(type, title, text) { + if (!globals.getConn()) return; + if (type == 'input-password') { + document.querySelector('div#status').style.display = 'none'; + document.querySelector('div#password').style.display = 'block'; + } else if (!type) { + document.querySelector('div#canvas').style.display = 'block'; + document.querySelector('div#password').style.display = 'none'; + document.querySelector('div#status').style.display = 'none'; + } else if (type == 'error') { + document.querySelector('div#status').style.display = 'block'; + document.querySelector('div#canvas').style.display = 'none'; + document.querySelector('div#text').innerHTML = '
' + text + '
'; + } else { + document.querySelector('div#password').style.display = 'none'; + document.querySelector('div#status').style.display = 'block'; + document.querySelector('div#text').innerHTML = '
' + text + '
'; + } + } + + window.cancel = () => { + globals.close(); + document.querySelector('div#connect').style.display = 'block'; + document.querySelector('div#password').style.display = 'none'; + document.querySelector('div#status').style.display = 'none'; + document.querySelector('div#canvas').style.display = 'none'; + } + + window.confirm = () => { + const password = document.querySelector('input#password').value; + if (password) { + document.querySelector('div#password').style.display = 'none'; + globals.getConn().login(password); + } + } +} \ No newline at end of file diff --git a/web/js/src/vite-env.d.ts b/web/js/src/vite-env.d.ts new file mode 100644 index 000000000..151aa6856 --- /dev/null +++ b/web/js/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/web/js/src/websock.ts b/web/js/src/websock.ts new file mode 100644 index 000000000..6f05e6f6b --- /dev/null +++ b/web/js/src/websock.ts @@ -0,0 +1,183 @@ +import * as message from "./message.js"; +import * as rendezvous from "./rendezvous.js"; +import * as globals from "./globals"; + +type Keys = "message" | "open" | "close" | "error"; + +export default class Websock { + _websocket: WebSocket; + _eventHandlers: { [key in Keys]: Function }; + _buf: (rendezvous.RendezvousMessage | message.Message)[]; + _status: any; + _latency: number; + _secretKey: [Uint8Array, number, number] | undefined; + _uri: string; + _isRendezvous: boolean; + + constructor(uri: string, isRendezvous: boolean = true) { + this._eventHandlers = { + message: (_: any) => {}, + open: () => {}, + close: () => {}, + error: () => {}, + }; + this._uri = uri; + this._status = ""; + this._buf = []; + this._websocket = new WebSocket(uri); + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.binaryType = "arraybuffer"; + this._latency = new Date().getTime(); + this._isRendezvous = isRendezvous; + } + + latency(): number { + return this._latency; + } + + setSecretKey(key: Uint8Array) { + this._secretKey = [key, 0, 0]; + } + + sendMessage(json: message.DeepPartial) { + let data = message.Message.encode( + message.Message.fromPartial(json) + ).finish(); + let k = this._secretKey; + if (k) { + k[1] += 1; + data = globals.encrypt(data, k[1], k[0]); + } + this._websocket.send(data); + } + + sendRendezvous(data: rendezvous.DeepPartial) { + this._websocket.send( + rendezvous.RendezvousMessage.encode( + rendezvous.RendezvousMessage.fromPartial(data) + ).finish() + ); + } + + parseMessage(data: Uint8Array) { + return message.Message.decode(data); + } + + parseRendezvous(data: Uint8Array) { + return rendezvous.RendezvousMessage.decode(data); + } + + // Event Handlers + off(evt: Keys) { + this._eventHandlers[evt] = () => {}; + } + + on(evt: Keys, handler: Function) { + this._eventHandlers[evt] = handler; + } + + async open(timeout: number = 12000): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (this._status != "open") { + reject(this._status || "Timeout"); + } + }, timeout); + this._websocket.onopen = () => { + this._latency = new Date().getTime() - this._latency; + this._status = "open"; + console.debug(">> WebSock.onopen"); + if (this._websocket?.protocol) { + console.info( + "Server choose sub-protocol: " + this._websocket.protocol + ); + } + + this._eventHandlers.open(); + console.info("WebSock.onopen"); + resolve(this); + }; + this._websocket.onclose = (e) => { + if (this._status == "open") { + // e.code 1000 means that the connection was closed normally. + // + } + this._status = e; + console.error("WebSock.onclose: "); + console.error(e); + this._eventHandlers.close(e); + reject("Reset by the peer"); + }; + this._websocket.onerror = (e: any) => { + if (!this._status) { + reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server"); + return; + } + this._status = e; + console.error("WebSock.onerror: ") + console.error(e); + this._eventHandlers.error(e); + }; + }); + } + + async next( + timeout = 12000 + ): Promise { + const func = ( + resolve: (value: rendezvous.RendezvousMessage | message.Message) => void, + reject: (reason: any) => void, + tm0: number + ) => { + if (this._buf.length) { + resolve(this._buf[0]); + this._buf.splice(0, 1); + } else { + if (this._status != "open") { + reject(this._status); + return; + } + if (new Date().getTime() > tm0 + timeout) { + reject("Timeout"); + } else { + setTimeout(() => func(resolve, reject, tm0), 1); + } + } + }; + return new Promise((resolve, reject) => { + func(resolve, reject, new Date().getTime()); + }); + } + + close() { + this._status = ""; + if (this._websocket) { + if ( + this._websocket.readyState === WebSocket.OPEN || + this._websocket.readyState === WebSocket.CONNECTING + ) { + console.info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = () => {}; + } + } + + _recv_message(e: any) { + if (e.data instanceof window.ArrayBuffer) { + let bytes = new Uint8Array(e.data); + const k = this._secretKey; + if (k) { + k[2] += 1; + bytes = globals.decrypt(bytes, k[2], k[0]); + } + this._buf.push( + this._isRendezvous + ? this.parseRendezvous(bytes) + : this.parseMessage(bytes) + ); + } + this._eventHandlers.message(e.data); + } +} diff --git a/web/js/ts_proto.py b/web/js/ts_proto.py new file mode 100755 index 000000000..f917c6b37 --- /dev/null +++ b/web/js/ts_proto.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import os + +path = os.path.abspath(os.path.join(os.getcwd(), '..', 'hbb', 'libs', 'hbb_common', 'protos')) + +if os.name == 'nt': + cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path + print(cmd) + os.system(cmd) + cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ message.proto'%path + print(cmd) + os.system(cmd) +else: + cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path + print(cmd) + os.system(cmd) + cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ message.proto'%path + print(cmd) + os.system(cmd) diff --git a/web/js/tsconfig.json b/web/js/tsconfig.json new file mode 100644 index 000000000..ca949de6a --- /dev/null +++ b/web/js/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "allowJs": true, + "lib": [ + "ESNext", + "DOM" + ], + "moduleResolution": "Node", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "include": [ + "./src" + ] +} \ No newline at end of file diff --git a/web/js/yarn.lock b/web/js/yarn.lock new file mode 100644 index 000000000..cf4caafe5 --- /dev/null +++ b/web/js/yarn.lock @@ -0,0 +1,374 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= + +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + +"@types/node@>=13.7.0": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" + integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== + +"@types/object-hash@^1.3.0": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b" + integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA== + +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + +esbuild-android-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44" + integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg== + +esbuild-darwin-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72" + integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ== + +esbuild-darwin-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a" + integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ== + +esbuild-freebsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85" + integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA== + +esbuild-freebsd-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52" + integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ== + +esbuild-linux-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69" + integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g== + +esbuild-linux-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3" + integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA== + +esbuild-linux-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1" + integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA== + +esbuild-linux-arm@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe" + integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA== + +esbuild-linux-mips64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7" + integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg== + +esbuild-linux-ppc64le@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2" + integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ== + +esbuild-netbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038" + integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w== + +esbuild-openbsd-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7" + integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== + +esbuild-sunos-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4" + integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw== + +esbuild-windows-32@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7" + integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw== + +esbuild-windows-64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294" + integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ== + +esbuild-windows-arm64@0.13.15: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3" + integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA== + +esbuild@^0.13.12: + version "0.13.15" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf" + integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw== + optionalDependencies: + esbuild-android-arm64 "0.13.15" + esbuild-darwin-64 "0.13.15" + esbuild-darwin-arm64 "0.13.15" + esbuild-freebsd-64 "0.13.15" + esbuild-freebsd-arm64 "0.13.15" + esbuild-linux-32 "0.13.15" + esbuild-linux-64 "0.13.15" + esbuild-linux-arm "0.13.15" + esbuild-linux-arm64 "0.13.15" + esbuild-linux-mips64le "0.13.15" + esbuild-linux-ppc64le "0.13.15" + esbuild-netbsd-64 "0.13.15" + esbuild-openbsd-64 "0.13.15" + esbuild-sunos-64 "0.13.15" + esbuild-windows-32 "0.13.15" + esbuild-windows-64 "0.13.15" + esbuild-windows-arm64 "0.13.15" + +fast-sha256@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-sha256/-/fast-sha256-1.3.0.tgz#7916ba2054eeb255982608cccd0f6660c79b7ae6" + integrity sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +is-core-module@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +libsodium-wrappers@^0.7.9: + version "0.7.9" + resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#4ffc2b69b8f7c7c7c5594a93a4803f80f6d0f346" + integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ== + dependencies: + libsodium "^0.7.0" + +libsodium@^0.7.0, libsodium@^0.7.9: + version "0.7.9" + resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b" + integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A== + +lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +nanoid@^3.1.30: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + +object-hash@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" + integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +pcm-player@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pcm-player/-/pcm-player-0.0.11.tgz#1dd0e37d8e1238c10b153079939b1fc88958a9ff" + integrity sha512-+FmX62jiqZa7wDCqSRQ1g3DuU6JNgpymgOLCWhmiE/Lj/M+rOUNqgNwVQX509LdA9dtBtVD3EQQUSp9JqU6upw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@^8.4.5: + version "8.4.5" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + +prettier@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== + +protobufjs@^6.8.8: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + +resolve@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f" + integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== + dependencies: + is-core-module "^2.8.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +rollup@^2.59.0: + version "2.64.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.64.0.tgz#f0f59774e21fbb56de438a37d06a2189632b207a" + integrity sha512-+c+lbw1lexBKSMb1yxGDVfJ+vchJH3qLbmavR+awDinTDA2C5Ug9u7lkOzj62SCu0PKUExsW36tpgW7Fmpn3yQ== + optionalDependencies: + fsevents "~2.3.2" + +source-map-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" + integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +ts-poet@^4.5.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.10.0.tgz#8732374655e87f8f833e5d110938e346713e8c66" + integrity sha512-V5xzt+LDMVtxWvK12WVwHhGHTA//CeoPdWOqka0mMjlRqq7RPKYSfWEnzJdMmhNbd34BwZuZpip4mm+nqEcbQA== + dependencies: + lodash "^4.17.15" + prettier "^2.5.1" + +ts-proto-descriptors@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5" + integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw== + dependencies: + long "^4.0.0" + protobufjs "^6.8.8" + +ts-proto@^1.101.0: + version "1.101.0" + resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.101.0.tgz#f8ce4523a0cb32ff224ff8a5759c2c046bf96244" + integrity sha512-XUV0WKQ3icHMToOOpUjf0RCKF9md+Lu9TV00LQlZ6ailJhbBqLh0BXQJ1PFUGxEW8YV65Wc/N8vAir152OE2Sg== + dependencies: + "@types/object-hash" "^1.3.0" + dataloader "^1.4.0" + object-hash "^1.3.1" + protobufjs "^6.8.8" + ts-poet "^4.5.0" + ts-proto-descriptors "^1.2.1" + +typescript@^4.4.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" + integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== + +vite@^2.7.2: + version "2.7.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.12.tgz#7784ab19e7ff98f6a192d2d7d877480a8c2b7e7d" + integrity sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ== + dependencies: + esbuild "^0.13.12" + postcss "^8.4.5" + resolve "^1.20.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2" + +wasm-feature-detect@^1.2.11: + version "1.2.11" + resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz#e21992fd1f1d41a47490e392a5893cb39d81e29e" + integrity sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w== + +zstddec@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.0.2.tgz#57e2f28dd1ff56b750e07d158a43f0611ad9eeb4" + integrity sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==