diff --git a/package.json b/package.json index a6d708e94..611351fff 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "libsodium": "^0.7.9", "libsodium-wrappers": "^0.7.9", "ogv": "^1.8.6", - "ts-proto": "^1.101.0" + "ts-proto": "^1.101.0", + "zstddec": "^0.0.2" } } diff --git a/src/connection.ts b/src/connection.ts index 05df1edda..6cbba8c12 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -19,16 +19,22 @@ export default class Connection { _interval: any; _id: string; _hash: message.Hash | undefined; - _msgbox: MsgboxCallback | undefined; - _draw: DrawCallback | undefined; + _msgbox: MsgboxCallback; + _draw: DrawCallback; _peerInfo: message.PeerInfo | undefined; _firstFrame: Boolean | undefined; _videoDecoder: any; _audioDecoder: any; + _password: string | undefined; constructor() { + this._msgbox = globals.msgbox; + this._draw = globals.draw; this._msgs = []; this._id = ""; + } + + async start(id: string) { this._interval = setInterval(() => { while (this._msgs.length) { this._ws?.sendMessage(this._msgs[0]); @@ -44,9 +50,6 @@ export default class Connection { this._audioDecoder = decoder; console.log("opus loaded"); }); - } - - async start(id: string) { const uri = getDefaultUri(); const ws = new Websock(uri); this._ws = ws; @@ -69,7 +72,7 @@ export default class Connection { const phr = msg.punchHoleResponse; const rr = msg.relayResponse; if (phr) { - if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNKNOWN) { + 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"); @@ -110,7 +113,8 @@ export default class Connection { uuid, }); ws.sendRendezvous({ requestRelay }); - await this.secure(pk); + const secure = (await this.secure(pk)) || false; + globals.pushEvent("connection_ready", { secure, direct: false }); await this.msgLoop(); } @@ -179,6 +183,7 @@ export default class Connection { }); await this._ws?.sendMessage({ publicKey }); this._ws?.setSecretKey(secretKey); + return true; } async msgLoop() { @@ -186,7 +191,7 @@ export default class Connection { const msg = this._ws?.parseMessage(await this._ws?.next()); if (msg?.hash) { this._hash = msg?.hash; - await this.handleHash(); + await this.login(this._password); this.msgbox("input-password", "Password Required", ""); } else if (msg?.testDelay) { const testDelay = msg?.testDelay; @@ -198,23 +203,30 @@ export default class Connection { if (r.error) { this.msgbox("error", "Error", r.error); } else if (r.peerInfo) { - this._peerInfo = r.peerInfo; - this.msgbox( - "success", - "Successful", - "Connected, waiting for image..." - ); + this.handlePeerInfo(r.peerInfo); } } else if (msg?.videoFrame) { this.handleVideoFrame(msg?.videoFrame!); + } else if (msg?.clipboard) { + const cb = msg?.clipboard; + if (cb.compress) cb.content = globals.decompress(cb.content); + globals.pushEvent("clipboard", cb); + } else if (msg?.cursorData) { + const cd = msg?.cursorData; + cd.colors = globals.decompress(cd.colors); + globals.pushEvent("cursor_data", cd); + } else if (msg?.cursorId) { + globals.pushEvent("cursor_id", { id: msg?.cursorId }); + } else if (msg?.cursorPosition) { + globals.pushEvent("cursor_position", msg?.cursorPosition); + } else if (msg?.misc) { + this.handleMisc(msg?.misc); + } else if (msg?.audioFrame) { + // } } } - async handleHash() { - await this._sendLoginMessage(); - } - msgbox(type_: string, title: string, text: string) { this._msgbox?.(type_, title, text); } @@ -224,12 +236,20 @@ export default class Connection { } close() { + this._msgs = []; clearInterval(this._interval); this._ws?.close(); this._videoDecoder?.close(); this._audioDecoder?.close(); } + async refresh() { + const misc = message.Misc.fromPartial({ + refreshVideo: true, + }); + await this._ws?.sendMessage({ misc }); + } + setMsgbox(callback: MsgboxCallback) { this._msgbox = callback; } @@ -238,19 +258,27 @@ export default class Connection { this._draw = callback; } - async login(password: string) { + async login(password: string | undefined, _remember: Boolean = false) { + this._password = password; this.msgbox("connecting", "Connecting...", "Logging in..."); - let salt = this._hash?.salt; - if (salt) { + const salt = this._hash?.salt; + if (salt && password) { let p = hash([password, salt]); - let challenge = this._hash?.challenge; + const challenge = this._hash?.challenge; if (challenge) { p = hash([p, challenge]); await this._sendLoginMessage(p); } + } else { + await this._sendLoginMessage(); } } + async reconnect() { + this.close(); + await this.start(this._id); + } + async _sendLoginMessage(password: Uint8Array | undefined = undefined) { const loginRequest = message.LoginRequest.fromPartial({ username: this._id!, @@ -267,7 +295,7 @@ export default class Connection { this._firstFrame = true; } if (vf.vp9s) { - let dec = this._videoDecoder; + const dec = this._videoDecoder; // dec.sync(); vf.vp9s.frames.forEach((f) => { dec.processFrame(f.data.slice(0).buffer, (ok: any) => { @@ -278,6 +306,44 @@ export default class Connection { }); } } + + 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); + } + + handleMisc(misc: message.Misc) { + if (misc.audioFormat) { + // + } else if (misc.permissionInfo) { + const p = misc.permissionInfo; + 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.switchDisplay) { + globals.pushEvent("switch_display", misc.switchDisplay); + } else if (misc.closeReason) { + this.msgbox("error", "Connection Error", misc.closeReason); + } + } } // @ts-ignore diff --git a/src/globals.js b/src/globals.js index cd3828dec..43d5a8022 100644 --- a/src/globals.js +++ b/src/globals.js @@ -1,7 +1,39 @@ import Connection from "./connection"; import _sodium from "libsodium-wrappers"; +import { ZSTDecoder } from 'zstddec'; + +const decompressor = new ZSTDDecoder(); +await decompressor.init(); + +var currentFrame = undefined; +var events = []; window.currentConnection = undefined; +window.getRgba = () => currentFrame; +window.getLanguage = () => navigator.language; + +export function msgbox(type, title, text) { + text = text.toLowerCase(); + var hasRetry = msgtype == "error" + && title == "Connection Error" + && !text.indexOf("offline") >= 0 + && !text.indexOf("exist") >= 0 + && !text.indexOf("handshake") >= 0 + && !text.indexOf("failed") >= 0 + && !text.indexOf("resolve") >= 0 + && !text.indexOf("mismatch") >= 0 + && !text.indexOf("manually") >= 0; + events.push({ name: 'msgbox', type, title, text, hasRetry }); +} + +export function pushEvent(name, payload) { + payload.name = name; + events.push(payload); +} + +export function draw(frame) { + currentFrame = frame; +} export function setConn(conn) { window.currentConnection = conn; @@ -14,6 +46,7 @@ export function getConn() { export function close() { getConn()?.close(); setConn(undefined); + currentFrame = undefined; } export function newConn() { @@ -73,4 +106,58 @@ export function encrypt(unsigned, nonce, key) { export function decrypt(signed, nonce, key) { return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key); +} + +export function decompress(compressedArray) { + const MAX = 1024 * 1024 * 64; + const MIN = 1024 * 1024; + let n = 30 * data.length; + if (n > MAX) { + n = MAX; + } + if (n < MIN) { + n = MIN; + } + try { + return decompressor.decode(compressedArray, n); + } catch (e) { + console.error('decompress failed: ' + e); + } +} + +window.setByName = (name, value) => { + switch (name) { + case 'connect': + newConn(); + break; + case 'login': + currentConnection.login(value.password, value.remember); + break; + case 'close': + close(); + break; + case 'refresh': + currentConnection.refresh(); + break; + case 'reconnect': + currentConnection.reconnect(); + break; + default: + break; + } +} + +window.getByName = (name, value) => { + switch (name) { + case 'peers': + return localStorage.getItem('peers'); + break; + case 'event': + if (events.length) { + const e = events[0]; + events.splice(0, 1); + return e; + } + break; + } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2e61c8dbf..c8e30b076 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,3 +376,8 @@ vite@^2.7.2: rollup "^2.59.0" optionalDependencies: fsevents "~2.3.2" + +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==