diff --git a/app/locale/fr.json b/app/locale/fr.json index 19e8255b..22531f73 100644 --- a/app/locale/fr.json +++ b/app/locale/fr.json @@ -1,21 +1,22 @@ { + "HTTPS is required for full functionality": "", "Connecting...": "En cours de connexion...", "Disconnecting...": "Déconnexion en cours...", "Reconnecting...": "Reconnexion en cours...", "Internal error": "Erreur interne", "Must set host": "Doit définir l'hôte", - "Connected (encrypted) to ": "Connecté (crypté) à ", - "Connected (unencrypted) to ": "Connecté (non crypté) à ", - "Something went wrong, connection is closed": "Quelque chose est arrivé, la connexion est fermée", + "Connected (encrypted) to ": "Connecté (chiffré) à ", + "Connected (unencrypted) to ": "Connecté (non chiffré) à ", + "Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée", "Failed to connect to server": "Échec de connexion au serveur", "Disconnected": "Déconnecté", - "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec raison: ", + "New connection has been rejected with reason: ": "Une nouvelle connexion a été rejetée avec motif : ", "New connection has been rejected": "Une nouvelle connexion a été rejetée", "Credentials are required": "Les identifiants sont requis", - "noVNC encountered an error:": "noVNC a rencontré une erreur:", + "noVNC encountered an error:": "noVNC a rencontré une erreur :", "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle", "Drag": "Faire glisser", - "Move/Drag Viewport": "Déplacer/faire glisser Viewport", + "Move/Drag Viewport": "Déplacer/faire glisser le Viewport", "Keyboard": "Clavier", "Show Keyboard": "Afficher le clavier", "Extra keys": "Touches supplémentaires", @@ -39,34 +40,39 @@ "Reboot": "Redémarrer", "Reset": "Réinitialiser", "Clipboard": "Presse-papiers", - "Clear": "Effacer", - "Fullscreen": "Plein écran", + "Edit clipboard content in the textarea below.": "", "Settings": "Paramètres", "Shared Mode": "Mode partagé", "View Only": "Afficher uniquement", "Clip to Window": "Clip à fenêtre", - "Scaling Mode:": "Mode mise à l'échelle:", + "Scaling Mode:": "Mode mise à l'échelle :", "None": "Aucun", "Local Scaling": "Mise à l'échelle locale", "Remote Resizing": "Redimensionnement à distance", "Advanced": "Avancé", - "Quality:": "Qualité:", - "Compression level:": "Niveau de compression:", - "Repeater ID:": "ID Répéteur:", + "Quality:": "Qualité :", + "Compression level:": "Niveau de compression :", + "Repeater ID:": "ID Répéteur :", "WebSocket": "WebSocket", - "Encrypt": "Crypter", - "Host:": "Hôte:", - "Port:": "Port:", - "Path:": "Chemin:", + "Encrypt": "Chiffrer", + "Host:": "Hôte :", + "Port:": "Port :", + "Path:": "Chemin :", "Automatic Reconnect": "Reconnecter automatiquemen", - "Reconnect Delay (ms):": "Délai de reconnexion (ms):", + "Reconnect Delay (ms):": "Délai de reconnexion (ms) :", "Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur", - "Logging:": "Se connecter:", - "Version:": "Version:", + "Logging:": "Se connecter :", + "Version:": "Version :", "Disconnect": "Déconnecter", "Connect": "Connecter", - "Username:": "Nom d'utilisateur:", - "Password:": "Mot de passe:", + "Server identity": "", + "The server has provided the following identifying information:": "", + "Fingerprint:": "", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "", + "Approve": "", + "Reject": "", + "Username:": "Nom d'utilisateur :", + "Password:": "Mot de passe :", "Send Credentials": "Envoyer les identifiants", "Cancel": "Annuler" } \ No newline at end of file diff --git a/app/locale/sv.json b/app/locale/sv.json index e46df45b..077ef42c 100644 --- a/app/locale/sv.json +++ b/app/locale/sv.json @@ -1,4 +1,5 @@ { + "HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet", "Connecting...": "Ansluter...", "Disconnecting...": "Kopplar ner...", "Reconnecting...": "Återansluter...", @@ -39,8 +40,8 @@ "Reboot": "Boota om", "Reset": "Återställ", "Clipboard": "Urklipp", - "Clear": "Rensa", - "Fullscreen": "Fullskärm", + "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.", + "Full Screen": "Fullskärm", "Settings": "Inställningar", "Shared Mode": "Delat Läge", "View Only": "Endast Visning", @@ -65,6 +66,13 @@ "Version:": "Version:", "Disconnect": "Koppla från", "Connect": "Anslut", + "Server identity": "Server-identitet", + "The server has provided the following identifying information:": "Servern har gett följande identifierande information:", + "Fingerprint:": "Fingeravtryck:", + "Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Kontrollera att informationen är korrekt och tryck sedan \"Godkänn\". Tryck annars \"Neka\".", + "Approve": "Godkänn", + "Reject": "Neka", + "Credentials": "Användaruppgifter", "Username:": "Användarnamn:", "Password:": "Lösenord:", "Send Credentials": "Skicka Användaruppgifter", diff --git a/app/localization.js b/app/localization.js index 84341da6..7d7e6e6a 100644 --- a/app/localization.js +++ b/app/localization.js @@ -16,13 +16,19 @@ export class Localizer { this.language = 'en'; // Current dictionary of translations - this.dictionary = undefined; + this._dictionary = undefined; } // Configure suitable language based on user preferences - setup(supportedLanguages) { + async setup(supportedLanguages, baseURL) { this.language = 'en'; // Default: US English + this._dictionary = undefined; + this._setupLanguage(supportedLanguages); + await this._setupDictionary(baseURL); + } + + _setupLanguage(supportedLanguages) { /* * Navigator.languages only available in Chrome (32+) and FireFox (32+) * Fall back to navigator.language for other browsers @@ -40,12 +46,6 @@ export class Localizer { .replace("_", "-") .split("-"); - // Built-in default? - if ((userLang[0] === 'en') && - ((userLang[1] === undefined) || (userLang[1] === 'us'))) { - return; - } - // First pass: perfect match for (let j = 0; j < supportedLanguages.length; j++) { const supLang = supportedLanguages[j] @@ -64,7 +64,12 @@ export class Localizer { return; } - // Second pass: fallback + // Second pass: English fallback + if (userLang[0] === 'en') { + return; + } + + // Third pass pass: other fallback for (let j = 0;j < supportedLanguages.length;j++) { const supLang = supportedLanguages[j] .toLowerCase() @@ -84,10 +89,32 @@ export class Localizer { } } + async _setupDictionary(baseURL) { + if (baseURL) { + if (!baseURL.endsWith("/")) { + baseURL = baseURL + "/"; + } + } else { + baseURL = ""; + } + + if (this.language === "en") { + return; + } + + let response = await fetch(baseURL + this.language + ".json"); + if (!response.ok) { + throw Error("" + response.status + " " + response.statusText); + } + + this._dictionary = await response.json(); + } + // Retrieve localised text get(id) { - if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { - return this.dictionary[id]; + if (typeof this._dictionary !== 'undefined' && + this._dictionary[id]) { + return this._dictionary[id]; } else { return id; } diff --git a/app/styles/base.css b/app/styles/base.css index 06e736a9..f83ad4b9 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -661,7 +661,7 @@ html { justify-content: center; align-content: center; - line-height: 25px; + line-height: 1.6; word-wrap: break-word; color: #fff; @@ -887,7 +887,7 @@ html { .noVNC_logo { color:yellow; font-family: 'Orbitron', 'OrbitronTTF', sans-serif; - line-height:90%; + line-height: 0.9; text-shadow: 0.1em 0.1em 0 black; } .noVNC_logo span{ diff --git a/app/styles/input.css b/app/styles/input.css index c5897ab3..dc345aab 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -86,13 +86,16 @@ option { * Checkboxes */ input[type=checkbox] { - position: relative; + display: inline-flex; + justify-content: center; + align-items: center; background-color: white; background-image: unset; border: 1px solid dimgrey; border-radius: 3px; width: 13px; height: 13px; + padding: 0; margin-right: 6px; vertical-align: bottom; transition: 0.2s background-color linear; @@ -103,14 +106,12 @@ input[type=checkbox]:checked { } input[type=checkbox]:checked::after { content: ""; - position: absolute; - top: 0; - left: 3px; + display: block; /* width & height doesn't work on inline elements */ width: 3px; height: 7px; border: 1px solid white; border-width: 0 2px 2px 0; - transform: rotate(40deg); + transform: rotate(40deg) translateY(-1px); } /* diff --git a/app/ui.js b/app/ui.js index c1f6776e..85695ca2 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1763,20 +1763,8 @@ const UI = { // Set up translations const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; -l10n.setup(LINGUAS); -if (l10n.language === "en" || l10n.dictionary !== undefined) { - UI.prime(); -} else { - fetch('app/locale/' + l10n.language + '.json') - .then((response) => { - if (!response.ok) { - throw Error("" + response.status + " " + response.statusText); - } - return response.json(); - }) - .then((translations) => { l10n.dictionary = translations; }) - .catch(err => Log.Error("Failed to load translations: " + err)) - .then(UI.prime); -} +l10n.setup(LINGUAS, "app/locale/") + .catch(err => Log.Error("Failed to load translations: " + err)) + .then(UI.prime); export default UI; diff --git a/app/webutil.js b/app/webutil.js index 084c69f6..6011442c 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -6,16 +6,16 @@ * See README.md for usage and integration instructions. */ -import { initLogging as mainInitLogging } from '../core/util/logging.js'; +import * as Log from '../core/util/logging.js'; // init log level reading the logging HTTP param export function initLogging(level) { "use strict"; if (typeof level !== "undefined") { - mainInitLogging(level); + Log.initLogging(level); } else { const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); - mainInitLogging(param || undefined); + Log.initLogging(param || undefined); } } @@ -25,14 +25,14 @@ export function initLogging(level) { // // For privacy (Using a hastag #, the parameters will not be sent to the server) // the url can be requested in the following way: -// https://www.example.com#myqueryparam=myvalue&password=secreatvalue +// https://www.example.com#myqueryparam=myvalue&password=secretvalue // // Even Mixing public and non public parameters will work: -// https://www.example.com?nonsecretparam=example.com#password=secreatvalue +// https://www.example.com?nonsecretparam=example.com#password=secretvalue export function getQueryVar(name, defVal) { "use strict"; const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = document.location.href.match(re); if (typeof defVal === 'undefined') { defVal = null; } if (match) { @@ -146,7 +146,7 @@ export function writeSetting(name, value) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.set(settings); } else { - localStorage.setItem(name, value); + localStorageSet(name, value); } } @@ -156,7 +156,7 @@ export function readSetting(name, defaultValue) { if ((name in settings) || (window.chrome && window.chrome.storage)) { value = settings[name]; } else { - value = localStorage.getItem(name); + value = localStorageGet(name); settings[name] = value; } if (typeof value === "undefined") { @@ -181,6 +181,70 @@ export function eraseSetting(name) { if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); } else { - localStorage.removeItem(name); + localStorageRemove(name); + } +} + +let loggedMsgs = []; +function logOnce(msg, level = "warn") { + if (!loggedMsgs.includes(msg)) { + switch (level) { + case "error": + Log.Error(msg); + break; + case "warn": + Log.Warn(msg); + break; + case "debug": + Log.Debug(msg); + break; + default: + Log.Info(msg); + } + loggedMsgs.push(msg); + } +} + +let cookiesMsg = "Couldn't access noVNC settings, are cookies disabled?"; + +function localStorageGet(name) { + let r; + try { + r = localStorage.getItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.getItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } + } + return r; +} +function localStorageSet(name, value) { + try { + localStorage.setItem(name, value); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.setItem(" + name + "," + value + + ")' failed: " + e, "debug"); + } else { + throw e; + } + } +} +function localStorageRemove(name) { + try { + localStorage.removeItem(name); + } catch (e) { + if (e instanceof DOMException) { + logOnce(cookiesMsg); + logOnce("'localStorage.removeItem(" + name + ")' failed: " + e, + "debug"); + } else { + throw e; + } } } diff --git a/core/crypto/aes.js b/core/crypto/aes.js new file mode 100644 index 00000000..e6aaea7c --- /dev/null +++ b/core/crypto/aes.js @@ -0,0 +1,178 @@ +export class AESECBCipher { + constructor() { + this._key = null; + } + + get algorithm() { + return { name: "AES-ECB" }; + } + + static async importKey(key, _algorithm, extractable, keyUsages) { + const cipher = new AESECBCipher; + await cipher._importKey(key, extractable, keyUsages); + return cipher; + } + + async _importKey(key, extractable, keyUsages) { + this._key = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CBC"}, extractable, keyUsages); + } + + async encrypt(_algorithm, plaintext) { + const x = new Uint8Array(plaintext); + if (x.length % 16 !== 0 || this._key === null) { + return null; + } + const n = x.length / 16; + for (let i = 0; i < n; i++) { + const y = new Uint8Array(await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: new Uint8Array(16), + }, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16); + x.set(y, i * 16); + } + return x; + } +} + +export class AESEAXCipher { + constructor() { + this._rawKey = null; + this._ctrKey = null; + this._cbcKey = null; + this._zeroBlock = new Uint8Array(16); + this._prefixBlock0 = this._zeroBlock; + this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); + } + + get algorithm() { + return { name: "AES-EAX" }; + } + + async _encryptBlock(block) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, block); + return new Uint8Array(encrypted).slice(0, 16); + } + + async _initCMAC() { + const k1 = await this._encryptBlock(this._zeroBlock); + const k2 = new Uint8Array(16); + const v = k1[0] >>> 6; + for (let i = 0; i < 15; i++) { + k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); + k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); + } + const lut = [0x0, 0x87, 0x0e, 0x89]; + k2[14] ^= v >>> 1; + k2[15] = (k1[15] << 2) ^ lut[v]; + k1[15] = (k1[15] << 1) ^ lut[v >> 1]; + this._k1 = k1; + this._k2 = k2; + } + + async _encryptCTR(data, counter) { + const encrypted = await window.crypto.subtle.encrypt({ + name: "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(encrypted); + } + + async _decryptCTR(data, counter) { + const decrypted = await window.crypto.subtle.decrypt({ + name: "AES-CTR", + counter: counter, + length: 128 + }, this._ctrKey, data); + return new Uint8Array(decrypted); + } + + async _computeCMAC(data, prefixBlock) { + if (prefixBlock.length !== 16) { + return null; + } + const n = Math.floor(data.length / 16); + const m = Math.ceil(data.length / 16); + const r = data.length - n * 16; + const cbcData = new Uint8Array((m + 1) * 16); + cbcData.set(prefixBlock); + cbcData.set(data, 16); + if (r === 0) { + for (let i = 0; i < 16; i++) { + cbcData[n * 16 + i] ^= this._k1[i]; + } + } else { + cbcData[(n + 1) * 16 + r] = 0x80; + for (let i = 0; i < 16; i++) { + cbcData[(n + 1) * 16 + i] ^= this._k2[i]; + } + } + let cbcEncrypted = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: this._zeroBlock, + }, this._cbcKey, cbcData); + + cbcEncrypted = new Uint8Array(cbcEncrypted); + const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); + return mac; + } + + static async importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new AESEAXCipher; + await cipher._importKey(key); + return cipher; + } + + async _importKey(key) { + this._rawKey = key; + this._ctrKey = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]); + this._cbcKey = await window.crypto.subtle.importKey( + "raw", key, {name: "AES-CBC"}, false, ["encrypt"]); + await this._initCMAC(); + } + + async encrypt(algorithm, message) { + const ad = algorithm.additionalData; + const nonce = algorithm.iv; + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const encrypted = await this._encryptCTR(message, nCMAC); + const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); + const mac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + mac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + const res = new Uint8Array(16 + encrypted.length); + res.set(encrypted); + res.set(mac, encrypted.length); + return res; + } + + async decrypt(algorithm, data) { + const encrypted = data.slice(0, data.length - 16); + const ad = algorithm.additionalData; + const nonce = algorithm.iv; + const mac = data.slice(data.length - 16); + const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); + const adCMAC = await this._computeCMAC(ad, this._prefixBlock1); + const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); + for (let i = 0; i < 16; i++) { + computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; + } + if (computedMac.length !== mac.length) { + return null; + } + for (let i = 0; i < mac.length; i++) { + if (computedMac[i] !== mac[i]) { + return null; + } + } + const res = await this._decryptCTR(encrypted, nCMAC); + return res; + } +} diff --git a/core/crypto/bigint.js b/core/crypto/bigint.js new file mode 100644 index 00000000..d3443265 --- /dev/null +++ b/core/crypto/bigint.js @@ -0,0 +1,34 @@ +export function modPow(b, e, m) { + let r = 1n; + b = b % m; + while (e > 0n) { + if ((e & 1n) === 1n) { + r = (r * b) % m; + } + e = e >> 1n; + b = (b * b) % m; + } + return r; +} + +export function bigIntToU8Array(bigint, padLength=0) { + let hex = bigint.toString(16); + if (padLength === 0) { + padLength = Math.ceil(hex.length / 2); + } + hex = hex.padStart(padLength * 2, '0'); + const length = hex.length / 2; + const arr = new Uint8Array(length); + for (let i = 0; i < length; i++) { + arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return arr; +} + +export function u8ArrayToBigInt(arr) { + let hex = '0x'; + for (let i = 0; i < arr.length; i++) { + hex += arr[i].toString(16).padStart(2, '0'); + } + return BigInt(hex); +} diff --git a/core/crypto/crypto.js b/core/crypto/crypto.js new file mode 100644 index 00000000..cc17da22 --- /dev/null +++ b/core/crypto/crypto.js @@ -0,0 +1,90 @@ +import { AESECBCipher, AESEAXCipher } from "./aes.js"; +import { DESCBCCipher, DESECBCipher } from "./des.js"; +import { RSACipher } from "./rsa.js"; +import { DHCipher } from "./dh.js"; +import { MD5 } from "./md5.js"; + +// A single interface for the cryptographic algorithms not supported by SubtleCrypto. +// Both synchronous and asynchronous implmentations are allowed. +class LegacyCrypto { + constructor() { + this._algorithms = { + "AES-ECB": AESECBCipher, + "AES-EAX": AESEAXCipher, + "DES-ECB": DESECBCipher, + "DES-CBC": DESCBCCipher, + "RSA-PKCS1-v1_5": RSACipher, + "DH": DHCipher, + "MD5": MD5, + }; + } + + encrypt(algorithm, key, data) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.encrypt !== "function") { + throw new Error("key does not support encryption"); + } + return key.encrypt(algorithm, data); + } + + decrypt(algorithm, key, data) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.decrypt !== "function") { + throw new Error("key does not support encryption"); + } + return key.decrypt(algorithm, data); + } + + importKey(format, keyData, algorithm, extractable, keyUsages) { + if (format !== "raw") { + throw new Error("key format is not supported"); + } + const alg = this._algorithms[algorithm.name]; + if (typeof alg === "undefined" || typeof alg.importKey !== "function") { + throw new Error("algorithm is not supported"); + } + return alg.importKey(keyData, algorithm, extractable, keyUsages); + } + + generateKey(algorithm, extractable, keyUsages) { + const alg = this._algorithms[algorithm.name]; + if (typeof alg === "undefined" || typeof alg.generateKey !== "function") { + throw new Error("algorithm is not supported"); + } + return alg.generateKey(algorithm, extractable, keyUsages); + } + + exportKey(format, key) { + if (format !== "raw") { + throw new Error("key format is not supported"); + } + if (typeof key.exportKey !== "function") { + throw new Error("key does not support exportKey"); + } + return key.exportKey(); + } + + digest(algorithm, data) { + const alg = this._algorithms[algorithm]; + if (typeof alg !== "function") { + throw new Error("algorithm is not supported"); + } + return alg(data); + } + + deriveBits(algorithm, key, length) { + if (key.algorithm.name !== algorithm.name) { + throw new Error("algorithm does not match"); + } + if (typeof key.deriveBits !== "function") { + throw new Error("key does not support deriveBits"); + } + return key.deriveBits(algorithm, length); + } +} + +export default new LegacyCrypto; diff --git a/core/des.js b/core/crypto/des.js similarity index 87% rename from core/des.js rename to core/crypto/des.js index ba1ebde0..8dab31fb 100644 --- a/core/des.js +++ b/core/crypto/des.js @@ -128,7 +128,7 @@ const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, /* eslint-enable comma-spacing */ -export default class DES { +class DES { constructor(password) { this.keys = []; @@ -258,9 +258,73 @@ export default class DES { } return b; } +} - // Encrypt 16 bytes of text using passwd as key - encrypt(t) { - return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16))); +export class DESECBCipher { + constructor() { + this._cipher = null; + } + + get algorithm() { + return { name: "DES-ECB" }; + } + + static importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new DESECBCipher; + cipher._importKey(key); + return cipher; + } + + _importKey(key, _extractable, _keyUsages) { + this._cipher = new DES(key); + } + + encrypt(_algorithm, plaintext) { + const x = new Uint8Array(plaintext); + if (x.length % 8 !== 0 || this._cipher === null) { + return null; + } + const n = x.length / 8; + for (let i = 0; i < n; i++) { + x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8); + } + return x; + } +} + +export class DESCBCCipher { + constructor() { + this._cipher = null; + } + + get algorithm() { + return { name: "DES-CBC" }; + } + + static importKey(key, _algorithm, _extractable, _keyUsages) { + const cipher = new DESCBCCipher; + cipher._importKey(key); + return cipher; + } + + _importKey(key) { + this._cipher = new DES(key); + } + + encrypt(algorithm, plaintext) { + const x = new Uint8Array(plaintext); + let y = new Uint8Array(algorithm.iv); + if (x.length % 8 !== 0 || this._cipher === null) { + return null; + } + const n = x.length / 8; + for (let i = 0; i < n; i++) { + for (let j = 0; j < 8; j++) { + y[j] ^= plaintext[i * 8 + j]; + } + y = this._cipher.enc8(y); + x.set(y, i * 8); + } + return x; } } diff --git a/core/crypto/dh.js b/core/crypto/dh.js new file mode 100644 index 00000000..bd705d9b --- /dev/null +++ b/core/crypto/dh.js @@ -0,0 +1,55 @@ +import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; + +class DHPublicKey { + constructor(key) { + this._key = key; + } + + get algorithm() { + return { name: "DH" }; + } + + exportKey() { + return this._key; + } +} + +export class DHCipher { + constructor() { + this._g = null; + this._p = null; + this._gBigInt = null; + this._pBigInt = null; + this._privateKey = null; + } + + get algorithm() { + return { name: "DH" }; + } + + static generateKey(algorithm, _extractable) { + const cipher = new DHCipher; + cipher._generateKey(algorithm); + return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) }; + } + + _generateKey(algorithm) { + const g = algorithm.g; + const p = algorithm.p; + this._keyBytes = p.length; + this._gBigInt = u8ArrayToBigInt(g); + this._pBigInt = u8ArrayToBigInt(p); + this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes)); + this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey); + this._publicKey = bigIntToU8Array(modPow( + this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes); + } + + deriveBits(algorithm, length) { + const bytes = Math.ceil(length / 8); + const pkey = new Uint8Array(algorithm.public); + const len = bytes > this._keyBytes ? bytes : this._keyBytes; + const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt); + return bigIntToU8Array(secret, len).slice(0, len); + } +} diff --git a/core/util/md5.js b/core/crypto/md5.js similarity index 94% rename from core/util/md5.js rename to core/crypto/md5.js index 49762ef9..fcfefff0 100644 --- a/core/util/md5.js +++ b/core/crypto/md5.js @@ -7,12 +7,15 @@ */ /* - * Performs MD5 hashing on a string of binary characters, returns an array of bytes + * Performs MD5 hashing on an array of bytes, returns an array of bytes */ -export function MD5(d) { - let r = M(V(Y(X(d), 8 * d.length))); - return r; +export async function MD5(d) { + let s = ""; + for (let i = 0; i < d.length; i++) { + s += String.fromCharCode(d[i]); + } + return M(V(Y(X(s), 8 * s.length))); } function M(d) { @@ -76,4 +79,4 @@ function add(d, g) { function rol(d, g) { return d << g | d >>> 32 - g; -} \ No newline at end of file +} diff --git a/core/crypto/rsa.js b/core/crypto/rsa.js new file mode 100644 index 00000000..68e8e869 --- /dev/null +++ b/core/crypto/rsa.js @@ -0,0 +1,132 @@ +import Base64 from "../base64.js"; +import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; + +export class RSACipher { + constructor() { + this._keyLength = 0; + this._keyBytes = 0; + this._n = null; + this._e = null; + this._d = null; + this._nBigInt = null; + this._eBigInt = null; + this._dBigInt = null; + this._extractable = false; + } + + get algorithm() { + return { name: "RSA-PKCS1-v1_5" }; + } + + _base64urlDecode(data) { + data = data.replace(/-/g, "+").replace(/_/g, "/"); + data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); + return Base64.decode(data); + } + + _padArray(arr, length) { + const res = new Uint8Array(length); + res.set(arr, length - arr.length); + return res; + } + + static async generateKey(algorithm, extractable, _keyUsages) { + const cipher = new RSACipher; + await cipher._generateKey(algorithm, extractable); + return { privateKey: cipher }; + } + + async _generateKey(algorithm, extractable) { + this._keyLength = algorithm.modulusLength; + this._keyBytes = Math.ceil(this._keyLength / 8); + const key = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: algorithm.modulusLength, + publicExponent: algorithm.publicExponent, + hash: {name: "SHA-256"}, + }, + true, ["encrypt", "decrypt"]); + const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey); + this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); + this._nBigInt = u8ArrayToBigInt(this._n); + this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); + this._eBigInt = u8ArrayToBigInt(this._e); + this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); + this._dBigInt = u8ArrayToBigInt(this._d); + this._extractable = extractable; + } + + static async importKey(key, _algorithm, extractable, keyUsages) { + if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") { + throw new Error("only support importing RSA public key"); + } + const cipher = new RSACipher; + await cipher._importKey(key, extractable); + return cipher; + } + + async _importKey(key, extractable) { + const n = key.n; + const e = key.e; + if (n.length !== e.length) { + throw new Error("the sizes of modulus and public exponent do not match"); + } + this._keyBytes = n.length; + this._keyLength = this._keyBytes * 8; + this._n = new Uint8Array(this._keyBytes); + this._e = new Uint8Array(this._keyBytes); + this._n.set(n); + this._e.set(e); + this._nBigInt = u8ArrayToBigInt(this._n); + this._eBigInt = u8ArrayToBigInt(this._e); + this._extractable = extractable; + } + + async encrypt(_algorithm, message) { + if (message.length > this._keyBytes - 11) { + return null; + } + const ps = new Uint8Array(this._keyBytes - message.length - 3); + window.crypto.getRandomValues(ps); + for (let i = 0; i < ps.length; i++) { + ps[i] = Math.floor(ps[i] * 254 / 255 + 1); + } + const em = new Uint8Array(this._keyBytes); + em[1] = 0x02; + em.set(ps, 2); + em.set(message, ps.length + 3); + const emBigInt = u8ArrayToBigInt(em); + const c = modPow(emBigInt, this._eBigInt, this._nBigInt); + return bigIntToU8Array(c, this._keyBytes); + } + + async decrypt(_algorithm, message) { + if (message.length !== this._keyBytes) { + return null; + } + const msgBigInt = u8ArrayToBigInt(message); + const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt); + const em = bigIntToU8Array(emBigInt, this._keyBytes); + if (em[0] !== 0x00 || em[1] !== 0x02) { + return null; + } + let i = 2; + for (; i < em.length; i++) { + if (em[i] === 0x00) { + break; + } + } + if (i === em.length) { + return null; + } + return em.slice(i + 1, em.length); + } + + async exportKey() { + if (!this._extractable) { + throw new Error("key is not extractable"); + } + return { n: this._n, e: this._e, d: this._d }; + } +} diff --git a/core/decoders/hextile.js b/core/decoders/hextile.js index ac21eff0..cc33e0e1 100644 --- a/core/decoders/hextile.js +++ b/core/decoders/hextile.js @@ -31,10 +31,7 @@ export default class HextileDecoder { return false; } - let rQ = sock.rQ; - let rQi = sock.rQi; - - let subencoding = rQ[rQi]; // Peek + let subencoding = sock.rQpeek8(); if (subencoding > 30) { // Raw throw new Error("Illegal hextile subencoding (subencoding: " + subencoding + ")"); @@ -65,7 +62,7 @@ export default class HextileDecoder { return false; } - let subrects = rQ[rQi + bytes - 1]; // Peek + let subrects = sock.rQpeekBytes(bytes).at(-1); if (subencoding & 0x10) { // SubrectsColoured bytes += subrects * (4 + 2); } else { @@ -79,7 +76,7 @@ export default class HextileDecoder { } // We know the encoding and have a whole tile - rQi++; + sock.rQshift8(); if (subencoding === 0) { if (this._lastsubencoding & 0x01) { // Weird: ignore blanks are RAW @@ -89,42 +86,36 @@ export default class HextileDecoder { } } else if (subencoding & 0x01) { // Raw let pixels = tw * th; + let data = sock.rQshiftBytes(pixels * 4, false); // Max sure the image is fully opaque for (let i = 0;i < pixels;i++) { - rQ[rQi + i * 4 + 3] = 255; + data[i * 4 + 3] = 255; } - display.blitImage(tx, ty, tw, th, rQ, rQi); - rQi += bytes - 1; + display.blitImage(tx, ty, tw, th, data, 0); } else { if (subencoding & 0x02) { // Background - this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._background = new Uint8Array(sock.rQshiftBytes(4)); } if (subencoding & 0x04) { // Foreground - this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + this._foreground = new Uint8Array(sock.rQshiftBytes(4)); } this._startTile(tx, ty, tw, th, this._background); if (subencoding & 0x08) { // AnySubrects - let subrects = rQ[rQi]; - rQi++; + let subrects = sock.rQshift8(); for (let s = 0; s < subrects; s++) { let color; if (subencoding & 0x10) { // SubrectsColoured - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; + color = sock.rQshiftBytes(4); } else { color = this._foreground; } - const xy = rQ[rQi]; - rQi++; + const xy = sock.rQshift8(); const sx = (xy >> 4); const sy = (xy & 0x0f); - const wh = rQ[rQi]; - rQi++; + const wh = sock.rQshift8(); const sw = (wh >> 4) + 1; const sh = (wh & 0x0f) + 1; @@ -133,7 +124,6 @@ export default class HextileDecoder { } this._finishTile(display); } - sock.rQi = rQi; this._lastsubencoding = subencoding; this._tiles--; } diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js index e1f2bdf8..feb2aeb6 100644 --- a/core/decoders/jpeg.js +++ b/core/decoders/jpeg.js @@ -11,131 +11,136 @@ export default class JPEGDecoder { constructor() { // RealVNC will reuse the quantization tables // and Huffman tables, so we need to cache them. - this._quantTables = []; - this._huffmanTables = []; this._cachedQuantTables = []; this._cachedHuffmanTables = []; - this._jpegLength = 0; this._segments = []; } decodeRect(x, y, width, height, sock, display, depth) { // A rect of JPEG encodings is simply a JPEG file - if (!this._parseJPEG(sock.rQslice(0))) { - return false; - } - const data = sock.rQshiftBytes(this._jpegLength); - if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { - // If there are quantization tables and Huffman tables in the JPEG - // image, we can directly render it. - display.imageRect(x, y, width, height, "image/jpeg", data); - return true; - } else { - // Otherwise we need to insert cached tables. - const sofIndex = this._segments.findIndex( - x => x[1] == 0xC0 || x[1] == 0xC2 - ); - if (sofIndex == -1) { - throw new Error("Illegal JPEG image without SOF"); - } - let segments = this._segments.slice(0, sofIndex); - segments = segments.concat(this._quantTables.length ? - this._quantTables : - this._cachedQuantTables); - segments.push(this._segments[sofIndex]); - segments = segments.concat(this._huffmanTables.length ? - this._huffmanTables : - this._cachedHuffmanTables, - this._segments.slice(sofIndex + 1)); - let length = 0; - for (let i = 0; i < segments.length; i++) { - length += segments[i].length; - } - const data = new Uint8Array(length); - length = 0; - for (let i = 0; i < segments.length; i++) { - data.set(segments[i], length); - length += segments[i].length; - } - display.imageRect(x, y, width, height, "image/jpeg", data); - return true; - } - } - - _parseJPEG(buffer) { - if (this._quantTables.length != 0) { - this._cachedQuantTables = this._quantTables; - } - if (this._huffmanTables.length != 0) { - this._cachedHuffmanTables = this._huffmanTables; - } - this._quantTables = []; - this._huffmanTables = []; - this._segments = []; - let i = 0; - let bufferLength = buffer.length; while (true) { - let j = i; - if (j + 2 > bufferLength) { + let segment = this._readSegment(sock); + if (segment === null) { return false; } - if (buffer[j] != 0xFF) { - throw new Error("Illegal JPEG marker received (byte: " + - buffer[j] + ")"); - } - const type = buffer[j+1]; - j += 2; - if (type == 0xD9) { - this._jpegLength = j; - this._segments.push(buffer.slice(i, j)); - return true; - } else if (type == 0xDA) { - // start of scan - let hasFoundEndOfScan = false; - for (let k = j + 3; k + 1 < bufferLength; k++) { - if (buffer[k] == 0xFF && buffer[k+1] != 0x00 && - !(buffer[k+1] >= 0xD0 && buffer[k+1] <= 0xD7)) { - j = k; - hasFoundEndOfScan = true; - break; - } - } - if (!hasFoundEndOfScan) { - return false; - } - this._segments.push(buffer.slice(i, j)); - i = j; - continue; - } else if (type >= 0xD0 && type < 0xD9 || type == 0x01) { - // No length after marker - this._segments.push(buffer.slice(i, j)); - i = j; - continue; - } - if (j + 2 > bufferLength) { - return false; - } - const length = (buffer[j] << 8) + buffer[j+1] - 2; - if (length < 0) { - throw new Error("Illegal JPEG length received (length: " + - length + ")"); - } - j += 2; - if (j + length > bufferLength) { - return false; - } - j += length; - const segment = buffer.slice(i, j); - if (type == 0xC4) { - // Huffman tables - this._huffmanTables.push(segment); - } else if (type == 0xDB) { - // Quantization tables - this._quantTables.push(segment); - } this._segments.push(segment); - i = j; + // End of image? + if (segment[1] === 0xD9) { + break; + } } + + let huffmanTables = []; + let quantTables = []; + for (let segment of this._segments) { + let type = segment[1]; + if (type === 0xC4) { + // Huffman tables + huffmanTables.push(segment); + } else if (type === 0xDB) { + // Quantization tables + quantTables.push(segment); + } + } + + const sofIndex = this._segments.findIndex( + x => x[1] == 0xC0 || x[1] == 0xC2 + ); + if (sofIndex == -1) { + throw new Error("Illegal JPEG image without SOF"); + } + + if (quantTables.length === 0) { + this._segments.splice(sofIndex+1, 0, + ...this._cachedQuantTables); + } + if (huffmanTables.length === 0) { + this._segments.splice(sofIndex+1, 0, + ...this._cachedHuffmanTables); + } + + let length = 0; + for (let segment of this._segments) { + length += segment.length; + } + + let data = new Uint8Array(length); + length = 0; + for (let segment of this._segments) { + data.set(segment, length); + length += segment.length; + } + + display.imageRect(x, y, width, height, "image/jpeg", data); + + if (huffmanTables.length !== 0) { + this._cachedHuffmanTables = huffmanTables; + } + if (quantTables.length !== 0) { + this._cachedQuantTables = quantTables; + } + + this._segments = []; + + return true; + } + + _readSegment(sock) { + if (sock.rQwait("JPEG", 2)) { + return null; + } + + let marker = sock.rQshift8(); + if (marker != 0xFF) { + throw new Error("Illegal JPEG marker received (byte: " + + marker + ")"); + } + let type = sock.rQshift8(); + if (type >= 0xD0 && type <= 0xD9 || type == 0x01) { + // No length after marker + return new Uint8Array([marker, type]); + } + + if (sock.rQwait("JPEG", 2, 2)) { + return null; + } + + let length = sock.rQshift16(); + if (length < 2) { + throw new Error("Illegal JPEG length received (length: " + + length + ")"); + } + + if (sock.rQwait("JPEG", length-2, 4)) { + return null; + } + + let extra = 0; + if (type === 0xDA) { + // start of scan + extra += 2; + while (true) { + if (sock.rQwait("JPEG", length-2+extra, 4)) { + return null; + } + let data = sock.rQpeekBytes(length-2+extra, false); + if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && + !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { + extra -= 2; + break; + } + extra++; + } + } + + let segment = new Uint8Array(2 + length + extra); + segment[0] = marker; + segment[1] = type; + segment[2] = length >> 8; + segment[3] = length; + segment.set(sock.rQshiftBytes(length-2+extra, false), 4); + + return segment; } } diff --git a/core/decoders/raw.js b/core/decoders/raw.js index d08f7ba9..3c166142 100644 --- a/core/decoders/raw.js +++ b/core/decoders/raw.js @@ -24,41 +24,34 @@ export default class RawDecoder { const pixelSize = depth == 8 ? 1 : 4; const bytesPerLine = width * pixelSize; - if (sock.rQwait("RAW", bytesPerLine)) { - return false; - } - - const curY = y + (height - this._lines); - const currHeight = Math.min(this._lines, - Math.floor(sock.rQlen / bytesPerLine)); - const pixels = width * currHeight; - - let data = sock.rQ; - let index = sock.rQi; - - // Convert data if needed - if (depth == 8) { - const newdata = new Uint8Array(pixels * 4); - for (let i = 0; i < pixels; i++) { - newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; - newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; - newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; - newdata[i * 4 + 3] = 255; + while (this._lines > 0) { + if (sock.rQwait("RAW", bytesPerLine)) { + return false; } - data = newdata; - index = 0; - } - // Max sure the image is fully opaque - for (let i = 0; i < pixels; i++) { - data[index + i * 4 + 3] = 255; - } + const curY = y + (height - this._lines); - display.blitImage(x, curY, width, currHeight, data, index); - sock.rQskipBytes(currHeight * bytesPerLine); - this._lines -= currHeight; - if (this._lines > 0) { - return false; + let data = sock.rQshiftBytes(bytesPerLine, false); + + // Convert data if needed + if (depth == 8) { + const newdata = new Uint8Array(width * 4); + for (let i = 0; i < width; i++) { + newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3; + newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3; + newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3; + newdata[i * 4 + 3] = 255; + } + data = newdata; + } + + // Max sure the image is fully opaque + for (let i = 0; i < width; i++) { + data[i * 4 + 3] = 255; + } + + display.blitImage(x, curY, width, 1, data, 0); + this._lines--; } return true; diff --git a/core/decoders/tight.js b/core/decoders/tight.js index 7952707c..45622080 100644 --- a/core/decoders/tight.js +++ b/core/decoders/tight.js @@ -76,12 +76,8 @@ export default class TightDecoder { return false; } - const rQi = sock.rQi; - const rQ = sock.rQ; - - display.fillRect(x, y, width, height, - [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false); - sock.rQskipBytes(3); + let pixel = sock.rQshiftBytes(3); + display.fillRect(x, y, width, height, pixel, false); return true; } @@ -316,7 +312,7 @@ export default class TightDecoder { return null; } - let data = sock.rQshiftBytes(this._len); + let data = sock.rQshiftBytes(this._len, false); this._len = 0; return data; diff --git a/core/decoders/zrle.js b/core/decoders/zrle.js index 97fbd58e..49128e79 100644 --- a/core/decoders/zrle.js +++ b/core/decoders/zrle.js @@ -32,7 +32,7 @@ export default class ZRLEDecoder { return false; } - const data = sock.rQshiftBytes(this._length); + const data = sock.rQshiftBytes(this._length, false); this._inflator.setInput(data); diff --git a/core/display.js b/core/display.js index bf8d5fab..fcd62699 100644 --- a/core/display.js +++ b/core/display.js @@ -15,7 +15,7 @@ export default class Display { this._drawCtx = null; this._renderQ = []; // queue drawing actions for in-oder rendering - this._flushing = false; + this._flushPromise = null; // the full frame buffer (logical canvas) size this._fbWidth = 0; @@ -61,10 +61,6 @@ export default class Display { this._scale = 1.0; this._clipViewport = false; - - // ===== EVENT HANDLERS ===== - - this.onflush = () => {}; // A flush request has finished } // ===== PROPERTIES ===== @@ -306,9 +302,14 @@ export default class Display { flush() { if (this._renderQ.length === 0) { - this.onflush(); + return Promise.resolve(); } else { - this._flushing = true; + if (this._flushPromise === null) { + this._flushPromise = new Promise((resolve) => { + this._flushResolve = resolve; + }); + } + return this._flushPromise; } } @@ -517,9 +518,11 @@ export default class Display { } } - if (this._renderQ.length === 0 && this._flushing) { - this._flushing = false; - this.onflush(); + if (this._renderQ.length === 0 && + this._flushPromise !== null) { + this._flushResolve(); + this._flushPromise = null; + this._flushResolve = null; } } } diff --git a/core/ra2.js b/core/ra2.js index 81a8a895..d330b848 100644 --- a/core/ra2.js +++ b/core/ra2.js @@ -1,146 +1,25 @@ -import Base64 from './base64.js'; import { encodeUTF8 } from './util/strings.js'; import EventTargetMixin from './util/eventtarget.js'; +import legacyCrypto from './crypto/crypto.js'; -export class AESEAXCipher { +class RA2Cipher { constructor() { - this._rawKey = null; - this._ctrKey = null; - this._cbcKey = null; - this._zeroBlock = new Uint8Array(16); - this._prefixBlock0 = this._zeroBlock; - this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); - this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); - } - - async _encryptBlock(block) { - const encrypted = await window.crypto.subtle.encrypt({ - name: "AES-CBC", - iv: this._zeroBlock, - }, this._cbcKey, block); - return new Uint8Array(encrypted).slice(0, 16); - } - - async _initCMAC() { - const k1 = await this._encryptBlock(this._zeroBlock); - const k2 = new Uint8Array(16); - const v = k1[0] >>> 6; - for (let i = 0; i < 15; i++) { - k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2); - k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1); - } - const lut = [0x0, 0x87, 0x0e, 0x89]; - k2[14] ^= v >>> 1; - k2[15] = (k1[15] << 2) ^ lut[v]; - k1[15] = (k1[15] << 1) ^ lut[v >> 1]; - this._k1 = k1; - this._k2 = k2; - } - - async _encryptCTR(data, counter) { - const encrypted = await window.crypto.subtle.encrypt({ - "name": "AES-CTR", - counter: counter, - length: 128 - }, this._ctrKey, data); - return new Uint8Array(encrypted); - } - - async _decryptCTR(data, counter) { - const decrypted = await window.crypto.subtle.decrypt({ - "name": "AES-CTR", - counter: counter, - length: 128 - }, this._ctrKey, data); - return new Uint8Array(decrypted); - } - - async _computeCMAC(data, prefixBlock) { - if (prefixBlock.length !== 16) { - return null; - } - const n = Math.floor(data.length / 16); - const m = Math.ceil(data.length / 16); - const r = data.length - n * 16; - const cbcData = new Uint8Array((m + 1) * 16); - cbcData.set(prefixBlock); - cbcData.set(data, 16); - if (r === 0) { - for (let i = 0; i < 16; i++) { - cbcData[n * 16 + i] ^= this._k1[i]; - } - } else { - cbcData[(n + 1) * 16 + r] = 0x80; - for (let i = 0; i < 16; i++) { - cbcData[(n + 1) * 16 + i] ^= this._k2[i]; - } - } - let cbcEncrypted = await window.crypto.subtle.encrypt({ - name: "AES-CBC", - iv: this._zeroBlock, - }, this._cbcKey, cbcData); - - cbcEncrypted = new Uint8Array(cbcEncrypted); - const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16); - return mac; - } - - async setKey(key) { - this._rawKey = key; - this._ctrKey = await window.crypto.subtle.importKey( - "raw", key, {"name": "AES-CTR"}, false, ["encrypt", "decrypt"]); - this._cbcKey = await window.crypto.subtle.importKey( - "raw", key, {"name": "AES-CBC"}, false, ["encrypt", "decrypt"]); - await this._initCMAC(); - } - - async encrypt(message, associatedData, nonce) { - const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); - const encrypted = await this._encryptCTR(message, nCMAC); - const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); - const mac = await this._computeCMAC(encrypted, this._prefixBlock2); - for (let i = 0; i < 16; i++) { - mac[i] ^= nCMAC[i] ^ adCMAC[i]; - } - const res = new Uint8Array(16 + encrypted.length); - res.set(encrypted); - res.set(mac, encrypted.length); - return res; - } - - async decrypt(encrypted, associatedData, nonce, mac) { - const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0); - const adCMAC = await this._computeCMAC(associatedData, this._prefixBlock1); - const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2); - for (let i = 0; i < 16; i++) { - computedMac[i] ^= nCMAC[i] ^ adCMAC[i]; - } - if (computedMac.length !== mac.length) { - return null; - } - for (let i = 0; i < mac.length; i++) { - if (computedMac[i] !== mac[i]) { - return null; - } - } - const res = await this._decryptCTR(encrypted, nCMAC); - return res; - } -} - -export class RA2Cipher { - constructor() { - this._cipher = new AESEAXCipher(); + this._cipher = null; this._counter = new Uint8Array(16); } async setKey(key) { - await this._cipher.setKey(key); + this._cipher = await legacyCrypto.importKey( + "raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]); } async makeMessage(message) { const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); - const encrypted = await this._cipher.encrypt(message, ad, this._counter); + const encrypted = await legacyCrypto.encrypt({ + name: "AES-EAX", + iv: this._counter, + additionalData: ad, + }, this._cipher, message); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); const res = new Uint8Array(message.length + 2 + 16); res.set(ad); @@ -148,164 +27,18 @@ export class RA2Cipher { return res; } - async receiveMessage(length, encrypted, mac) { + async receiveMessage(length, encrypted) { const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); - const res = await this._cipher.decrypt(encrypted, ad, this._counter, mac); + const res = await legacyCrypto.decrypt({ + name: "AES-EAX", + iv: this._counter, + additionalData: ad, + }, this._cipher, encrypted); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++); return res; } } -export class RSACipher { - constructor(keyLength) { - this._key = null; - this._keyLength = keyLength; - this._keyBytes = Math.ceil(keyLength / 8); - this._n = null; - this._e = null; - this._d = null; - this._nBigInt = null; - this._eBigInt = null; - this._dBigInt = null; - } - - _base64urlDecode(data) { - data = data.replace(/-/g, "+").replace(/_/g, "/"); - data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); - return Base64.decode(data); - } - - _u8ArrayToBigInt(arr) { - let hex = '0x'; - for (let i = 0; i < arr.length; i++) { - hex += arr[i].toString(16).padStart(2, '0'); - } - return BigInt(hex); - } - - _padArray(arr, length) { - const res = new Uint8Array(length); - res.set(arr, length - arr.length); - return res; - } - - _bigIntToU8Array(bigint, padLength=0) { - let hex = bigint.toString(16); - if (padLength === 0) { - padLength = Math.ceil(hex.length / 2) * 2; - } - hex = hex.padStart(padLength * 2, '0'); - const length = hex.length / 2; - const arr = new Uint8Array(length); - for (let i = 0; i < length; i++) { - arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); - } - return arr; - } - - _modPow(b, e, m) { - if (m === 1n) { - return 0; - } - let r = 1n; - b = b % m; - while (e > 0) { - if (e % 2n === 1n) { - r = (r * b) % m; - } - e = e / 2n; - b = (b * b) % m; - } - return r; - } - - async generateKey() { - this._key = await window.crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: this._keyLength, - publicExponent: new Uint8Array([0x01, 0x00, 0x01]), - hash: {name: "SHA-256"}, - }, - true, ["encrypt", "decrypt"]); - const privateKey = await window.crypto.subtle.exportKey("jwk", this._key.privateKey); - this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); - this._nBigInt = this._u8ArrayToBigInt(this._n); - this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); - this._eBigInt = this._u8ArrayToBigInt(this._e); - this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); - this._dBigInt = this._u8ArrayToBigInt(this._d); - } - - setPublicKey(n, e) { - if (n.length !== this._keyBytes || e.length !== this._keyBytes) { - return; - } - this._n = new Uint8Array(this._keyBytes); - this._e = new Uint8Array(this._keyBytes); - this._n.set(n); - this._e.set(e); - this._nBigInt = this._u8ArrayToBigInt(this._n); - this._eBigInt = this._u8ArrayToBigInt(this._e); - } - - encrypt(message) { - if (message.length > this._keyBytes - 11) { - return null; - } - const ps = new Uint8Array(this._keyBytes - message.length - 3); - window.crypto.getRandomValues(ps); - for (let i = 0; i < ps.length; i++) { - ps[i] = Math.floor(ps[i] * 254 / 255 + 1); - } - const em = new Uint8Array(this._keyBytes); - em[1] = 0x02; - em.set(ps, 2); - em.set(message, ps.length + 3); - const emBigInt = this._u8ArrayToBigInt(em); - const c = this._modPow(emBigInt, this._eBigInt, this._nBigInt); - return this._bigIntToU8Array(c, this._keyBytes); - } - - decrypt(message) { - if (message.length !== this._keyBytes) { - return null; - } - const msgBigInt = this._u8ArrayToBigInt(message); - const emBigInt = this._modPow(msgBigInt, this._dBigInt, this._nBigInt); - const em = this._bigIntToU8Array(emBigInt, this._keyBytes); - if (em[0] !== 0x00 || em[1] !== 0x02) { - return null; - } - let i = 2; - for (; i < em.length; i++) { - if (em[i] === 0x00) { - break; - } - } - if (i === em.length) { - return null; - } - return em.slice(i + 1, em.length); - } - - get keyLength() { - return this._keyLength; - } - - get n() { - return this._n; - } - - get e() { - return this._e; - } - - get d() { - return this._d; - } -} - export default class RSAAESAuthenticationState extends EventTargetMixin { constructor(sock, getCredentials) { super(); @@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { this._hasStarted = true; // 1: Receive server public key await this._waitSockAsync(4); - const serverKeyLengthBuffer = this._sock.rQslice(0, 4); + const serverKeyLengthBuffer = this._sock.rQpeekBytes(4); const serverKeyLength = this._sock.rQshift32(); if (serverKeyLength < 1024) { throw new Error("RA2: server public key is too short: " + serverKeyLength); @@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { await this._waitSockAsync(serverKeyBytes * 2); const serverN = this._sock.rQshiftBytes(serverKeyBytes); const serverE = this._sock.rQshiftBytes(serverKeyBytes); - const serverRSACipher = new RSACipher(serverKeyLength); - serverRSACipher.setPublicKey(serverN, serverE); + const serverRSACipher = await legacyCrypto.importKey( + "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]); const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); serverPublickey.set(serverKeyLengthBuffer); serverPublickey.set(serverN, 4); serverPublickey.set(serverE, 4 + serverKeyBytes); // verify server public key + let approveKey = this._waitApproveKeyAsync(); this.dispatchEvent(new CustomEvent("serververification", { detail: { type: "RSA", publickey: serverPublickey } })); - await this._waitApproveKeyAsync(); + await approveKey; // 2: Send client public key const clientKeyLength = 2048; const clientKeyBytes = Math.ceil(clientKeyLength / 8); - const clientRSACipher = new RSACipher(clientKeyLength); - await clientRSACipher.generateKey(); - const clientN = clientRSACipher.n; - const clientE = clientRSACipher.e; + const clientRSACipher = (await legacyCrypto.generateKey({ + name: "RSA-PKCS1-v1_5", + modulusLength: clientKeyLength, + publicExponent: new Uint8Array([1, 0, 1]), + }, true, ["encrypt"])).privateKey; + const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher); + const clientN = clientExportedRSAKey.n; + const clientE = clientExportedRSAKey.e; const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2); clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; @@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { clientPublicKey[3] = clientKeyLength & 0xff; clientPublicKey.set(clientN, 4); clientPublicKey.set(clientE, 4 + clientKeyBytes); - this._sock.send(clientPublicKey); + this._sock.sQpushBytes(clientPublicKey); + this._sock.flush(); // 3: Send client random const clientRandom = new Uint8Array(16); window.crypto.getRandomValues(clientRandom); - const clientEncryptedRandom = serverRSACipher.encrypt(clientRandom); + const clientEncryptedRandom = await legacyCrypto.encrypt( + { name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom); const clientRandomMessage = new Uint8Array(2 + serverKeyBytes); clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; clientRandomMessage[1] = serverKeyBytes & 0xff; clientRandomMessage.set(clientEncryptedRandom, 2); - this._sock.send(clientRandomMessage); + this._sock.sQpushBytes(clientRandomMessage); + this._sock.flush(); // 4: Receive server random await this._waitSockAsync(2); @@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { throw new Error("RA2: wrong encrypted message length"); } const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); - const serverRandom = clientRSACipher.decrypt(serverEncryptedRandom); + const serverRandom = await legacyCrypto.decrypt( + { name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom); if (serverRandom === null || serverRandom.length !== 16) { throw new Error("RA2: corrupted server encrypted random"); } @@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); serverHash = new Uint8Array(serverHash); clientHash = new Uint8Array(clientHash); - this._sock.send(await clientCipher.makeMessage(clientHash)); + this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash)); + this._sock.flush(); await this._waitSockAsync(2 + 20 + 16); if (this._sock.rQshift16() !== 20) { throw new Error("RA2: wrong server hash"); } const serverHashReceived = await serverCipher.receiveMessage( - 20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); + 20, this._sock.rQshiftBytes(20 + 16)); if (serverHashReceived === null) { throw new Error("RA2: failed to authenticate the message"); } @@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { throw new Error("RA2: wrong subtype"); } let subtype = (await serverCipher.receiveMessage( - 1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); + 1, this._sock.rQshiftBytes(1 + 16))); if (subtype === null) { throw new Error("RA2: failed to authenticate the message"); } subtype = subtype[0]; + let waitCredentials = this._waitCredentialsAsync(subtype); if (subtype === 1) { if (this._getCredentials().username === undefined || this._getCredentials().password === undefined) { @@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { } else { throw new Error("RA2: wrong subtype"); } - await this._waitCredentialsAsync(subtype); + await waitCredentials; let username; if (subtype === 1) { username = encodeUTF8(this._getCredentials().username).slice(0, 255); @@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { for (let i = 0; i < password.length; i++) { credentials[username.length + 2 + i] = password.charCodeAt(i); } - this._sock.send(await clientCipher.makeMessage(credentials)); + this._sock.sQpushBytes(await clientCipher.makeMessage(credentials)); + this._sock.flush(); } get hasStarted() { @@ -564,4 +309,4 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { set hasStarted(s) { this._hasStarted = s; } -} \ No newline at end of file +} diff --git a/core/rfb.js b/core/rfb.js index 6afd7c65..fb9df0b9 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js"; import GestureHandler from "./input/gesturehandler.js"; import Cursor from "./util/cursor.js"; import Websock from "./websock.js"; -import DES from "./des.js"; import KeyTable from "./input/keysym.js"; import XtScancode from "./input/xtscancodes.js"; import { encodings } from "./encodings.js"; import RSAAESAuthenticationState from "./ra2.js"; -import { MD5 } from "./util/md5.js"; +import legacyCrypto from "./crypto/crypto.js"; import RawDecoder from "./decoders/raw.js"; import CopyRectDecoder from "./decoders/copyrect.js"; @@ -258,7 +257,6 @@ export default class RFB extends EventTargetMixin { Log.Error("Display exception: " + exc); throw exc; } - this._display.onflush = this._onFlush.bind(this); this._keyboard = new Keyboard(this._canvas); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); @@ -960,7 +958,7 @@ export default class RFB extends EventTargetMixin { } _handleMessage() { - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { Log.Warn("handleMessage called on an empty receive queue"); return; } @@ -977,7 +975,7 @@ export default class RFB extends EventTargetMixin { if (!this._normalMsg()) { break; } - if (this._sock.rQlen === 0) { + if (this._sock.rQwait("message", 1)) { break; } } @@ -1383,7 +1381,8 @@ export default class RFB extends EventTargetMixin { while (repeaterID.length < 250) { repeaterID += "\0"; } - this._sock.sendString(repeaterID); + this._sock.sQpushString(repeaterID); + this._sock.flush(); return true; } @@ -1393,7 +1392,8 @@ export default class RFB extends EventTargetMixin { const cversion = "00" + parseInt(this._rfbVersion, 10) + ".00" + ((this._rfbVersion * 10) % 10); - this._sock.sendString("RFB " + cversion + "\n"); + this._sock.sQpushString("RFB " + cversion + "\n"); + this._sock.flush(); Log.Debug('Sent ProtocolVersion: ' + cversion); this._rfbInitState = 'Security'; @@ -1445,7 +1445,8 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + types + ")"); } - this._sock.send([this._rfbAuthScheme]); + this._sock.sQpush8(this._rfbAuthScheme); + this._sock.flush(); } else { // Server decides if (this._sock.rQwait("security scheme", 4)) { return false; } @@ -1507,12 +1508,15 @@ export default class RFB extends EventTargetMixin { return false; } - const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) + - String.fromCharCode(this._rfbCredentials.target.length) + - this._rfbCredentials.username + - this._rfbCredentials.target; - this._sock.sendString(xvpAuthStr); + this._sock.sQpush8(this._rfbCredentials.username.length); + this._sock.sQpush8(this._rfbCredentials.target.length); + this._sock.sQpushString(this._rfbCredentials.username); + this._sock.sQpushString(this._rfbCredentials.target); + + this._sock.flush(); + this._rfbAuthScheme = securityTypeVNCAuth; + return this._negotiateAuthentication(); } @@ -1530,7 +1534,9 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported VeNCrypt version " + major + "." + minor); } - this._sock.send([0, 2]); + this._sock.sQpush8(0); + this._sock.sQpush8(2); + this._sock.flush(); this._rfbVeNCryptState = 1; } @@ -1589,12 +1595,10 @@ export default class RFB extends EventTargetMixin { return this._fail("Unsupported security types (types: " + subtypes + ")"); } - this._sock.send([this._rfbAuthScheme >> 24, - this._rfbAuthScheme >> 16, - this._rfbAuthScheme >> 8, - this._rfbAuthScheme]); + this._sock.sQpush32(this._rfbAuthScheme); + this._sock.flush(); - this._rfbVeNCryptState == 4; + this._rfbVeNCryptState = 4; return true; } } @@ -1611,20 +1615,11 @@ export default class RFB extends EventTargetMixin { const user = encodeUTF8(this._rfbCredentials.username); const pass = encodeUTF8(this._rfbCredentials.password); - this._sock.send([ - (user.length >> 24) & 0xFF, - (user.length >> 16) & 0xFF, - (user.length >> 8) & 0xFF, - user.length & 0xFF - ]); - this._sock.send([ - (pass.length >> 24) & 0xFF, - (pass.length >> 16) & 0xFF, - (pass.length >> 8) & 0xFF, - pass.length & 0xFF - ]); - this._sock.sendString(user); - this._sock.sendString(pass); + this._sock.sQpush32(user.length); + this._sock.sQpush32(pass.length); + this._sock.sQpushString(user); + this._sock.sQpushString(pass); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; @@ -1643,7 +1638,8 @@ export default class RFB extends EventTargetMixin { // TODO(directxman12): make genDES not require an Array const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); const response = RFB.genDES(this._rfbCredentials.password, challenge); - this._sock.send(response); + this._sock.sQpushBytes(response); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -1661,8 +1657,9 @@ export default class RFB extends EventTargetMixin { if (this._rfbCredentials.ardPublicKey != undefined && this._rfbCredentials.ardCredentials != undefined) { // if the async web crypto is done return the results - this._sock.send(this._rfbCredentials.ardCredentials); - this._sock.send(this._rfbCredentials.ardPublicKey); + this._sock.sQpushBytes(this._rfbCredentials.ardCredentials); + this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey); + this._sock.flush(); this._rfbCredentials.ardCredentials = null; this._rfbCredentials.ardPublicKey = null; this._rfbInitState = "SecurityResult"; @@ -1681,77 +1678,35 @@ export default class RFB extends EventTargetMixin { let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key - let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); - let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); - - this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding); + let clientKey = legacyCrypto.generateKey( + { name: "DH", g: generator, p: prime }, false, ["deriveBits"]); + this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey); return false; } - _modPow(base, exponent, modulus) { + async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) { + const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey); + const sharedKey = legacyCrypto.deriveBits( + { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8); - let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); - let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); - let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); + const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63); + const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - let b = BigInt(baseHex); - let e = BigInt(exponentHex); - let m = BigInt(modulusHex); - let r = 1n; - b = b % m; - while (e > 0) { - if (e % 2n === 1n) { - r = (r * b) % m; - } - e = e / 2n; - b = (b * b) % m; + const credentials = window.crypto.getRandomValues(new Uint8Array(128)); + for (let i = 0; i < username.length; i++) { + credentials[i] = username.charCodeAt(i); } - let hexResult = r.toString(16); - - while (hexResult.length/2 String.fromCharCode(byte)).join(''); - let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]); - let data = new Uint8Array(string.length); - for (let i = 0; i < string.length; ++i) { - data[i] = string.charCodeAt(i); - } - let encrypted = new Uint8Array(data.length); - for (let i=0;i { - this.dispatchEvent(new CustomEvent('securityresult')); + }) + .then(() => { this._rfbInitState = "SecurityResult"; return true; }).finally(() => { @@ -1934,15 +1893,15 @@ export default class RFB extends EventTargetMixin { const g = this._sock.rQshiftBytes(8); const p = this._sock.rQshiftBytes(8); const A = this._sock.rQshiftBytes(8); - const b = window.crypto.getRandomValues(new Uint8Array(8)); - const B = new Uint8Array(this._modPow(g, b, p)); - const secret = new Uint8Array(this._modPow(A, b, p)); + const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]); + const B = legacyCrypto.exportKey("raw", dhKey.publicKey); + const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64); - const des = new DES(secret); + const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]); const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); - const usernameBytes = new Uint8Array(256); - const passwordBytes = new Uint8Array(64); + let usernameBytes = new Uint8Array(256); + let passwordBytes = new Uint8Array(64); window.crypto.getRandomValues(usernameBytes); window.crypto.getRandomValues(passwordBytes); for (let i = 0; i < username.length; i++) { @@ -1953,25 +1912,12 @@ export default class RFB extends EventTargetMixin { passwordBytes[i] = password.charCodeAt(i); } passwordBytes[password.length] = 0; - let x = new Uint8Array(secret); - for (let i = 0; i < 32; i++) { - for (let j = 0; j < 8; j++) { - x[j] ^= usernameBytes[i * 8 + j]; - } - x = des.enc8(x); - usernameBytes.set(x, i * 8); - } - x = new Uint8Array(secret); - for (let i = 0; i < 8; i++) { - for (let j = 0; j < 8; j++) { - x[j] ^= passwordBytes[i * 8 + j]; - } - x = des.enc8(x); - passwordBytes.set(x, i * 8); - } - this._sock.send(B); - this._sock.send(usernameBytes); - this._sock.send(passwordBytes); + usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes); + passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes); + this._sock.sQpushBytes(B); + this._sock.sQpushBytes(usernameBytes); + this._sock.sQpushBytes(passwordBytes); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -2199,7 +2145,8 @@ export default class RFB extends EventTargetMixin { return this._handleSecurityReason(); case 'ClientInitialisation': - this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation + this._sock.flush(); this._rfbInitState = 'ServerInitialisation'; return true; @@ -2512,19 +2459,11 @@ export default class RFB extends EventTargetMixin { default: this._fail("Unexpected server message (type " + msgType + ")"); - Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30)); return true; } } - _onFlush() { - this._flushing = false; - // Resume processing - if (this._sock.rQlen > 0) { - this._handleMessage(); - } - } - _framebufferUpdate() { if (this._FBU.rects === 0) { if (this._sock.rQwait("FBU header", 3, 1)) { return false; } @@ -2535,7 +2474,14 @@ export default class RFB extends EventTargetMixin { // to avoid building up an excessive queue if (this._display.pending()) { this._flushing = true; - this._display.flush(); + this._display.flush() + .then(() => { + this._flushing = false; + // Resume processing + if (!this._sock.rQwait("message", 1)) { + this._handleMessage(); + } + }); return false; } } @@ -2545,13 +2491,13 @@ export default class RFB extends EventTargetMixin { if (this._sock.rQwait("rect header", 12)) { return false; } /* New FramebufferUpdate */ - const hdr = this._sock.rQshiftBytes(12); - this._FBU.x = (hdr[0] << 8) + hdr[1]; - this._FBU.y = (hdr[2] << 8) + hdr[3]; - this._FBU.width = (hdr[4] << 8) + hdr[5]; - this._FBU.height = (hdr[6] << 8) + hdr[7]; - this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); + this._FBU.x = this._sock.rQshift16(); + this._FBU.y = this._sock.rQshift16(); + this._FBU.width = this._sock.rQshift16(); + this._FBU.height = this._sock.rQshift16(); + this._FBU.encoding = this._sock.rQshift32(); + /* Encodings are signed */ + this._FBU.encoding >>= 0; } if (!this._handleRect()) { @@ -2785,26 +2731,18 @@ export default class RFB extends EventTargetMixin { const firstUpdate = !this._supportsSetDesktopSize; this._supportsSetDesktopSize = true; - // Normally we only apply the current resize mode after a - // window resize event. However there is no such trigger on the - // initial connect. And we don't know if the server supports - // resizing until we've gotten here. - if (firstUpdate) { - this._requestRemoteResize(); - } - this._sock.rQskipBytes(1); // number-of-screens this._sock.rQskipBytes(3); // padding for (let i = 0; i < numberOfScreens; i += 1) { // Save the id and flags of the first screen if (i === 0) { - this._screenID = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height - this._screenFlags = this._sock.rQshiftBytes(4); // flags + this._screenID = this._sock.rQshift32(); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screenFlags = this._sock.rQshift32(); // flags } else { this._sock.rQskipBytes(16); } @@ -2842,6 +2780,14 @@ export default class RFB extends EventTargetMixin { this._resize(this._FBU.width, this._FBU.height); } + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } + return true; } @@ -2937,28 +2883,22 @@ export default class RFB extends EventTargetMixin { static genDES(password, challenge) { const passwordChars = password.split('').map(c => c.charCodeAt(0)); - return (new DES(passwordChars)).encrypt(challenge); + const key = legacyCrypto.importKey( + "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]); + return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge); } } // Class Methods RFB.messages = { keyEvent(sock, keysym, down) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(4); // msg-type + sock.sQpush8(down); - buff[offset] = 4; // msg-type - buff[offset + 1] = down; + sock.sQpush16(0); - buff[offset + 2] = 0; - buff[offset + 3] = 0; + sock.sQpush32(keysym); - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - sock._sQlen += 8; sock.flush(); }, @@ -2972,46 +2912,28 @@ RFB.messages = { return xtScanCode; } - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(255); // msg-type + sock.sQpush8(0); // sub msg-type - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type + sock.sQpush16(down); - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; + sock.sQpush32(keysym); const RFBkeycode = getRFBkeycode(keycode); - buff[offset + 8] = (RFBkeycode >> 24); - buff[offset + 9] = (RFBkeycode >> 16); - buff[offset + 10] = (RFBkeycode >> 8); - buff[offset + 11] = RFBkeycode; + sock.sQpush32(RFBkeycode); - sock._sQlen += 12; sock.flush(); }, pointerEvent(sock, x, y, mask) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(5); // msg-type - buff[offset] = 5; // msg-type + sock.sQpush8(mask); - buff[offset + 1] = mask; + sock.sQpush16(x); + sock.sQpush16(y); - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; - - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; - - sock._sQlen += 6; sock.flush(); }, @@ -3111,14 +3033,11 @@ RFB.messages = { }, clientCutText(sock, data, extended = false) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(6); // msg-type - buff[offset] = 6; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding let length; if (extended) { @@ -3127,121 +3046,63 @@ RFB.messages = { length = data.length; } - buff[offset + 4] = length >> 24; - buff[offset + 5] = length >> 16; - buff[offset + 6] = length >> 8; - buff[offset + 7] = length; - - sock._sQlen += 8; - - // We have to keep track of from where in the data we begin creating the - // buffer for the flush in the next iteration. - let dataOffset = 0; - - let remaining = data.length; - while (remaining > 0) { - - let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); - for (let i = 0; i < flushSize; i++) { - buff[sock._sQlen + i] = data[dataOffset + i]; - } - - sock._sQlen += flushSize; - sock.flush(); - - remaining -= flushSize; - dataOffset += flushSize; - } - + sock.sQpush32(length); + sock.sQpushBytes(data); + sock.flush(); }, setDesktopSize(sock, width, height, id, flags) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(251); // msg-type - buff[offset] = 251; // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = width >> 8; // width - buff[offset + 3] = width; - buff[offset + 4] = height >> 8; // height - buff[offset + 5] = height; + sock.sQpush8(0); // padding - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding + sock.sQpush16(width); + sock.sQpush16(height); + + sock.sQpush8(1); // number-of-screens + + sock.sQpush8(0); // padding // screen array - buff[offset + 8] = id >> 24; // id - buff[offset + 9] = id >> 16; - buff[offset + 10] = id >> 8; - buff[offset + 11] = id; - buff[offset + 12] = 0; // x-position - buff[offset + 13] = 0; - buff[offset + 14] = 0; // y-position - buff[offset + 15] = 0; - buff[offset + 16] = width >> 8; // width - buff[offset + 17] = width; - buff[offset + 18] = height >> 8; // height - buff[offset + 19] = height; - buff[offset + 20] = flags >> 24; // flags - buff[offset + 21] = flags >> 16; - buff[offset + 22] = flags >> 8; - buff[offset + 23] = flags; + sock.sQpush32(id); + sock.sQpush16(0); // x-position + sock.sQpush16(0); // y-position + sock.sQpush16(width); + sock.sQpush16(height); + sock.sQpush32(flags); - sock._sQlen += 24; sock.flush(); }, clientFence(sock, flags, payload) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(248); // msg-type - buff[offset] = 248; // msg-type + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush32(flags); - buff[offset + 4] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; + sock.sQpush8(payload.length); + sock.sQpushString(payload); - const n = payload.length; - - buff[offset + 8] = n; // length - - for (let i = 0; i < n; i++) { - buff[offset + 9 + i] = payload.charCodeAt(i); - } - - sock._sQlen += 9 + n; sock.flush(); }, enableContinuousUpdates(sock, enable, x, y, width, height) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(150); // msg-type - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag + sock.sQpush8(enable); - buff[offset + 2] = x >> 8; // x - buff[offset + 3] = x; - buff[offset + 4] = y >> 8; // y - buff[offset + 5] = y; - buff[offset + 6] = width >> 8; // width - buff[offset + 7] = width; - buff[offset + 8] = height >> 8; // height - buff[offset + 9] = height; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(width); + sock.sQpush16(height); - sock._sQlen += 10; sock.flush(); }, pixelFormat(sock, depth, trueColor) { - const buff = sock._sQ; - const offset = sock._sQlen; - let bpp; if (depth > 16) { @@ -3254,100 +3115,69 @@ RFB.messages = { const bits = Math.floor(depth/3); - buff[offset] = 0; // msg-type + sock.sQpush8(0); // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 4] = bpp; // bits-per-pixel - buff[offset + 5] = depth; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = trueColor ? 1 : 0; // true-color + sock.sQpush8(bpp); + sock.sQpush8(depth); + sock.sQpush8(0); // little-endian + sock.sQpush8(trueColor ? 1 : 0); - buff[offset + 8] = 0; // red-max - buff[offset + 9] = (1 << bits) - 1; // red-max + sock.sQpush16((1 << bits) - 1); // red-max + sock.sQpush16((1 << bits) - 1); // green-max + sock.sQpush16((1 << bits) - 1); // blue-max - buff[offset + 10] = 0; // green-max - buff[offset + 11] = (1 << bits) - 1; // green-max + sock.sQpush8(bits * 0); // red-shift + sock.sQpush8(bits * 1); // green-shift + sock.sQpush8(bits * 2); // blue-shift - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = (1 << bits) - 1; // blue-max + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding + sock.sQpush8(0); // padding - buff[offset + 14] = bits * 0; // red-shift - buff[offset + 15] = bits * 1; // green-shift - buff[offset + 16] = bits * 2; // blue-shift - - buff[offset + 17] = 0; // padding - buff[offset + 18] = 0; // padding - buff[offset + 19] = 0; // padding - - sock._sQlen += 20; sock.flush(); }, clientEncodings(sock, encodings) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(2); // msg-type - buff[offset] = 2; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = encodings.length >> 8; - buff[offset + 3] = encodings.length; - - let j = offset + 4; + sock.sQpush16(encodings.length); for (let i = 0; i < encodings.length; i++) { - const enc = encodings[i]; - buff[j] = enc >> 24; - buff[j + 1] = enc >> 16; - buff[j + 2] = enc >> 8; - buff[j + 3] = enc; - - j += 4; + sock.sQpush32(encodings[i]); } - sock._sQlen += j - offset; sock.flush(); }, fbUpdateRequest(sock, incremental, x, y, w, h) { - const buff = sock._sQ; - const offset = sock._sQlen; - if (typeof(x) === "undefined") { x = 0; } if (typeof(y) === "undefined") { y = 0; } - buff[offset] = 3; // msg-type - buff[offset + 1] = incremental ? 1 : 0; + sock.sQpush8(3); // msg-type - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; + sock.sQpush8(incremental ? 1 : 0); - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(w); + sock.sQpush16(h); - buff[offset + 6] = (w >> 8) & 0xFF; - buff[offset + 7] = w & 0xFF; - - buff[offset + 8] = (h >> 8) & 0xFF; - buff[offset + 9] = h & 0xFF; - - sock._sQlen += 10; sock.flush(); }, xvpOp(sock, ver, op) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(250); // msg-type - buff[offset] = 250; // msg-type - buff[offset + 1] = 0; // padding + sock.sQpush8(0); // padding - buff[offset + 2] = ver; - buff[offset + 3] = op; + sock.sQpush8(ver); + sock.sQpush8(op); - sock._sQlen += 4; sock.flush(); } }; diff --git a/core/websock.js b/core/websock.js index 37b33fcc..21327c31 100644 --- a/core/websock.js +++ b/core/websock.js @@ -94,27 +94,7 @@ export default class Websock { return "unknown"; } - get sQ() { - return this._sQ; - } - - get rQ() { - return this._rQ; - } - - get rQi() { - return this._rQi; - } - - set rQi(val) { - this._rQi = val; - } - // Receive Queue - get rQlen() { - return this._rQlen - this._rQi; - } - rQpeek8() { return this._rQ[this._rQi]; } @@ -141,42 +121,47 @@ export default class Websock { for (let byte = bytes - 1; byte >= 0; byte--) { res += this._rQ[this._rQi++] << (byte * 8); } - return res; + return res >>> 0; } rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } let str = ""; // Handle large arrays in steps to avoid long strings on the stack for (let i = 0; i < len; i += 4096) { - let part = this.rQshiftBytes(Math.min(4096, len - i)); + let part = this.rQshiftBytes(Math.min(4096, len - i), false); str += String.fromCharCode.apply(null, part); } return str; } - rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + rQshiftBytes(len, copy=true) { this._rQi += len; - return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + if (copy) { + return this._rQ.slice(this._rQi - len, this._rQi); + } else { + return this._rQ.subarray(this._rQi - len, this._rQi); + } } rQshiftTo(target, len) { - if (len === undefined) { len = this.rQlen; } // TODO: make this just use set with views when using a ArrayBuffer to store the rQ target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); this._rQi += len; } - rQslice(start, end = this.rQlen) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + rQpeekBytes(len, copy=true) { + if (copy) { + return this._rQ.slice(this._rQi, this._rQi + len); + } else { + return this._rQ.subarray(this._rQi, this._rQi + len); + } } // Check to see if we must wait for 'num' bytes (default to FBU.bytes) // to be available in the receive queue. Return true if we need to // wait (and possibly print a debug message), otherwise false. rQwait(msg, num, goback) { - if (this.rQlen < num) { + if (this._rQlen - this._rQi < num) { if (goback) { if (this._rQi < goback) { throw new Error("rQwait cannot backup " + goback + " bytes"); @@ -190,21 +175,56 @@ export default class Websock { // Send Queue + sQpush8(num) { + this._sQensureSpace(1); + this._sQ[this._sQlen++] = num; + } + + sQpush16(num) { + this._sQensureSpace(2); + this._sQ[this._sQlen++] = (num >> 8) & 0xff; + this._sQ[this._sQlen++] = (num >> 0) & 0xff; + } + + sQpush32(num) { + this._sQensureSpace(4); + this._sQ[this._sQlen++] = (num >> 24) & 0xff; + this._sQ[this._sQlen++] = (num >> 16) & 0xff; + this._sQ[this._sQlen++] = (num >> 8) & 0xff; + this._sQ[this._sQlen++] = (num >> 0) & 0xff; + } + + sQpushString(str) { + let bytes = str.split('').map(chr => chr.charCodeAt(0)); + this.sQpushBytes(new Uint8Array(bytes)); + } + + sQpushBytes(bytes) { + for (let offset = 0;offset < bytes.length;) { + this._sQensureSpace(1); + + let chunkSize = this._sQbufferSize - this._sQlen; + if (chunkSize > bytes.length - offset) { + chunkSize = bytes.length - offset; + } + + this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen); + this._sQlen += chunkSize; + offset += chunkSize; + } + } + flush() { if (this._sQlen > 0 && this.readyState === 'open') { - this._websocket.send(this._encodeMessage()); + this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen)); this._sQlen = 0; } } - send(arr) { - this._sQ.set(arr, this._sQlen); - this._sQlen += arr.length; - this.flush(); - } - - sendString(str) { - this.send(str.split('').map(chr => chr.charCodeAt(0))); + _sQensureSpace(bytes) { + if (this._sQbufferSize - this._sQlen < bytes) { + this.flush(); + } } // Event Handlers @@ -283,17 +303,12 @@ export default class Websock { } // private methods - _encodeMessage() { - // Put in a binary arraybuffer - // according to the spec, you can send ArrayBufferViews with the send method - return new Uint8Array(this._sQ.buffer, 0, this._sQlen); - } // We want to move all the unread data to the start of the queue, // e.g. compacting. // The function also expands the receive que if needed, and for // performance reasons we combine these two actions to avoid - // unneccessary copying. + // unnecessary copying. _expandCompactRQ(minFit) { // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place // instead of resizing @@ -309,7 +324,7 @@ export default class Websock { // we don't want to grow unboundedly if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { this._rQbufferSize = MAX_RQ_GROW_SIZE; - if (this._rQbufferSize - this.rQlen < minFit) { + if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) { throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); } } @@ -327,25 +342,22 @@ export default class Websock { } // push arraybuffer values onto the end of the receive que - _DecodeMessage(data) { - const u8 = new Uint8Array(data); + _recvMessage(e) { + if (this._rQlen == this._rQi) { + // All data has now been processed, this means we + // can reset the receive queue. + this._rQlen = 0; + this._rQi = 0; + } + const u8 = new Uint8Array(e.data); if (u8.length > this._rQbufferSize - this._rQlen) { this._expandCompactRQ(u8.length); } this._rQ.set(u8, this._rQlen); this._rQlen += u8.length; - } - _recvMessage(e) { - this._DecodeMessage(e.data); - if (this.rQlen > 0) { + if (this._rQlen - this._rQi > 0) { this._eventHandlers.message(); - if (this._rQlen == this._rQi) { - // All data has now been processed, this means we - // can reset the receive queue. - this._rQlen = 0; - this._rQi = 0; - } } else { Log.Debug("Ignoring empty message"); } diff --git a/docs/API-internal.md b/docs/API-internal.md index 2cac8ddf..c41e0f32 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -81,9 +81,3 @@ None | blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display | drawImage | (img, x, y) | Draw image and track damage | autoscale | (containerWidth, containerHeight) | Scale the display - -### 2.2.3 Callbacks - -| name | parameters | description -| ------- | ---------- | ------------ -| onflush | () | A display flush has been requested and we are now ready to resume FBU processing diff --git a/package.json b/package.json index a1f0b794..5847887f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novnc/novnc", - "version": "1.4.0-beta", + "version": "1.4.0", "description": "An HTML5 VNC client", "browser": "lib/rfb", "directories": { diff --git a/po/ja.po b/po/ja.po index a9b3dcd1..64da7323 100644 --- a/po/ja.po +++ b/po/ja.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: noVNC 1.1.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2020-07-03 16:11+0200\n" -"PO-Revision-Date: 2021-01-15 12:37+0900\n" +"POT-Creation-Date: 2022-12-27 15:24+0100\n" +"PO-Revision-Date: 2023-03-21 12:42+0900\n" "Last-Translator: nnn1590 \n" "Language-Team: Japanese\n" "Language: ja\n" @@ -19,286 +19,325 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 2.3\n" -#: ../app/ui.js:394 +#: ../app/ui.js:69 +msgid "HTTPS is required for full functionality" +msgstr "すべての機能を使用するにはHTTPS接続が必要です" + +#: ../app/ui.js:410 msgid "Connecting..." msgstr "接続しています..." -#: ../app/ui.js:401 +#: ../app/ui.js:417 msgid "Disconnecting..." msgstr "切断しています..." -#: ../app/ui.js:407 +#: ../app/ui.js:423 msgid "Reconnecting..." msgstr "再接続しています..." -#: ../app/ui.js:412 +#: ../app/ui.js:428 msgid "Internal error" msgstr "内部エラー" -#: ../app/ui.js:1008 +#: ../app/ui.js:1026 msgid "Must set host" msgstr "ホストを設定する必要があります" -#: ../app/ui.js:1090 +#: ../app/ui.js:1110 msgid "Connected (encrypted) to " msgstr "接続しました (暗号化済み): " -#: ../app/ui.js:1092 +#: ../app/ui.js:1112 msgid "Connected (unencrypted) to " msgstr "接続しました (暗号化されていません): " -#: ../app/ui.js:1115 +#: ../app/ui.js:1135 msgid "Something went wrong, connection is closed" msgstr "何らかの問題で、接続が閉じられました" -#: ../app/ui.js:1118 +#: ../app/ui.js:1138 msgid "Failed to connect to server" msgstr "サーバーへの接続に失敗しました" -#: ../app/ui.js:1128 +#: ../app/ui.js:1150 msgid "Disconnected" msgstr "切断しました" -#: ../app/ui.js:1143 +#: ../app/ui.js:1165 msgid "New connection has been rejected with reason: " msgstr "新規接続は次の理由で拒否されました: " -#: ../app/ui.js:1146 +#: ../app/ui.js:1168 msgid "New connection has been rejected" msgstr "新規接続は拒否されました" -#: ../app/ui.js:1181 +#: ../app/ui.js:1234 msgid "Credentials are required" msgstr "資格情報が必要です" -#: ../vnc.html:74 +#: ../vnc.html:57 msgid "noVNC encountered an error:" msgstr "noVNC でエラーが発生しました:" -#: ../vnc.html:84 +#: ../vnc.html:67 msgid "Hide/Show the control bar" msgstr "コントロールバーを隠す/表示する" -#: ../vnc.html:91 +#: ../vnc.html:76 msgid "Drag" msgstr "ドラッグ" -#: ../vnc.html:91 +#: ../vnc.html:76 msgid "Move/Drag Viewport" msgstr "ビューポートを移動/ドラッグ" -#: ../vnc.html:97 +#: ../vnc.html:82 msgid "Keyboard" msgstr "キーボード" -#: ../vnc.html:97 +#: ../vnc.html:82 msgid "Show Keyboard" msgstr "キーボードを表示" -#: ../vnc.html:102 +#: ../vnc.html:87 msgid "Extra keys" msgstr "追加キー" -#: ../vnc.html:102 +#: ../vnc.html:87 msgid "Show Extra Keys" msgstr "追加キーを表示" -#: ../vnc.html:107 +#: ../vnc.html:92 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:107 +#: ../vnc.html:92 msgid "Toggle Ctrl" -msgstr "Ctrl キーを切り替え" +msgstr "Ctrl キーをトグル" -#: ../vnc.html:110 +#: ../vnc.html:95 msgid "Alt" msgstr "Alt" -#: ../vnc.html:110 +#: ../vnc.html:95 msgid "Toggle Alt" -msgstr "Alt キーを切り替え" +msgstr "Alt キーをトグル" -#: ../vnc.html:113 +#: ../vnc.html:98 msgid "Toggle Windows" -msgstr "Windows キーを切り替え" +msgstr "Windows キーをトグル" -#: ../vnc.html:113 +#: ../vnc.html:98 msgid "Windows" msgstr "Windows" -#: ../vnc.html:116 +#: ../vnc.html:101 msgid "Send Tab" msgstr "Tab キーを送信" -#: ../vnc.html:116 +#: ../vnc.html:101 msgid "Tab" msgstr "Tab" -#: ../vnc.html:119 +#: ../vnc.html:104 msgid "Esc" msgstr "Esc" -#: ../vnc.html:119 +#: ../vnc.html:104 msgid "Send Escape" msgstr "Escape キーを送信" -#: ../vnc.html:122 +#: ../vnc.html:107 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:122 +#: ../vnc.html:107 msgid "Send Ctrl-Alt-Del" msgstr "Ctrl-Alt-Del を送信" -#: ../vnc.html:129 +#: ../vnc.html:114 msgid "Shutdown/Reboot" msgstr "シャットダウン/再起動" -#: ../vnc.html:129 +#: ../vnc.html:114 msgid "Shutdown/Reboot..." msgstr "シャットダウン/再起動..." -#: ../vnc.html:135 +#: ../vnc.html:120 msgid "Power" msgstr "電源" -#: ../vnc.html:137 +#: ../vnc.html:122 msgid "Shutdown" msgstr "シャットダウン" -#: ../vnc.html:138 +#: ../vnc.html:123 msgid "Reboot" msgstr "再起動" -#: ../vnc.html:139 +#: ../vnc.html:124 msgid "Reset" msgstr "リセット" -#: ../vnc.html:144 ../vnc.html:150 +#: ../vnc.html:129 ../vnc.html:135 msgid "Clipboard" msgstr "クリップボード" -#: ../vnc.html:154 -msgid "Clear" -msgstr "クリア" +#: ../vnc.html:137 +msgid "Edit clipboard content in the textarea below." +msgstr "以下の入力欄からクリップボードの内容を編集できます。" -#: ../vnc.html:160 -msgid "Fullscreen" +#: ../vnc.html:145 +msgid "Full Screen" msgstr "全画面表示" -#: ../vnc.html:165 ../vnc.html:172 +#: ../vnc.html:150 ../vnc.html:156 msgid "Settings" msgstr "設定" -#: ../vnc.html:175 +#: ../vnc.html:160 msgid "Shared Mode" msgstr "共有モード" -#: ../vnc.html:178 +#: ../vnc.html:163 msgid "View Only" -msgstr "表示のみ" +msgstr "表示専用" -#: ../vnc.html:182 +#: ../vnc.html:167 msgid "Clip to Window" msgstr "ウィンドウにクリップ" -#: ../vnc.html:185 +#: ../vnc.html:170 msgid "Scaling Mode:" msgstr "スケーリングモード:" -#: ../vnc.html:187 +#: ../vnc.html:172 msgid "None" msgstr "なし" -#: ../vnc.html:188 +#: ../vnc.html:173 msgid "Local Scaling" msgstr "ローカルスケーリング" -#: ../vnc.html:189 +#: ../vnc.html:174 msgid "Remote Resizing" msgstr "リモートでリサイズ" -#: ../vnc.html:194 +#: ../vnc.html:179 msgid "Advanced" msgstr "高度" -#: ../vnc.html:197 +#: ../vnc.html:182 msgid "Quality:" msgstr "品質:" -#: ../vnc.html:201 +#: ../vnc.html:186 msgid "Compression level:" msgstr "圧縮レベル:" -#: ../vnc.html:206 +#: ../vnc.html:191 msgid "Repeater ID:" msgstr "リピーター ID:" -#: ../vnc.html:210 +#: ../vnc.html:195 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:213 +#: ../vnc.html:198 msgid "Encrypt" msgstr "暗号化" -#: ../vnc.html:216 +#: ../vnc.html:201 msgid "Host:" msgstr "ホスト:" -#: ../vnc.html:220 +#: ../vnc.html:205 msgid "Port:" msgstr "ポート:" -#: ../vnc.html:224 +#: ../vnc.html:209 msgid "Path:" msgstr "パス:" -#: ../vnc.html:231 +#: ../vnc.html:216 msgid "Automatic Reconnect" msgstr "自動再接続" -#: ../vnc.html:234 +#: ../vnc.html:219 msgid "Reconnect Delay (ms):" msgstr "再接続する遅延 (ミリ秒):" -#: ../vnc.html:239 +#: ../vnc.html:224 msgid "Show Dot when No Cursor" -msgstr "カーソルがないときにドットを表示" +msgstr "カーソルがないときにドットを表示する" -#: ../vnc.html:244 +#: ../vnc.html:229 msgid "Logging:" msgstr "ロギング:" -#: ../vnc.html:253 +#: ../vnc.html:238 msgid "Version:" msgstr "バージョン:" -#: ../vnc.html:261 +#: ../vnc.html:246 msgid "Disconnect" msgstr "切断" -#: ../vnc.html:280 +#: ../vnc.html:269 msgid "Connect" msgstr "接続" -#: ../vnc.html:290 +#: ../vnc.html:278 +msgid "Server identity" +msgstr "サーバーの識別情報" + +#: ../vnc.html:281 +msgid "The server has provided the following identifying information:" +msgstr "サーバーは以下の識別情報を提供しています:" + +#: ../vnc.html:285 +msgid "Fingerprint:" +msgstr "フィンガープリント:" + +#: ../vnc.html:288 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"この情報が正しい場合は「承認」を、そうでない場合は「拒否」を押してく" +"ださい。" + +#: ../vnc.html:293 +msgid "Approve" +msgstr "承認" + +#: ../vnc.html:294 +msgid "Reject" +msgstr "拒否" + +#: ../vnc.html:302 +msgid "Credentials" +msgstr "資格情報" + +#: ../vnc.html:306 msgid "Username:" msgstr "ユーザー名:" -#: ../vnc.html:294 +#: ../vnc.html:310 msgid "Password:" msgstr "パスワード:" -#: ../vnc.html:298 +#: ../vnc.html:314 msgid "Send Credentials" msgstr "資格情報を送信" -#: ../vnc.html:308 +#: ../vnc.html:323 msgid "Cancel" msgstr "キャンセル" +#~ msgid "Clear" +#~ msgstr "クリア" + #~ msgid "Password is required" #~ msgstr "パスワードが必要です" diff --git a/po/sv.po b/po/sv.po index 0f0e90b5..972e4000 100644 --- a/po/sv.po +++ b/po/sv.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: noVNC 1.3.0\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n" -"POT-Creation-Date: 2021-08-27 16:03+0200\n" -"PO-Revision-Date: 2021-08-27 16:18+0200\n" +"POT-Creation-Date: 2023-01-20 12:54+0100\n" +"PO-Revision-Date: 2023-01-20 12:58+0100\n" "Last-Translator: Samuel Mannehed \n" "Language-Team: none\n" "Language: sv\n" @@ -17,265 +17,269 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.0.3\n" +"X-Generator: Poedit 3.2.2\n" -#: ../app/ui.js:400 +#: ../app/ui.js:69 +msgid "HTTPS is required for full functionality" +msgstr "HTTPS krävs för full funktionalitet" + +#: ../app/ui.js:410 msgid "Connecting..." msgstr "Ansluter..." -#: ../app/ui.js:407 +#: ../app/ui.js:417 msgid "Disconnecting..." msgstr "Kopplar ner..." -#: ../app/ui.js:413 +#: ../app/ui.js:423 msgid "Reconnecting..." msgstr "Återansluter..." -#: ../app/ui.js:418 +#: ../app/ui.js:428 msgid "Internal error" msgstr "Internt fel" -#: ../app/ui.js:1009 +#: ../app/ui.js:1026 msgid "Must set host" msgstr "Du måste specifiera en värd" -#: ../app/ui.js:1091 +#: ../app/ui.js:1110 msgid "Connected (encrypted) to " msgstr "Ansluten (krypterat) till " -#: ../app/ui.js:1093 +#: ../app/ui.js:1112 msgid "Connected (unencrypted) to " msgstr "Ansluten (okrypterat) till " -#: ../app/ui.js:1116 +#: ../app/ui.js:1135 msgid "Something went wrong, connection is closed" msgstr "Något gick fel, anslutningen avslutades" -#: ../app/ui.js:1119 +#: ../app/ui.js:1138 msgid "Failed to connect to server" msgstr "Misslyckades att ansluta till servern" -#: ../app/ui.js:1129 +#: ../app/ui.js:1150 msgid "Disconnected" msgstr "Frånkopplad" -#: ../app/ui.js:1144 +#: ../app/ui.js:1165 msgid "New connection has been rejected with reason: " msgstr "Ny anslutning har blivit nekad med följande skäl: " -#: ../app/ui.js:1147 +#: ../app/ui.js:1168 msgid "New connection has been rejected" msgstr "Ny anslutning har blivit nekad" -#: ../app/ui.js:1182 +#: ../app/ui.js:1234 msgid "Credentials are required" msgstr "Användaruppgifter krävs" -#: ../vnc.html:61 +#: ../vnc.html:55 msgid "noVNC encountered an error:" msgstr "noVNC stötte på ett problem:" -#: ../vnc.html:71 +#: ../vnc.html:65 msgid "Hide/Show the control bar" msgstr "Göm/Visa kontrollbaren" -#: ../vnc.html:78 +#: ../vnc.html:74 msgid "Drag" msgstr "Dra" -#: ../vnc.html:78 +#: ../vnc.html:74 msgid "Move/Drag Viewport" msgstr "Flytta/Dra Vyn" -#: ../vnc.html:84 +#: ../vnc.html:80 msgid "Keyboard" msgstr "Tangentbord" -#: ../vnc.html:84 +#: ../vnc.html:80 msgid "Show Keyboard" msgstr "Visa Tangentbord" -#: ../vnc.html:89 +#: ../vnc.html:85 msgid "Extra keys" msgstr "Extraknappar" -#: ../vnc.html:89 +#: ../vnc.html:85 msgid "Show Extra Keys" msgstr "Visa Extraknappar" -#: ../vnc.html:94 +#: ../vnc.html:90 msgid "Ctrl" msgstr "Ctrl" -#: ../vnc.html:94 +#: ../vnc.html:90 msgid "Toggle Ctrl" msgstr "Växla Ctrl" -#: ../vnc.html:97 +#: ../vnc.html:93 msgid "Alt" msgstr "Alt" -#: ../vnc.html:97 +#: ../vnc.html:93 msgid "Toggle Alt" msgstr "Växla Alt" -#: ../vnc.html:100 +#: ../vnc.html:96 msgid "Toggle Windows" msgstr "Växla Windows" -#: ../vnc.html:100 +#: ../vnc.html:96 msgid "Windows" msgstr "Windows" -#: ../vnc.html:103 +#: ../vnc.html:99 msgid "Send Tab" msgstr "Skicka Tab" -#: ../vnc.html:103 +#: ../vnc.html:99 msgid "Tab" msgstr "Tab" -#: ../vnc.html:106 +#: ../vnc.html:102 msgid "Esc" msgstr "Esc" -#: ../vnc.html:106 +#: ../vnc.html:102 msgid "Send Escape" msgstr "Skicka Escape" -#: ../vnc.html:109 +#: ../vnc.html:105 msgid "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del" -#: ../vnc.html:109 +#: ../vnc.html:105 msgid "Send Ctrl-Alt-Del" msgstr "Skicka Ctrl-Alt-Del" -#: ../vnc.html:116 +#: ../vnc.html:112 msgid "Shutdown/Reboot" msgstr "Stäng av/Boota om" -#: ../vnc.html:116 +#: ../vnc.html:112 msgid "Shutdown/Reboot..." msgstr "Stäng av/Boota om..." -#: ../vnc.html:122 +#: ../vnc.html:118 msgid "Power" msgstr "Ström" -#: ../vnc.html:124 +#: ../vnc.html:120 msgid "Shutdown" msgstr "Stäng av" -#: ../vnc.html:125 +#: ../vnc.html:121 msgid "Reboot" msgstr "Boota om" -#: ../vnc.html:126 +#: ../vnc.html:122 msgid "Reset" msgstr "Återställ" -#: ../vnc.html:131 ../vnc.html:137 +#: ../vnc.html:127 ../vnc.html:133 msgid "Clipboard" msgstr "Urklipp" -#: ../vnc.html:141 -msgid "Clear" -msgstr "Rensa" +#: ../vnc.html:135 +msgid "Edit clipboard content in the textarea below." +msgstr "Redigera urklippets innehåll i fältet nedan." -#: ../vnc.html:147 -msgid "Fullscreen" +#: ../vnc.html:143 +msgid "Full Screen" msgstr "Fullskärm" -#: ../vnc.html:152 ../vnc.html:159 +#: ../vnc.html:148 ../vnc.html:154 msgid "Settings" msgstr "Inställningar" -#: ../vnc.html:162 +#: ../vnc.html:158 msgid "Shared Mode" msgstr "Delat Läge" -#: ../vnc.html:165 +#: ../vnc.html:161 msgid "View Only" msgstr "Endast Visning" -#: ../vnc.html:169 +#: ../vnc.html:165 msgid "Clip to Window" msgstr "Begränsa till Fönster" -#: ../vnc.html:172 +#: ../vnc.html:168 msgid "Scaling Mode:" msgstr "Skalningsläge:" -#: ../vnc.html:174 +#: ../vnc.html:170 msgid "None" msgstr "Ingen" -#: ../vnc.html:175 +#: ../vnc.html:171 msgid "Local Scaling" msgstr "Lokal Skalning" -#: ../vnc.html:176 +#: ../vnc.html:172 msgid "Remote Resizing" msgstr "Ändra Storlek" -#: ../vnc.html:181 +#: ../vnc.html:177 msgid "Advanced" msgstr "Avancerat" -#: ../vnc.html:184 +#: ../vnc.html:180 msgid "Quality:" msgstr "Kvalitet:" -#: ../vnc.html:188 +#: ../vnc.html:184 msgid "Compression level:" msgstr "Kompressionsnivå:" -#: ../vnc.html:193 +#: ../vnc.html:189 msgid "Repeater ID:" msgstr "Repeater-ID:" -#: ../vnc.html:197 +#: ../vnc.html:193 msgid "WebSocket" msgstr "WebSocket" -#: ../vnc.html:200 +#: ../vnc.html:196 msgid "Encrypt" msgstr "Kryptera" -#: ../vnc.html:203 +#: ../vnc.html:199 msgid "Host:" msgstr "Värd:" -#: ../vnc.html:207 +#: ../vnc.html:203 msgid "Port:" msgstr "Port:" -#: ../vnc.html:211 +#: ../vnc.html:207 msgid "Path:" msgstr "Sökväg:" -#: ../vnc.html:218 +#: ../vnc.html:214 msgid "Automatic Reconnect" msgstr "Automatisk Återanslutning" -#: ../vnc.html:221 +#: ../vnc.html:217 msgid "Reconnect Delay (ms):" msgstr "Fördröjning (ms):" -#: ../vnc.html:226 +#: ../vnc.html:222 msgid "Show Dot when No Cursor" msgstr "Visa prick när ingen muspekare finns" -#: ../vnc.html:231 +#: ../vnc.html:227 msgid "Logging:" msgstr "Loggning:" -#: ../vnc.html:240 +#: ../vnc.html:236 msgid "Version:" msgstr "Version:" -#: ../vnc.html:248 +#: ../vnc.html:244 msgid "Disconnect" msgstr "Koppla från" @@ -283,18 +287,53 @@ msgstr "Koppla från" msgid "Connect" msgstr "Anslut" -#: ../vnc.html:277 +#: ../vnc.html:276 +msgid "Server identity" +msgstr "Server-identitet" + +#: ../vnc.html:279 +msgid "The server has provided the following identifying information:" +msgstr "Servern har gett följande identifierande information:" + +#: ../vnc.html:283 +msgid "Fingerprint:" +msgstr "Fingeravtryck:" + +#: ../vnc.html:286 +msgid "" +"Please verify that the information is correct and press \"Approve\". " +"Otherwise press \"Reject\"." +msgstr "" +"Kontrollera att informationen är korrekt och tryck sedan " +"\"Godkänn\". Tryck annars \"Neka\"." + +#: ../vnc.html:291 +msgid "Approve" +msgstr "Godkänn" + +#: ../vnc.html:292 +msgid "Reject" +msgstr "Neka" + +#: ../vnc.html:300 +msgid "Credentials" +msgstr "Användaruppgifter" + +#: ../vnc.html:304 msgid "Username:" msgstr "Användarnamn:" -#: ../vnc.html:281 +#: ../vnc.html:308 msgid "Password:" msgstr "Lösenord:" -#: ../vnc.html:285 +#: ../vnc.html:312 msgid "Send Credentials" msgstr "Skicka Användaruppgifter" -#: ../vnc.html:295 +#: ../vnc.html:321 msgid "Cancel" msgstr "Avbryt" + +#~ msgid "Clear" +#~ msgstr "Rensa" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 24244243..02094820 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -42,7 +42,7 @@ parts: - jq websockify: - source: https://github.com/novnc/websockify/archive/v0.9.0.tar.gz + source: https://github.com/novnc/websockify/archive/v0.11.0.tar.gz plugin: python stage-packages: - python3-numpy diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index 8fb02c57..d273fe07 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -42,7 +42,7 @@ export default class FakeWebSocket { } _getSentData() { - const res = new Uint8Array(this._sendQueue.buffer, 0, this.bufferedAmount); + const res = this._sendQueue.slice(0, this.bufferedAmount); this.bufferedAmount = 0; return res; } @@ -55,11 +55,15 @@ export default class FakeWebSocket { } _receiveData(data) { - // Break apart the data to expose bugs where we assume data is - // neatly packaged - for (let i = 0;i < data.length;i++) { - let buf = data.subarray(i, i+1); - this.onmessage(new MessageEvent("message", { 'data': buf })); + if (data.length < 4096) { + // Break apart the data to expose bugs where we assume data is + // neatly packaged + for (let i = 0;i < data.length;i++) { + let buf = data.slice(i, i+1); + this.onmessage(new MessageEvent("message", { 'data': buf.buffer })); + } + } else { + this.onmessage(new MessageEvent("message", { 'data': data.buffer })); } } } diff --git a/tests/playback-ui.js b/tests/playback-ui.js index d76adb4f..b1f263ea 100644 --- a/tests/playback-ui.js +++ b/tests/playback-ui.js @@ -24,7 +24,7 @@ function loadFile() { return new Promise((resolve, reject) => { const script = document.createElement("script"); script.onload = resolve; - script.onerror = reject; + script.onerror = () => { reject("Failed to load " + fname); }; document.body.appendChild(script); script.src = "../recordings/" + fname; }); @@ -200,6 +200,9 @@ function start() { player.onrfbdisconnected = (evt) => { if (!evt.detail.clean) { message(`noVNC sent disconnected during iteration ${evt.detail.iteration} frame ${evt.detail.frame}`); + + document.getElementById('startButton').disabled = false; + document.getElementById('startButton').value = "Start"; } }; player.onfinish = (evt) => { diff --git a/tests/playback.js b/tests/playback.js index 19ab2c34..955df0ee 100644 --- a/tests/playback.js +++ b/tests/playback.js @@ -131,12 +131,10 @@ export default class RecordingPlayer { _doPacket() { // Avoid having excessive queue buildup in non-realtime mode if (this._trafficManagement && this._rfb._flushing) { - const orig = this._rfb._display.onflush; - this._rfb._display.onflush = () => { - this._rfb._display.onflush = orig; - this._rfb._onFlush(); - this._doPacket(); - }; + this._rfb.flush() + .then(() => { + this._doPacket(); + }); return; } @@ -150,13 +148,8 @@ export default class RecordingPlayer { _finish() { if (this._rfb._display.pending()) { - this._rfb._display.onflush = () => { - if (this._rfb._flushing) { - this._rfb._onFlush(); - } - this._finish(); - }; - this._rfb._display.flush(); + this._rfb._display.flush() + .then(() => { this._finish(); }); } else { this._running = false; this._ws.onclose({code: 1000, reason: ""}); diff --git a/tests/test.copyrect.js b/tests/test.copyrect.js index 90ba0c68..a10cddce 100644 --- a/tests/test.copyrect.js +++ b/tests/test.copyrect.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('CopyRect Decoder', function () { @@ -47,12 +50,15 @@ describe('CopyRect Decoder', function () { display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]); display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 0, 2, 2, 2, - [0x00, 0x02, 0x00, 0x00], - display, 24); - testDecodeRect(decoder, 2, 2, 2, 2, - [0x00, 0x00, 0x00, 0x00], - display, 24); + let done; + done = testDecodeRect(decoder, 0, 2, 2, 2, + [0x00, 0x02, 0x00, 0x00], + display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 2, 2, 2, + [0x00, 0x00, 0x00, 0x00], + display, 24); + expect(done).to.be.true; let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -69,7 +75,9 @@ describe('CopyRect Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [0x00, 0x00, 0x00, 0x00], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, + [0x00, 0x00, 0x00, 0x00], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -78,6 +86,7 @@ describe('CopyRect Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); }); diff --git a/tests/test.display.js b/tests/test.display.js index 0604997c..e6c0406f 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -298,14 +298,11 @@ describe('Display/Canvas Helper', function () { expect(display).to.have.displayed(checkedData); }); - it('should support drawing images via #imageRect', function (done) { + it('should support drawing images via #imageRect', async function () { display.imageRect(0, 0, 4, 4, "image/png", makeImagePng(checkedData, 4, 4)); display.flip(); - display.onflush = () => { - expect(display).to.have.displayed(checkedData); - done(); - }; - display.flush(); + await display.flush(); + expect(display).to.have.displayed(checkedData); }); it('should support blit images with true color via #blitImage', function () { @@ -360,12 +357,11 @@ describe('Display/Canvas Helper', function () { expect(img.addEventListener).to.have.been.calledOnce; }); - it('should call callback when queue is flushed', function () { - display.onflush = sinon.spy(); + it('should resolve promise when queue is flushed', async function () { display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); - expect(display.onflush).to.not.have.been.called; - display.flush(); - expect(display.onflush).to.have.been.calledOnce; + let promise = display.flush(); + expect(promise).to.be.an.instanceOf(Promise); + await promise; }); it('should draw a blit image on type "blit"', function () { diff --git a/tests/test.hextile.js b/tests/test.hextile.js index a7034f05..cbe6f7b5 100644 --- a/tests/test.hextile.js +++ b/tests/test.hextile.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } function push32(arr, num) { @@ -62,7 +65,7 @@ describe('Hextile Decoder', function () { data.push(2 | (2 << 4)); // x: 2, y: 2 data.push(1 | (1 << 4)); // width: 2, height: 2 - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -71,6 +74,7 @@ describe('Hextile Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -92,8 +96,9 @@ describe('Hextile Decoder', function () { data.push(0); } - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -102,13 +107,14 @@ describe('Hextile Decoder', function () { data.push(0x02); push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let expected = []; for (let i = 0; i < 16; i++) { push32(expected, 0x00ff00ff); } + expect(done).to.be.true; expect(display).to.have.displayed(new Uint8Array(expected)); }); @@ -125,7 +131,7 @@ describe('Hextile Decoder', function () { // send an empty frame data.push(0x00); - testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 32, 4, data, display, 24); let expected = []; for (let i = 0; i < 16; i++) { @@ -135,6 +141,7 @@ describe('Hextile Decoder', function () { push32(expected, 0x00ff00ff); // rect 2: same bkground color } + expect(done).to.be.true; expect(display).to.have.displayed(new Uint8Array(expected)); }); @@ -156,7 +163,7 @@ describe('Hextile Decoder', function () { data.push(2 | (2 << 4)); // x: 2, y: 2 data.push(1 | (1 << 4)); // width: 2, height: 2 - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -165,6 +172,7 @@ describe('Hextile Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -190,7 +198,7 @@ describe('Hextile Decoder', function () { data.push(0); // x: 0, y: 0 data.push(1 | (1 << 4)); // width: 2, height: 2 - testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 17, data, display, 24); let targetData = [ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -205,6 +213,7 @@ describe('Hextile Decoder', function () { } expected = expected.concat(targetData.slice(0, 16)); + expect(done).to.be.true; expect(display).to.have.displayed(new Uint8Array(expected)); }); @@ -218,7 +227,7 @@ describe('Hextile Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -227,6 +236,7 @@ describe('Hextile Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); }); diff --git a/tests/test.jpeg.js b/tests/test.jpeg.js index 6834f03d..8dee4891 100644 --- a/tests/test.jpeg.js +++ b/tests/test.jpeg.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('JPEG Decoder', function () { @@ -41,7 +44,7 @@ describe('JPEG Decoder', function () { display.resize(4, 4); }); - it('should handle JPEG rects', function (done) { + it('should handle JPEG rects', async function () { let data = [ // JPEG data 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, @@ -131,7 +134,8 @@ describe('JPEG Decoder', function () { 0xff, 0xd9, ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + expect(decodeDone).to.be.true; let targetData = new Uint8Array([ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, @@ -147,14 +151,11 @@ describe('JPEG Decoder', function () { return diff < 5; } - display.onflush = () => { - expect(display).to.have.displayed(targetData, almost); - done(); - }; - display.flush(); + await display.flush(); + expect(display).to.have.displayed(targetData, almost); }); - it('should handle JPEG rects without Huffman and quantification tables', function (done) { + it('should handle JPEG rects without Huffman and quantification tables', async function () { let data1 = [ // JPEG data 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, @@ -244,7 +245,12 @@ describe('JPEG Decoder', function () { 0xff, 0xd9, ]; - testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24); + let decodeDone; + + decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24); + expect(decodeDone).to.be.true; + + display.fillRect(0, 0, 4, 4, [128, 128, 128, 255]); let data2 = [ // JPEG data @@ -263,7 +269,8 @@ describe('JPEG Decoder', function () { 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9, ]; - testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24); + decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24); + expect(decodeDone).to.be.true; let targetData = new Uint8Array([ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, @@ -279,10 +286,7 @@ describe('JPEG Decoder', function () { return diff < 5; } - display.onflush = () => { - expect(display).to.have.displayed(targetData, almost); - done(); - }; - display.flush(); + await display.flush(); + expect(display).to.have.displayed(targetData, almost); }); }); diff --git a/tests/test.localization.js b/tests/test.localization.js index 7e8e6c13..916ff846 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -1,61 +1,146 @@ const expect = chai.expect; -import { l10n } from '../app/localization.js'; +import _, { Localizer, l10n } from '../app/localization.js'; describe('Localization', function () { "use strict"; + let origNavigator; + let fetch; + + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + + Object.defineProperty(window, "navigator", {value: {}}); + window.navigator.languages = []; + + fetch = sinon.stub(window, "fetch"); + fetch.resolves(new Response("{}")); + }); + afterEach(function () { + fetch.restore(); + + Object.defineProperty(window, "navigator", origNavigator); + }); + + describe('Singleton', function () { + it('should export a singleton object', function () { + expect(l10n).to.be.instanceOf(Localizer); + }); + it('should export a singleton translation function', async function () { + // FIXME: Can we use some spy instead? + window.navigator.languages = ["de"]; + fetch.resolves(new Response(JSON.stringify({ "Foobar": "gazonk" }))); + await l10n.setup(["de"]); + expect(_("Foobar")).to.equal("gazonk"); + }); + }); + describe('language selection', function () { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - - Object.defineProperty(window, "navigator", {value: {}}); - window.navigator.languages = []; - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); - it('should use English by default', function () { - expect(l10n.language).to.equal('en'); + let lclz = new Localizer(); + expect(lclz.language).to.equal('en'); }); - it('should use English if no user language matches', function () { + it('should use English if no user language matches', async function () { window.navigator.languages = ["nl", "de"]; - l10n.setup(["es", "fr"]); - expect(l10n.language).to.equal('en'); + let lclz = new Localizer(); + await lclz.setup(["es", "fr"]); + expect(lclz.language).to.equal('en'); }); - it('should use the most preferred user language', function () { + it('should fall back to generic English for other English', async function () { + window.navigator.languages = ["en-AU", "de"]; + let lclz = new Localizer(); + await lclz.setup(["de", "fr", "en-GB"]); + expect(lclz.language).to.equal('en'); + }); + it('should prefer specific English over generic', async function () { + window.navigator.languages = ["en-GB", "de"]; + let lclz = new Localizer(); + await lclz.setup(["de", "en-AU", "en-GB"]); + expect(lclz.language).to.equal('en-GB'); + }); + it('should use the most preferred user language', async function () { window.navigator.languages = ["nl", "de", "fr"]; - l10n.setup(["es", "fr", "de"]); - expect(l10n.language).to.equal('de'); + let lclz = new Localizer(); + await lclz.setup(["es", "fr", "de"]); + expect(lclz.language).to.equal('de'); }); - it('should prefer sub-languages languages', function () { + it('should prefer sub-languages languages', async function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt", "pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); + let lclz = new Localizer(); + await lclz.setup(["pt", "pt-BR"]); + expect(lclz.language).to.equal('pt-BR'); }); - it('should fall back to language "parents"', function () { + it('should fall back to language "parents"', async function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["fr", "pt", "de"]); - expect(l10n.language).to.equal('pt'); + let lclz = new Localizer(); + await lclz.setup(["fr", "pt", "de"]); + expect(lclz.language).to.equal('pt'); }); - it('should not use specific language when user asks for a generic language', function () { + it('should not use specific language when user asks for a generic language', async function () { window.navigator.languages = ["pt", "de"]; - l10n.setup(["fr", "pt-BR", "de"]); - expect(l10n.language).to.equal('de'); + let lclz = new Localizer(); + await lclz.setup(["fr", "pt-BR", "de"]); + expect(lclz.language).to.equal('de'); }); - it('should handle underscore as a separator', function () { + it('should handle underscore as a separator', async function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt_BR"]); - expect(l10n.language).to.equal('pt_BR'); + let lclz = new Localizer(); + await lclz.setup(["pt_BR"]); + expect(lclz.language).to.equal('pt_BR'); }); - it('should handle difference in case', function () { + it('should handle difference in case', async function () { window.navigator.languages = ["pt-br"]; - l10n.setup(["pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); + let lclz = new Localizer(); + await lclz.setup(["pt-BR"]); + expect(lclz.language).to.equal('pt-BR'); + }); + }); + + describe('Translation loading', function () { + it('should not fetch a translation for English', async function () { + window.navigator.languages = []; + let lclz = new Localizer(); + await lclz.setup([]); + expect(fetch).to.not.have.been.called; + }); + it('should fetch dictionary relative base URL', async function () { + window.navigator.languages = ["de", "fr"]; + fetch.resolves(new Response('{ "Foobar": "gazonk" }')); + let lclz = new Localizer(); + await lclz.setup(["ru", "fr"], "/some/path/"); + expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json"); + expect(lclz.get("Foobar")).to.equal("gazonk"); + }); + it('should handle base URL without trailing slash', async function () { + window.navigator.languages = ["de", "fr"]; + fetch.resolves(new Response('{ "Foobar": "gazonk" }')); + let lclz = new Localizer(); + await lclz.setup(["ru", "fr"], "/some/path"); + expect(fetch).to.have.been.calledOnceWith("/some/path/fr.json"); + expect(lclz.get("Foobar")).to.equal("gazonk"); + }); + it('should handle current base URL', async function () { + window.navigator.languages = ["de", "fr"]; + fetch.resolves(new Response('{ "Foobar": "gazonk" }')); + let lclz = new Localizer(); + await lclz.setup(["ru", "fr"]); + expect(fetch).to.have.been.calledOnceWith("fr.json"); + expect(lclz.get("Foobar")).to.equal("gazonk"); + }); + it('should fail if dictionary cannot be found', async function () { + window.navigator.languages = ["de", "fr"]; + fetch.resolves(new Response('{}', { status: 404 })); + let lclz = new Localizer(); + let ok = false; + try { + await lclz.setup(["ru", "fr"], "/some/path/"); + } catch (e) { + ok = true; + } + expect(ok).to.be.true; }); }); }); diff --git a/tests/test.ra2.js b/tests/test.ra2.js deleted file mode 100644 index cc505b1f..00000000 --- a/tests/test.ra2.js +++ /dev/null @@ -1,357 +0,0 @@ -const expect = chai.expect; - -import RFB from '../core/rfb.js'; - -import FakeWebSocket from './fake.websocket.js'; - -function fakeGetRandomValues(arr) { - if (arr.length === 16) { - arr.set(new Uint8Array([ - 0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9, - 0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5, - ])); - } else { - arr.set(new Uint8Array([ - 0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95, - 0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf, - 0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf, - 0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87, - 0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e, - 0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a, - 0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3, - 0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62, - 0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69, - 0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0, - 0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c, - 0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77, - 0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7, - 0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f, - 0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15, - 0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70, - 0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64, - 0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c, - 0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14, - 0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b, - 0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a, - 0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f, - 0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f, - 0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58, - 0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc, - 0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2, - 0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f, - 0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a, - 0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81, - 0x31, 0xb0, 0x69, 0xd4, 0x4e, - ])); - } -} - -async function fakeGeneratekey() { - let key = JSON.parse('{"alg":"RSA-OAEP-256","d":"B7QR2yI8sXjo8vQhJpX9odqqR\ -6wIuPrTM1B1JJEKVeSrr7OYcc1FRJ52Vap9LIAU-ezigs9QDvWMxknB8motLnG69Wck37nt9_z4s8l\ -FQp0nROA-oaR92HW34KNL1b2fEVWGI0N86h730MvTJC5O2cmKeMezIG-oNqbbfFyP8AW-WLdDlgZm1\ -1-FjzhbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5WeoyX3yBtQy\ -dpauW6rrgyWdtP4hDFIoZsX6w1i-UMWMMwlIB5FdnUSi26igVGADGpV_vGMP36bv-EHp0bY-Qp0gpI\ -fLfgQ","dp":"Z1v5UceFfV2bhmbG19eGYb30jFxqoRBq36PKNY7IunMs1keYy0FpLbyGhtgMZ1Ymm\ -c8wEzGYsCPEP-ykcun_rlyu7YxmcnyC9YQqTqLyqvO-7rUqDvk9TMfdqWFP6heADRhKZmEbmcau6_m\ -2MwwK9kOkMKWvpqp8_TpJMnAH7zE","dq":"OBacRE15aY3NtCR4cvP5os3sT70JbDdDLHT3IHZM6r\ -E35CYNpLDia2chm_wnMcYvKFW9zC2ajRZ15i9c_VXQzS7ZlTaQYBFyMt7kVhxMEMFsPv1crD6t3uEI\ -j0LNuNYyy0jkon_LPZKQFK654CiL-L2YaNXOH4HbHP02dWeVQIE","e":"AQAB","ext":true,"ke\ -y_ops":["decrypt"],"kty":"RSA","n":"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3yThy1P_mcqr\ -GDQkRiGVdcTxAk38T9PgLztmspF-6U5TAHO-gSmmW88AC9m6f1Mspps6r7zl-M_OG-TwvGzf3BDz8z\ -Eg1FPbZV7whO1M4TCAZ0PqwG7qCc6nK1WiAhaKrSpzuPdL1igfNBsX7qu5wgw4ZTTGSLbVC_LfULQ5\ -FADgFTRXUSaxm1F8C_Lwy6a2e4nTcXilmtN2IHUjHegzm-Tq2HizmR3ARdWJpESYIW5-AXoiqj29tD\ -rqCmu2WPkB2psVp83IzZfaQNQzjNfvA8GpimkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q","p":"2Q_lN\ -L7vCOBzAppYzCZo3WSh0hX-MOZyPUznks5U2TjmfdNZoL6_FJRiGyyLvwSiZFdEAAvpAyESFfFigng\ -AqMLSf448nPg15VUGj533CotsEM0WpoEr1JCgqdUbgDAfJQIBcwOmegBqd7lWm7uzEnRCvouB70ybk\ -JfpdprhkVE","q":"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXGzslLt5nLmss8JqdLoDDrijjU-gjeRh\ -7lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXbMq2sUZqJvYEyL74\ -H2Zrt0RPAux7XQLEVgND6ROdXnMJ70O0","qi":"qfl4cXQkz4BNqa2De0-PfdU-8d1w3onnaGqx1D\ -s2fHzD_SJ4cNghn2TksoT9Qo64b3pUjH9igi2pyEjomk6D12N6FG0e10u7vFKv3W5YqUOgTpYdbcWH\ -dZ2qZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7Wk_0MdqQy8qvixWD4zLcY"}'); - key = await window.crypto.subtle.importKey("jwk", key, { - name: "RSA-OAEP", - hash: {name: "SHA-256"} - }, true, ["decrypt"]); - return {privateKey: key}; -} - -const receiveData = new Uint8Array([ - // server public key - 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42, - 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6, - 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f, - 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08, - 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a, - 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d, - 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40, - 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd, - 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33, - 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07, - 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7, - 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11, - 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7, - 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff, - 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca, - 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39, - 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda, - 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a, - 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07, - 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a, - 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a, - 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62, - 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f, - 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39, - 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3, - 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7, - 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d, - 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98, - 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71, - 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31, - 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f, - 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01, - 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x01, - // server random - 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb, - 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe, - 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31, - 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b, - 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe, - 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0, - 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79, - 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6, - 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66, - 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a, - 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4, - 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5, - 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11, - 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35, - 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32, - 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29, - 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4, - 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6, - 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab, - 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48, - 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15, - 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5, - 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a, - 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36, - 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11, - 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11, - 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59, - 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d, - 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95, - 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14, - 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, - 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, - 0xdf, 0xcb, - // server hash - 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a, - 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47, - 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01, - 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e, - 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13, - // subtype - 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c, - 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7, - 0x94, 0xd0, 0x19, -]); - -const sendData = new Uint8Array([ - // client public key - 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9, - 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8, - 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f, - 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f, - 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62, - 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4, - 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17, - 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12, - 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7, - 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce, - 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6, - 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d, - 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4, - 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06, - 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20, - 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74, - 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea, - 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64, - 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43, - 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12, - 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c, - 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a, - 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde, - 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39, - 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49, - 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3, - 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65, - 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc, - 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd, - 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0, - 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28, - 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80, - 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x01, 0x00, 0x01, - // client random - 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6, - 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30, - 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c, - 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68, - 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4, - 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c, - 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b, - 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d, - 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e, - 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a, - 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a, - 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5, - 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62, - 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19, - 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23, - 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c, - 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed, - 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb, - 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b, - 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27, - 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde, - 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce, - 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2, - 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b, - 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd, - 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad, - 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e, - 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b, - 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83, - 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4, - 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, - 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, - 0xed, 0x84, - // client hash - 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e, - 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea, - 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f, - 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84, - 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe, - // credentials - 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f, - 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82, - 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29, - 0x91, 0x38, -]); - -describe('RA2 handshake', function () { - let sock; - let rfb; - let sentData; - - before(() => { - FakeWebSocket.replace(); - sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues); - sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey); - }); - after(() => { - FakeWebSocket.restore(); - window.crypto.getRandomValues.restore(); - window.crypto.subtle.generateKey.restore(); - }); - - it('should fire the serververification event', function (done) { - sentData = new Uint8Array(); - rfb = new RFB(document.createElement('div'), "ws://example.com"); - sock = rfb._sock; - sock.send = (data) => { - let res = new Uint8Array(sentData.length + data.length); - res.set(sentData); - res.set(data, sentData.length); - sentData = res; - }; - rfb._rfbInitState = "Security"; - rfb._rfbVersion = 3.8; - sock._websocket._receiveData(new Uint8Array([1, 6])); - rfb.addEventListener("serververification", (e) => { - expect(e.detail.publickey).to.eql(receiveData.slice(0, 516)); - done(); - }); - sock._websocket._receiveData(receiveData); - }); - - it('should handle approveServer and fire the credentialsrequired event', function (done) { - rfb.addEventListener("credentialsrequired", (e) => { - expect(e.detail.types).to.eql(["password"]); - done(); - }); - rfb.approveServer(); - }); - - it('should match sendData after sending credentials', function (done) { - rfb.addEventListener("securityresult", (event) => { - expect(sentData.slice(1)).to.eql(sendData); - done(); - }); - rfb.sendCredentials({ "password": "123456" }); - }); -}); diff --git a/tests/test.raw.js b/tests/test.raw.js index bc7adc78..4a634ccd 100644 --- a/tests/test.raw.js +++ b/tests/test.raw.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('Raw Decoder', function () { @@ -42,22 +45,36 @@ describe('Raw Decoder', function () { }); it('should handle the Raw encoding', function () { - testDecodeRect(decoder, 0, 0, 2, 2, - [0xff, 0x00, 0x00, 0, 0x00, 0xff, 0x00, 0, - 0x00, 0xff, 0x00, 0, 0xff, 0x00, 0x00, 0], - display, 24); - testDecodeRect(decoder, 2, 0, 2, 2, - [0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0, - 0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0], - display, 24); - testDecodeRect(decoder, 0, 2, 4, 1, - [0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0, - 0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0], - display, 24); - testDecodeRect(decoder, 0, 3, 4, 1, - [0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0, - 0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0], - display, 24); + let done; + + done = testDecodeRect(decoder, 0, 0, 2, 2, + [0xff, 0x00, 0x00, 0, + 0x00, 0xff, 0x00, 0, + 0x00, 0xff, 0x00, 0, + 0xff, 0x00, 0x00, 0], + display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 0, 2, 2, + [0x00, 0x00, 0xff, 0, + 0x00, 0x00, 0xff, 0, + 0x00, 0x00, 0xff, 0, + 0x00, 0x00, 0xff, 0], + display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 2, 4, 1, + [0xee, 0x00, 0xff, 0, + 0x00, 0xee, 0xff, 0, + 0xaa, 0xee, 0xff, 0, + 0xab, 0xee, 0xff, 0], + display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 3, 4, 1, + [0xee, 0x00, 0xff, 0, + 0x00, 0xee, 0xff, 0, + 0xaa, 0xee, 0xff, 0, + 0xab, 0xee, 0xff, 0], + display, 24); + expect(done).to.be.true; let targetData = new Uint8Array([ 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, @@ -70,18 +87,24 @@ describe('Raw Decoder', function () { }); it('should handle the Raw encoding in low colour mode', function () { - testDecodeRect(decoder, 0, 0, 2, 2, - [0x30, 0x30, 0x30, 0x30], - display, 8); - testDecodeRect(decoder, 2, 0, 2, 2, - [0x0c, 0x0c, 0x0c, 0x0c], - display, 8); - testDecodeRect(decoder, 0, 2, 4, 1, - [0x0c, 0x0c, 0x30, 0x30], - display, 8); - testDecodeRect(decoder, 0, 3, 4, 1, - [0x0c, 0x0c, 0x30, 0x30], - display, 8); + let done; + + done = testDecodeRect(decoder, 0, 0, 2, 2, + [0x30, 0x30, 0x30, 0x30], + display, 8); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 0, 2, 2, + [0x0c, 0x0c, 0x0c, 0x0c], + display, 8); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 2, 4, 1, + [0x0c, 0x0c, 0x30, 0x30], + display, 8); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 3, 4, 1, + [0x0c, 0x0c, 0x30, 0x30], + display, 8); + expect(done).to.be.true; let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -98,7 +121,7 @@ describe('Raw Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -107,6 +130,7 @@ describe('Raw Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -115,7 +139,7 @@ describe('Raw Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8); + let done = testDecodeRect(decoder, 1, 2, 0, 0, [], display, 8); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -124,6 +148,7 @@ describe('Raw Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 2da38184..bf12a460 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -8,6 +8,7 @@ import { encodings } from '../core/encodings.js'; import { toUnsigned32bit } from '../core/util/int.js'; import { encodeUTF8 } from '../core/util/strings.js'; import KeyTable from '../core/input/keysym.js'; +import legacyCrypto from '../core/crypto/crypto.js'; import FakeWebSocket from './fake.websocket.js'; @@ -286,26 +287,6 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._sock.off).to.have.been.calledWith('open'); }); }); - - describe('#sendCredentials', function () { - let client; - beforeEach(function () { - client = makeRFB(); - client._rfbConnectionState = 'connecting'; - }); - - it('should set the rfb credentials properly"', function () { - client.sendCredentials({ password: 'pass' }); - expect(client._rfbCredentials).to.deep.equal({ password: 'pass' }); - }); - - it('should call initMsg "soon"', function () { - client._initMsg = sinon.spy(); - client.sendCredentials({ password: 'pass' }); - this.clock.tick(5); - expect(client._initMsg).to.have.been.calledOnce; - }); - }); }); describe('Public API Basic Behavior', function () { @@ -316,89 +297,114 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('#sendCtrlAlDel', function () { it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { - const expected = {_sQ: new Uint8Array(48), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 0xFFE3, 1); - RFB.messages.keyEvent(expected, 0xFFE9, 1); - RFB.messages.keyEvent(expected, 0xFFFF, 1); - RFB.messages.keyEvent(expected, 0xFFFF, 0); - RFB.messages.keyEvent(expected, 0xFFE9, 0); - RFB.messages.keyEvent(expected, 0xFFE3, 0); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.keyEvent(esock, 0xFFE3, 1); + RFB.messages.keyEvent(esock, 0xFFE9, 1); + RFB.messages.keyEvent(esock, 0xFFFF, 1); + RFB.messages.keyEvent(esock, 0xFFFF, 0); + RFB.messages.keyEvent(esock, 0xFFE9, 0); + RFB.messages.keyEvent(esock, 0xFFE3, 0); + let expected = ews._getSentData(); client.sendCtrlAltDel(); - expect(client._sock).to.have.sent(expected._sQ); + + expect(client._sock).to.have.sent(expected); }); it('should not send the keys if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); client._rfbConnectionState = "connecting"; client.sendCtrlAltDel(); - expect(client._sock.flush).to.not.have.been.called; + expect(client._sock).to.have.sent(new Uint8Array([])); }); it('should not send the keys if we are set as view_only', function () { - sinon.spy(client._sock, 'flush'); client._viewOnly = true; client.sendCtrlAltDel(); - expect(client._sock.flush).to.not.have.been.called; + expect(client._sock).to.have.sent(new Uint8Array([])); }); }); describe('#sendKey', function () { it('should send a single key with the given code and state (down = true)', function () { - const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.keyEvent(esock, 123, 1); + let expected = ews._getSentData(); + client.sendKey(123, 'Key123', true); - expect(client._sock).to.have.sent(expected._sQ); + + expect(client._sock).to.have.sent(expected); }); it('should send both a down and up event if the state is not specified', function () { - const expected = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); - RFB.messages.keyEvent(expected, 123, 0); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.keyEvent(esock, 123, 1); + RFB.messages.keyEvent(esock, 123, 0); + let expected = ews._getSentData(); + client.sendKey(123, 'Key123'); - expect(client._sock).to.have.sent(expected._sQ); + + expect(client._sock).to.have.sent(expected); }); it('should not send the key if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); client._rfbConnectionState = "connecting"; client.sendKey(123, 'Key123'); - expect(client._sock.flush).to.not.have.been.called; + expect(client._sock).to.have.sent(new Uint8Array([])); }); it('should not send the key if we are set as view_only', function () { - sinon.spy(client._sock, 'flush'); client._viewOnly = true; client.sendKey(123, 'Key123'); - expect(client._sock.flush).to.not.have.been.called; + expect(client._sock).to.have.sent(new Uint8Array([])); }); it('should send QEMU extended events if supported', function () { client._qemuExtKeyEventSupported = true; - const expected = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; - RFB.messages.QEMUExtendedKeyEvent(expected, 0x20, true, 0x0039); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.QEMUExtendedKeyEvent(esock, 0x20, true, 0x0039); + let expected = ews._getSentData(); + client.sendKey(0x20, 'Space', true); - expect(client._sock).to.have.sent(expected._sQ); + + expect(client._sock).to.have.sent(expected); }); it('should not send QEMU extended events if unknown key code', function () { client._qemuExtKeyEventSupported = true; - const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.keyEvent(esock, 123, 1); + let expected = ews._getSentData(); + client.sendKey(123, 'FooBar', true); - expect(client._sock).to.have.sent(expected._sQ); + + expect(client._sock).to.have.sent(expected); }); }); describe('#focus', function () { it('should move focus to canvas object', function () { - client._canvas.focus = sinon.spy(); + sinon.spy(client._canvas, "focus"); client.focus(); expect(client._canvas.focus).to.have.been.calledOnce; }); it('should include focus options', function () { - client._canvas.focus = sinon.spy(); + sinon.spy(client._canvas, "focus"); client.focus({ foobar: 12, gazonk: true }); expect(client._canvas.focus).to.have.been.calledOnce; expect(client._canvas.focus).to.have.been.calledWith({ foobar: 12, gazonk: true}); @@ -407,7 +413,7 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('#blur', function () { it('should remove focus from canvas object', function () { - client._canvas.blur = sinon.spy(); + sinon.spy(client._canvas, "blur"); client.blur(); expect(client._canvas.blur).to.have.been.calledOnce; }); @@ -808,10 +814,32 @@ describe('Remote Frame Buffer Protocol Client', function () { let client; beforeEach(function () { client = makeRFB(); - client._supportsSetDesktopSize = true; client.resizeSession = true; container.style.width = '70px'; container.style.height = '80px'; + + const incoming = [ 0x00, // msg-type=FBU + 0x00, // padding + 0x00, 0x01, // number of rects = 1 + 0x00, 0x00, // reason = server initialized + 0x00, 0x00, // status = no error + 0x00, 0x04, // new width = 4 + 0x00, 0x04, // new height = 4 + 0xff, 0xff, + 0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize + 0x01, // number of screens = 1 + 0x00, 0x00, + 0x00, // padding + 0x78, 0x90, + 0xab, 0xcd, // screen id = 0 + 0x00, 0x00, // screen x = 0 + 0x00, 0x00, // screen y = 0 + 0x00, 0x04, // screen width = 4 + 0x00, 0x04, // screen height = 4 + 0x12, 0x34, + 0x56, 0x78]; // screen flags + client._sock._websocket._receiveData(new Uint8Array(incoming)); + sinon.spy(RFB.messages, "setDesktopSize"); }); @@ -827,6 +855,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should request a resize when initially connecting', function () { + // Create a new object that hasn't yet seen a + // ExtendedDesktopSize rect + client = makeRFB(); + client.resizeSession = true; + container.style.width = '70px'; + container.style.height = '80px'; + // Simple ExtendedDesktopSize FBU message const incoming = [ 0x00, // msg-type=FBU 0x00, // padding @@ -840,17 +875,14 @@ describe('Remote Frame Buffer Protocol Client', function () { 0x01, // number of screens = 1 0x00, 0x00, 0x00, // padding - 0x00, 0x00, - 0x00, 0x00, // screen id = 0 + 0x78, 0x90, + 0xab, 0xcd, // screen id = 0 0x00, 0x00, // screen x = 0 0x00, 0x00, // screen y = 0 0x00, 0x04, // screen width = 4 0x00, 0x04, // screen height = 4 - 0x00, 0x00, - 0x00, 0x00]; // screen flags - - // This property is indirectly used as a marker for the first update - client._supportsSetDesktopSize = false; + 0x12, 0x34, + 0x56, 0x78]; // screen flags // First message should trigger a resize @@ -860,7 +892,7 @@ describe('Remote Frame Buffer Protocol Client', function () { // not the reported size from the server expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( - sinon.match.object, 70, 80, 0, 0); + sinon.match.object, 70, 80, 0x7890abcd, 0x12345678); RFB.messages.setDesktopSize.resetHistory(); @@ -878,7 +910,8 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); }); it('should not request the same size twice', function () { @@ -889,7 +922,7 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; expect(RFB.messages.setDesktopSize).to.have.been.calledWith( - sinon.match.object, 40, 50, 0, 0); + sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); // Server responds with the requested size 40x50 const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, @@ -928,7 +961,8 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(200); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 40, 50, 0x7890abcd, 0x12345678); }); it('should not resize when resize is disabled', function () { @@ -968,9 +1002,9 @@ describe('Remote Frame Buffer Protocol Client', function () { // Simple ExtendedDesktopSize FBU message, new size: 100x100 const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x64, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0xab, 0xab, 0xab, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00 ]; + 0x11, 0x22, 0x33, 0x44 ]; // Note that this will cause the browser to display scrollbars // since the framebuffer is 100x100 and the container is 70x80. @@ -990,8 +1024,8 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(1000); expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize.firstCall.args[1]).to.equal(120); - expect(RFB.messages.setDesktopSize.firstCall.args[2]).to.equal(130); + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 120, 130, 0xabababab, 0x11223344); }); }); @@ -1100,9 +1134,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail on an invalid version', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + sendVer('002.000', client); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); }); @@ -1159,25 +1197,31 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail if there are no supported schemes', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + const authSchemes = [1, 32]; client._sock._websocket._receiveData(new Uint8Array(authSchemes)); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); it('should fail with the appropriate message if no types are sent', function () { const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; - sinon.spy(client, '_fail'); + let callback = sinon.spy(); + client.addEventListener("securityfailure", callback); + client._sock._websocket._receiveData(new Uint8Array(failureData)); - expect(client._fail).to.have.been.calledOnce; - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on no security types (reason: whoops)'); + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.status).to.equal(1); + expect(callback.args[0][0].detail.reason).to.equal("whoops"); }); it('should transition to the Authentication state and continue on successful negotiation', function () { - const authSchemes = [1, 1]; - client._negotiateAuthentication = sinon.spy(); + const authSchemes = [1, 2]; + sinon.spy(client, "_negotiateAuthentication"); client._sock._websocket._receiveData(new Uint8Array(authSchemes)); expect(client._rfbInitState).to.equal('Authentication'); expect(client._negotiateAuthentication).to.have.been.calledOnce; @@ -1196,11 +1240,14 @@ describe('Remote Frame Buffer Protocol Client', function () { sendVer('003.006\n', client); client._sock._websocket._getSentData(); + let callback = sinon.spy(); + client.addEventListener("securityfailure", callback); - sinon.spy(client, '_fail'); client._sock._websocket._receiveData(new Uint8Array(data)); - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on authentication scheme (reason: Whoopsies)'); + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.status).to.equal(1); + expect(callback.args[0][0].detail.reason).to.equal("Whoopsies"); }); it('should transition straight to ServerInitialisation on "no auth" for versions < 3.7', function () { @@ -1224,9 +1271,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail on an unknown auth scheme', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + sendSecurity(57, client); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); describe('VNC Authentication (type 2) Handler', function () { @@ -1239,44 +1290,505 @@ describe('Remote Frame Buffer Protocol Client', function () { for (let i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receiveData(new Uint8Array(challenge)); - expect(client._rfbCredentials).to.be.empty; expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.types).to.have.members(["password"]); }); it('should encrypt the password with DES and then send it back', function () { - client._rfbCredentials = { password: 'passwd' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ password: 'passwd' }); + }); sendSecurity(2, client); client._sock._websocket._getSentData(); // skip the choice of auth reply const challenge = []; for (let i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receiveData(new Uint8Array(challenge)); + clock.tick(); const desPass = RFB.genDES('passwd', challenge); expect(client._sock).to.have.sent(new Uint8Array(desPass)); }); it('should transition to SecurityResult immediately after sending the password', function () { - client._rfbCredentials = { password: 'passwd' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ password: 'passwd' }); + }); sendSecurity(2, client); const challenge = []; for (let i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receiveData(new Uint8Array(challenge)); + clock.tick(); expect(client._rfbInitState).to.equal('SecurityResult'); }); }); + describe('RSA-AES Authentication (type 6) Handler', function () { + function fakeGetRandomValues(arr) { + if (arr.length === 16) { + arr.set(new Uint8Array([ + 0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9, + 0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5, + ])); + } else { + arr.set(new Uint8Array([ + 0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95, + 0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf, + 0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf, + 0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87, + 0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e, + 0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a, + 0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3, + 0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62, + 0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69, + 0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0, + 0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c, + 0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77, + 0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7, + 0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f, + 0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15, + 0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70, + 0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64, + 0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c, + 0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14, + 0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b, + 0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a, + 0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f, + 0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f, + 0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58, + 0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc, + 0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2, + 0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f, + 0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a, + 0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81, + 0x31, 0xb0, 0x69, 0xd4, 0x4e, + ])); + } + } + + async function fakeGeneratekey() { + let key = { "alg": "RSA-OAEP-256", + "d": "B7QR2yI8sXjo8vQhJpX9odqqR6wIuPr" + + "TM1B1JJEKVeSrr7OYcc1FRJ52Vap9LI" + + "AU-ezigs9QDvWMxknB8motLnG69Wck3" + + "7nt9_z4s8lFQp0nROA-oaR92HW34KNL" + + "1b2fEVWGI0N86h730MvTJC5O2cmKeMe" + + "zIG-oNqbbfFyP8AW-WLdDlgZm11-Fjz" + + "hbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx" + + "4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5" + + "WeoyX3yBtQydpauW6rrgyWdtP4hDFIo" + + "ZsX6w1i-UMWMMwlIB5FdnUSi26igVGA" + + "DGpV_vGMP36bv-EHp0bY-Qp0gpIfLfgQ", + "dp": "Z1v5UceFfV2bhmbG19eGYb30jFxqoR" + + "Bq36PKNY7IunMs1keYy0FpLbyGhtgM" + + "Z1Ymmc8wEzGYsCPEP-ykcun_rlyu7Y" + + "xmcnyC9YQqTqLyqvO-7rUqDvk9TMfd" + + "qWFP6heADRhKZmEbmcau6_m2MwwK9k" + + "OkMKWvpqp8_TpJMnAH7zE", + "dq": "OBacRE15aY3NtCR4cvP5os3sT70JbD" + + "dDLHT3IHZM6rE35CYNpLDia2chm_wn" + + "McYvKFW9zC2ajRZ15i9c_VXQzS7ZlT" + + "aQYBFyMt7kVhxMEMFsPv1crD6t3uEI" + + "j0LNuNYyy0jkon_LPZKQFK654CiL-L" + + "2YaNXOH4HbHP02dWeVQIE", + "e": "AQAB", + "ext": true, + "key_ops": ["decrypt"], + "kty": "RSA", + "n": "m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3" + + "yThy1P_mcqrGDQkRiGVdcTxAk38T9Pg" + + "LztmspF-6U5TAHO-gSmmW88AC9m6f1M" + + "spps6r7zl-M_OG-TwvGzf3BDz8zEg1F" + + "PbZV7whO1M4TCAZ0PqwG7qCc6nK1WiA" + + "haKrSpzuPdL1igfNBsX7qu5wgw4ZTTG" + + "SLbVC_LfULQ5FADgFTRXUSaxm1F8C_L" + + "wy6a2e4nTcXilmtN2IHUjHegzm-Tq2H" + + "izmR3ARdWJpESYIW5-AXoiqj29tDrqC" + + "mu2WPkB2psVp83IzZfaQNQzjNfvA8Gp" + + "imkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q", + "p": "2Q_lNL7vCOBzAppYzCZo3WSh0hX-MOZ" + + "yPUznks5U2TjmfdNZoL6_FJRiGyyLvw" + + "SiZFdEAAvpAyESFfFigngAqMLSf448n" + + "Pg15VUGj533CotsEM0WpoEr1JCgqdUb" + + "gDAfJQIBcwOmegBqd7lWm7uzEnRCvou" + + "B70ybkJfpdprhkVE", + "q": "tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXG" + + "zslLt5nLmss8JqdLoDDrijjU-gjeRh7" + + "lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ" + + "1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXb" + + "Mq2sUZqJvYEyL74H2Zrt0RPAux7XQLE" + + "VgND6ROdXnMJ70O0", + "qi": "qfl4cXQkz4BNqa2De0-PfdU-8d1w3o" + + "nnaGqx1Ds2fHzD_SJ4cNghn2TksoT9" + + "Qo64b3pUjH9igi2pyEjomk6D12N6FG" + + "0e10u7vFKv3W5YqUOgTpYdbcWHdZ2q" + + "ZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7" + + "Wk_0MdqQy8qvixWD4zLcY", + }; + key = await window.crypto.subtle.importKey("jwk", key, { + name: "RSA-OAEP", + hash: {name: "SHA-256"} + }, true, ["decrypt"]); + return {privateKey: key}; + } + + before(() => { + sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues); + sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey); + }); + after(() => { + window.crypto.getRandomValues.restore(); + window.crypto.subtle.generateKey.restore(); + }); + + beforeEach(function () { + sendSecurity(6, client); + expect(client._sock).to.have.sent(new Uint8Array([6])); + }); + + const serverPublicKey = [ + 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42, + 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6, + 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f, + 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08, + 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a, + 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d, + 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40, + 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd, + 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33, + 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07, + 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7, + 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11, + 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7, + 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff, + 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca, + 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39, + 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda, + 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a, + 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07, + 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a, + 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a, + 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62, + 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f, + 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39, + 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3, + 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7, + 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d, + 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98, + 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71, + 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31, + 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f, + 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01, + 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + ]; + + const serverRandom = [ + 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb, + 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe, + 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31, + 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b, + 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe, + 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0, + 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79, + 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6, + 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66, + 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a, + 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4, + 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5, + 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11, + 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35, + 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32, + 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29, + 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4, + 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6, + 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab, + 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48, + 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15, + 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5, + 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a, + 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36, + 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11, + 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11, + 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59, + 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d, + 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95, + 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14, + 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, + 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, + 0xdf, 0xcb, + ]; + + const serverHash = [ + 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a, + 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47, + 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01, + 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e, + 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13, + ]; + + const subType = [ + 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c, + 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7, + 0x94, 0xd0, 0x19, + ]; + + const clientPublicKey = [ + 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9, + 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8, + 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f, + 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f, + 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62, + 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4, + 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17, + 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12, + 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7, + 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce, + 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6, + 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d, + 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4, + 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06, + 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20, + 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74, + 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea, + 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64, + 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43, + 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12, + 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c, + 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a, + 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde, + 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39, + 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49, + 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3, + 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65, + 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc, + 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd, + 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0, + 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28, + 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80, + 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + ]; + + const clientRandom = [ + 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6, + 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30, + 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c, + 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68, + 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4, + 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c, + 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b, + 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d, + 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e, + 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a, + 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a, + 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5, + 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62, + 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19, + 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23, + 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c, + 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed, + 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb, + 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b, + 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27, + 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde, + 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce, + 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2, + 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b, + 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd, + 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad, + 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e, + 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b, + 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83, + 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4, + 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, + 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, + 0xed, 0x84, + ]; + + const clientHash = [ + 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e, + 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea, + 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f, + 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84, + 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe, + ]; + + const credentialsData = [ + 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f, + 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82, + 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29, + 0x91, 0x38, + ]; + + it('should fire the serververification event', async function () { + let verification = new Promise((resolve, reject) => { + client.addEventListener("serververification", (e) => { + resolve(e.detail.publickey); + }); + }); + + client._sock._websocket._receiveData(new Uint8Array(serverPublicKey)); + client._sock._websocket._receiveData(new Uint8Array(serverRandom)); + + expect(await verification).to.deep.equal(new Uint8Array(serverPublicKey)); + }); + + it('should handle approveServer and fire the credentialsrequired event', async function () { + let verification = new Promise((resolve, reject) => { + client.addEventListener("serververification", (e) => { + resolve(e.detail.publickey); + }); + }); + let credentials = new Promise((resolve, reject) => { + client.addEventListener("credentialsrequired", (e) => { + resolve(e.detail.types); + }); + }); + + client._sock._websocket._receiveData(new Uint8Array(serverPublicKey)); + client._sock._websocket._receiveData(new Uint8Array(serverRandom)); + + await verification; + client.approveServer(); + + client._sock._websocket._receiveData(new Uint8Array(serverHash)); + client._sock._websocket._receiveData(new Uint8Array(subType)); + + expect(await credentials).to.have.members(["password"]); + }); + + it('should send credentials to server', async function () { + let verification = new Promise((resolve, reject) => { + client.addEventListener("serververification", (e) => { + resolve(e.detail.publickey); + }); + }); + let credentials = new Promise((resolve, reject) => { + client.addEventListener("credentialsrequired", (e) => { + resolve(e.detail.types); + }); + }); + + client._sock._websocket._receiveData(new Uint8Array(serverPublicKey)); + client._sock._websocket._receiveData(new Uint8Array(serverRandom)); + + await verification; + client.approveServer(); + + client._sock._websocket._receiveData(new Uint8Array(serverHash)); + client._sock._websocket._receiveData(new Uint8Array(subType)); + + await credentials; + + let expected = []; + expected = expected.concat(clientPublicKey); + expected = expected.concat(clientRandom); + expected = expected.concat(clientHash); + expect(client._sock).to.have.sent(new Uint8Array(expected)); + + client.sendCredentials({ "password": "123456" }); + clock.tick(); + + // FIXME: We don't have a good way to know when + // the async stuff is done, so we hook in + // to this internal function that is + // called at the end + await new Promise((resolve, reject) => { + sinon.stub(client._sock._websocket, "send") + .callsFake((data) => { + FakeWebSocket.prototype.send.call(client._sock._websocket, data); + resolve(); + }); + }); + + expect(client._sock).to.have.sent(new Uint8Array(credentialsData)); + }); + }); + describe('ARD Authentication (type 30) Handler', function () { + let byteArray = new Uint8Array(Array.from(new Uint8Array(128).keys())); + function fakeGetRandomValues(arr) { + if (arr.length == 128) { + arr.set(byteArray); + } + return arr; + } + before(() => { + sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues); + }); + after(() => { + window.crypto.getRandomValues.restore(); + }); it('should fire the credentialsrequired event if all credentials are missing', function () { const spy = sinon.spy(); client.addEventListener("credentialsrequired", spy); - client._rfbCredentials = {}; sendSecurity(30, client); - expect(client._rfbCredentials).to.be.empty; expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.types).to.have.members(["username", "password"]); }); @@ -1284,7 +1796,7 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should fire the credentialsrequired event if some credentials are missing', function () { const spy = sinon.spy(); client.addEventListener("credentialsrequired", spy); - client._rfbCredentials = { password: 'password'}; + client.sendCredentials({ password: 'password'}); sendSecurity(30, client); expect(spy).to.have.been.calledOnce; @@ -1292,41 +1804,56 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should return properly encrypted credentials and public key', async function () { - client._rfbCredentials = { username: 'user', - password: 'password' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'user', + password: 'password' }); + }); sendSecurity(30, client); expect(client._sock).to.have.sent([30]); - function byteArray(length) { - return Array.from(new Uint8Array(length).keys()); - } + const generator = new Uint8Array([127, 255]); + const prime = new Uint8Array(byteArray); + const serverKey = legacyCrypto.generateKey( + { name: "DH", g: generator, p: prime }, false, ["deriveBits"]); + const clientKey = legacyCrypto.generateKey( + { name: "DH", g: generator, p: prime }, false, ["deriveBits"]); + const serverPublicKey = legacyCrypto.exportKey("raw", serverKey.publicKey); + const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey); - let generator = [127, 255]; - let prime = byteArray(128); - let serverPrivateKey = byteArray(128); - let serverPublicKey = client._modPow(generator, serverPrivateKey, prime); + let data = []; - let clientPrivateKey = byteArray(128); - let clientPublicKey = client._modPow(generator, clientPrivateKey, prime); + data = data.concat(Array.from(generator)); + push16(data, prime.length); + data = data.concat(Array.from(prime)); + data = data.concat(Array.from(serverPublicKey)); - let padding = Array.from(byteArray(64), byte => String.fromCharCode(65+byte%26)).join(''); + client._sock._websocket._receiveData(new Uint8Array(data)); - await client._negotiateARDAuthAsync(generator, 128, prime, serverPublicKey, clientPrivateKey, padding); - - client._negotiateARDAuth(); + // FIXME: We don't have a good way to know when the + // async stuff is done, so we hook in to this + // internal function that is called at the + // end + await new Promise((resolve, reject) => { + sinon.stub(client, "_resumeAuthentication") + .callsFake(() => { + RFB.prototype._resumeAuthentication.call(client); + resolve(); + }); + }); + clock.tick(); expect(client._rfbInitState).to.equal('SecurityResult'); let expectEncrypted = new Uint8Array([ - 232, 234, 159, 162, 170, 180, 138, 104, 164, 49, 53, 96, 20, 36, 21, 15, - 217, 219, 107, 173, 196, 60, 96, 142, 215, 71, 13, 185, 185, 47, 5, 175, - 151, 30, 194, 55, 173, 214, 141, 161, 36, 138, 146, 3, 178, 89, 43, 248, - 131, 134, 205, 174, 9, 150, 171, 74, 222, 201, 20, 2, 30, 168, 162, 123, - 46, 86, 81, 221, 44, 211, 180, 247, 221, 61, 95, 155, 157, 241, 76, 76, - 49, 217, 234, 75, 147, 237, 199, 159, 93, 140, 191, 174, 52, 90, 133, 58, - 243, 81, 112, 182, 64, 62, 149, 7, 151, 28, 36, 161, 247, 247, 36, 96, - 230, 95, 58, 207, 46, 183, 100, 139, 143, 155, 224, 43, 219, 3, 71, 139]); + 199, 39, 204, 95, 190, 70, 127, 66, 5, 106, 153, 228, 123, 236, 150, 206, + 62, 107, 11, 4, 21, 242, 92, 184, 9, 81, 35, 125, 56, 167, 1, 215, + 182, 145, 183, 75, 245, 197, 47, 19, 122, 94, 64, 76, 77, 163, 222, 143, + 186, 174, 84, 39, 244, 179, 227, 114, 83, 231, 42, 106, 205, 43, 159, 110, + 209, 240, 157, 246, 237, 206, 134, 153, 195, 112, 92, 60, 28, 234, 91, 66, + 131, 38, 187, 195, 110, 167, 212, 241, 32, 250, 212, 213, 202, 89, 180, 21, + 71, 217, 209, 81, 42, 61, 118, 248, 65, 123, 98, 78, 139, 111, 202, 137, + 50, 185, 37, 173, 58, 99, 187, 53, 42, 125, 13, 165, 232, 163, 151, 42, 0]); let output = new Uint8Array(256); output.set(expectEncrypted, 0); @@ -1397,8 +1924,10 @@ describe('Remote Frame Buffer Protocol Client', function () { window.crypto.getRandomValues.restore(); }); it('should send public value and encrypted credentials', function () { - client._rfbCredentials = { username: 'username', - password: 'password123456' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'username', + password: 'password123456' }); + }); sendSecurity(113, client); expect(client._sock).to.have.sent([113]); @@ -1410,6 +1939,7 @@ describe('Remote Frame Buffer Protocol Client', function () { client._sock._websocket._receiveData(g); client._sock._websocket._receiveData(p); client._sock._websocket._receiveData(A); + clock.tick(); expect(client._sock).to.have.sent(expected); expect(client._rfbInitState).to.equal('SecurityResult'); @@ -1418,21 +1948,22 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('XVP Authentication (type 22) Handler', function () { it('should fall through to standard VNC authentication upon completion', function () { - client._rfbCredentials = { username: 'user', - target: 'target', - password: 'password' }; - client._negotiateStdVNCAuth = sinon.spy(); + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'user', + target: 'target', + password: 'password' }); + }); + sinon.spy(client, "_negotiateStdVNCAuth"); sendSecurity(22, client); + clock.tick(); expect(client._negotiateStdVNCAuth).to.have.been.calledOnce; }); it('should fire the credentialsrequired event if all credentials are missing', function () { const spy = sinon.spy(); client.addEventListener("credentialsrequired", spy); - client._rfbCredentials = {}; sendSecurity(22, client); - expect(client._rfbCredentials).to.be.empty; expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]); }); @@ -1440,8 +1971,8 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should fire the credentialsrequired event if some credentials are missing', function () { const spy = sinon.spy(); client.addEventListener("credentialsrequired", spy); - client._rfbCredentials = { username: 'user', - target: 'target' }; + client.sendCredentials({ username: 'user', + target: 'target' }); sendSecurity(22, client); expect(spy).to.have.been.calledOnce; @@ -1449,12 +1980,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should send user and target separately', function () { - client._rfbCredentials = { username: 'user', - target: 'target', - password: 'password' }; - client._negotiateStdVNCAuth = sinon.spy(); - + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'user', + target: 'target', + password: 'password' }); + }); sendSecurity(22, client); + clock.tick(); const expected = [22, 4, 6]; // auth selection, len user, len target for (let i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } @@ -1492,9 +2024,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail if no supported tunnels are listed', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + sendNumStrPairs([[123, 'OTHR', 'SOMETHNG']], client); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); it('should choose the notunnel tunnel type', function () { @@ -1515,25 +2051,18 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._rfbInitState).to.equal('SecurityResult'); }); - /*it('should attempt to use VNC auth over no auth when possible', function () { - client._rfbTightVNC = true; - client._negotiateStdVNCAuth = sinon.spy(); - sendNumStrPairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client); - expect(client._sock).to.have.sent([0, 0, 0, 1]); - expect(client._negotiateStdVNCAuth).to.have.been.calledOnce; - expect(client._rfbAuthScheme).to.equal(2); - });*/ // while this would make sense, the original code doesn't actually do this - it('should accept the "no auth" auth type and transition to SecurityResult', function () { - client._rfbTightVNC = true; + sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._getSentData(); // skip the tunnel choice here sendNumStrPairs([[1, 'STDV', 'NOAUTH__']], client); expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); expect(client._rfbInitState).to.equal('SecurityResult'); }); it('should accept VNC authentication and transition to that', function () { - client._rfbTightVNC = true; - client._negotiateStdVNCAuth = sinon.spy(); + sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._getSentData(); // skip the tunnel choice here + sinon.spy(client, "_negotiateStdVNCAuth"); sendNumStrPairs([[2, 'STDV', 'VNCAUTH__']], client); expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); expect(client._negotiateStdVNCAuth).to.have.been.calledOnce; @@ -1541,10 +2070,15 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail if there are no supported auth types', function () { - sinon.spy(client, "_fail"); - client._rfbTightVNC = true; + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + + sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._getSentData(); // skip the tunnel choice here sendNumStrPairs([[23, 'stdv', 'badval__']], client); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); }); @@ -1555,9 +2089,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail with non-0.2 versions', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + client._sock._websocket._receiveData(new Uint8Array([0, 1])); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); it('should fail if there are no supported subtypes', function () { @@ -1567,9 +2105,11 @@ describe('Remote Frame Buffer Protocol Client', function () { // Server ACK. client._sock._websocket._receiveData(new Uint8Array([0])); // Subtype list - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4])); - expect(client._fail).to.have.been.calledOnce; + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); it('should support standard types', function () { @@ -1623,74 +2163,68 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); }); + }); - it('should support Plain authentication', function () { - client._rfbCredentials = { username: 'username', password: 'password' }; + describe('Plain Authentication (type 256) Handler', function () { + beforeEach(function () { + sendSecurity(19, client); + expect(client._sock).to.have.sent(new Uint8Array([19])); // VeNCrypt version client._sock._websocket._receiveData(new Uint8Array([0, 2])); expect(client._sock).to.have.sent(new Uint8Array([0, 2])); // Server ACK. client._sock._websocket._receiveData(new Uint8Array([0])); - // Subtype list. + }); + + it('should support Plain authentication', function () { + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'username', password: 'password' }); + }); client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0])); + + clock.tick(); const expectedResponse = []; - push32(expectedResponse, 256); // Chosen subtype. - push32(expectedResponse, client._rfbCredentials.username.length); - push32(expectedResponse, client._rfbCredentials.password.length); - pushString(expectedResponse, client._rfbCredentials.username); - pushString(expectedResponse, client._rfbCredentials.password); + push32(expectedResponse, 8); + push32(expectedResponse, 8); + pushString(expectedResponse, 'username'); + pushString(expectedResponse, 'password'); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - client._initMsg = sinon.spy(); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); - expect(client._initMsg).to.have.been.called; }); it('should support Plain authentication with an empty password', function () { - client._rfbCredentials = { username: 'username', password: '' }; - // VeNCrypt version - client._sock._websocket._receiveData(new Uint8Array([0, 2])); - expect(client._sock).to.have.sent(new Uint8Array([0, 2])); - // Server ACK. - client._sock._websocket._receiveData(new Uint8Array([0])); - // Subtype list. + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'username', password: '' }); + }); client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0])); + + clock.tick(); const expectedResponse = []; - push32(expectedResponse, 256); // Chosen subtype. - push32(expectedResponse, client._rfbCredentials.username.length); - push32(expectedResponse, client._rfbCredentials.password.length); - pushString(expectedResponse, client._rfbCredentials.username); - pushString(expectedResponse, client._rfbCredentials.password); + push32(expectedResponse, 8); + push32(expectedResponse, 0); + pushString(expectedResponse, 'username'); + pushString(expectedResponse, ''); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - client._initMsg = sinon.spy(); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); - expect(client._initMsg).to.have.been.called; }); it('should support Plain authentication with a very long username and password', function () { - client._rfbCredentials = { username: 'a'.repeat(300), password: 'a'.repeat(300) }; - // VeNCrypt version - client._sock._websocket._receiveData(new Uint8Array([0, 2])); - expect(client._sock).to.have.sent(new Uint8Array([0, 2])); - // Server ACK. - client._sock._websocket._receiveData(new Uint8Array([0])); - // Subtype list. + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'a'.repeat(300), password: 'b'.repeat(300) }); + }); client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0])); + + clock.tick(); const expectedResponse = []; - push32(expectedResponse, 256); // Chosen subtype. - push32(expectedResponse, client._rfbCredentials.username.length); - push32(expectedResponse, client._rfbCredentials.password.length); - pushString(expectedResponse, client._rfbCredentials.username); - pushString(expectedResponse, client._rfbCredentials.password); + push32(expectedResponse, 300); + push32(expectedResponse, 300); + pushString(expectedResponse, 'a'.repeat(300)); + pushString(expectedResponse, 'b'.repeat(300)); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - client._initMsg = sinon.spy(); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); - expect(client._initMsg).to.have.been.called; }); }); }); @@ -1961,23 +2495,31 @@ describe('Remote Frame Buffer Protocol Client', function () { } it('should send an update request if there is sufficient data', function () { - const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.fbUpdateRequest(expectedMsg, true, 0, 0, 640, 20); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20); + let expected = ews._getSentData(); client._framebufferUpdate = () => true; client._sock._websocket._receiveData(new Uint8Array([0])); - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); }); it('should not send an update request if we need more data', function () { client._sock._websocket._receiveData(new Uint8Array([0])); - expect(client._sock._websocket._getSentData()).to.have.length(0); + expect(client._sock).to.have.sent(new Uint8Array([])); }); it('should resume receiving an update if we previously did not have enough data', function () { - const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.fbUpdateRequest(expectedMsg, true, 0, 0, 640, 20); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20); + let expected = ews._getSentData(); // just enough to set FBU.rects client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 3])); @@ -1986,7 +2528,7 @@ describe('Remote Frame Buffer Protocol Client', function () { client._framebufferUpdate = function () { this._sock.rQskipBytes(1); return true; }; // we magically have enough data // 247 should *not* be used as the message type here client._sock._websocket._receiveData(new Uint8Array([247])); - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); }); it('should not send a request in continuous updates mode', function () { @@ -1994,14 +2536,18 @@ describe('Remote Frame Buffer Protocol Client', function () { client._framebufferUpdate = () => true; client._sock._websocket._receiveData(new Uint8Array([0])); - expect(client._sock._websocket._getSentData()).to.have.length(0); + expect(client._sock).to.have.sent(new Uint8Array([])); }); it('should fail on an unsupported encoding', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + const rectInfo = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; sendFbuMsg([rectInfo], [[]], client); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); describe('Message Encoding Handlers', function () { @@ -2411,7 +2957,11 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should handle the last_rect pseudo-encoding', function () { sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100); - expect(client._FBU.rects).to.equal(0); + // Send a bell message and make sure it is parsed + let spy = sinon.spy(); + client.addEventListener("bell", spy); + client._sock._websocket._receiveData(new Uint8Array([0x02])); + expect(spy).to.have.been.calledOnce; }); it('should handle the DesktopName pseudo-encoding', function () { @@ -2443,9 +2993,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail on unknown XVP message types', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + client._sock._websocket._receiveData(new Uint8Array([250, 0, 10, 237])); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); }); @@ -2519,17 +3073,14 @@ describe('Remote Frame Buffer Protocol Client', function () { let data = [3, 0, 0, 0]; const flags = [0x10, 0x00, 0x00, 0x01]; - /* The size 10 (utf8 encoded string size) and the - string "Aå漢字!" utf8 encoded and deflated. */ - let deflatedData = [120, 94, 99, 96, 96, 224, 114, 60, - 188, 244, 217, 158, 69, 79, 215, - 78, 87, 4, 0, 35, 207, 6, 66]; + let text = encodeUTF8("Aå漢字!"); + let deflatedText = deflateWithSize(text); // How much data we are sending. - push32(data, toUnsigned32bit(-(4 + deflatedData.length))); + push32(data, toUnsigned32bit(-(4 + deflatedText.length))); data = data.concat(flags); - data = data.concat(deflatedData); + data = data.concat(Array.from(deflatedText)); const spy = sinon.spy(); client.addEventListener("clipboard", spy); @@ -2553,15 +3104,12 @@ describe('Remote Frame Buffer Protocol Client', function () { push32(data, toUnsigned32bit(-(4 + deflatedText.length))); data = data.concat(flags); - - let sendData = new Uint8Array(data.length + deflatedText.length); - sendData.set(data); - sendData.set(deflatedText, data.length); + data = data.concat(Array.from(deflatedText)); const spy = sinon.spy(); client.addEventListener("clipboard", spy); - client._sock._websocket._receiveData(sendData); + client._sock._websocket._receiveData(new Uint8Array(data)); expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.text).to.equal(expectedData); client.removeEventListener("clipboard", spy); @@ -2580,15 +3128,12 @@ describe('Remote Frame Buffer Protocol Client', function () { push32(data, toUnsigned32bit(-(4 + deflatedText.length))); data = data.concat(flags); - - let sendData = new Uint8Array(data.length + deflatedText.length); - sendData.set(data); - sendData.set(deflatedText, data.length); + data = data.concat(Array.from(deflatedText)); const spy = sinon.spy(); client.addEventListener("clipboard", spy); - client._sock._websocket._receiveData(sendData); + client._sock._websocket._receiveData(new Uint8Array(data)); expect(spy).to.have.been.calledOnce; expect(spy.args[0][0].detail.text).to.equal(expectedData); client.removeEventListener("clipboard", spy); @@ -2697,41 +3242,47 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should respond correctly to ServerFence', function () { - const expectedMsg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; - const incomingMsg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; - const payload = "foo\x00ab9"; + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + // ClientFence and ServerFence are identical in structure - RFB.messages.clientFence(expectedMsg, (1<<0) | (1<<1), payload); - RFB.messages.clientFence(incomingMsg, 0xffffffff, payload); + RFB.messages.clientFence(esock, (1<<0) | (1<<1), payload); + let expected = ews._getSentData(); + RFB.messages.clientFence(esock, 0xffffffff, payload); + let incoming = ews._getSentData(); - client._sock._websocket._receiveData(incomingMsg._sQ); + client._sock._websocket._receiveData(incoming); - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); - expectedMsg._sQlen = 0; - incomingMsg._sQlen = 0; + RFB.messages.clientFence(esock, (1<<0), payload); + expected = ews._getSentData(); + RFB.messages.clientFence(esock, (1<<0) | (1<<31), payload); + incoming = ews._getSentData(); - RFB.messages.clientFence(expectedMsg, (1<<0), payload); - RFB.messages.clientFence(incomingMsg, (1<<0) | (1<<31), payload); + client._sock._websocket._receiveData(incoming); - client._sock._websocket._receiveData(incomingMsg._sQ); - - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); }); it('should enable continuous updates on first EndOfContinousUpdates', function () { - const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - - RFB.messages.enableContinuousUpdates(expectedMsg, true, 0, 0, 640, 20); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 640, 20); + let expected = ews._getSentData(); expect(client._enabledContinuousUpdates).to.be.false; client._sock._websocket._receiveData(new Uint8Array([150])); expect(client._enabledContinuousUpdates).to.be.true; - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); }); it('should disable continuous updates on subsequent EndOfContinousUpdates', function () { @@ -2744,24 +3295,32 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should update continuous updates on resize', function () { - const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.enableContinuousUpdates(expectedMsg, true, 0, 0, 90, 700); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 90, 700); + let expected = ews._getSentData(); client._resize(450, 160); - expect(client._sock._websocket._getSentData()).to.have.length(0); + expect(client._sock).to.have.sent(new Uint8Array([])); client._enabledContinuousUpdates = true; client._resize(90, 700); - expect(client._sock).to.have.sent(expectedMsg._sQ); + expect(client._sock).to.have.sent(expected); }); it('should fail on an unknown message type', function () { - sinon.spy(client, "_fail"); + let callback = sinon.spy(); + client.addEventListener("disconnect", callback); + client._sock._websocket._receiveData(new Uint8Array([87])); - expect(client._fail).to.have.been.calledOnce; + + expect(callback).to.have.been.calledOnce; + expect(callback.args[0][0].detail.clean).to.be.false; }); }); @@ -3174,10 +3733,16 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('Keyboard Events', function () { it('should send a key message on a key press', function () { + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); + RFB.messages.keyEvent(esock, 0x41, 1); + let expected = ews._getSentData(); + client._handleKeyEvent(0x41, 'KeyA', true); - const keyMsg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(keyMsg, 0x41, 1); - expect(client._sock).to.have.sent(keyMsg._sQ); + + expect(client._sock).to.have.sent(expected); }); it('should not send messages in view-only mode', function () { @@ -3882,13 +4447,13 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('WebSocket Events', function () { // message events it('should do nothing if we receive an empty message and have nothing in the queue', function () { - client._normalMsg = sinon.spy(); + sinon.spy(client, "_normalMsg"); client._sock._websocket._receiveData(new Uint8Array([])); expect(client._normalMsg).to.not.have.been.called; }); it('should handle a message in the connected state as a normal message', function () { - client._normalMsg = sinon.spy(); + sinon.spy(client, "_normalMsg"); client._sock._websocket._receiveData(new Uint8Array([1, 2, 3])); expect(client._normalMsg).to.have.been.called; }); @@ -3896,7 +4461,7 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should handle a message in any non-disconnected/failed state like an init message', function () { client._rfbConnectionState = 'connecting'; client._rfbInitState = 'ProtocolVersion'; - client._initMsg = sinon.spy(); + sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([1, 2, 3])); expect(client._initMsg).to.have.been.called; }); @@ -4160,31 +4725,67 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('RFB messages', function () { let sock; - before(function () { - FakeWebSocket.replace(); + beforeEach(function () { + let websock = new FakeWebSocket(); + websock._open(); sock = new Websock(); - sock.open(); + sock.attach(websock); }); - after(function () { - FakeWebSocket.restore(); + describe('Input Events', function () { + it('should send correct data for keyboard events', function () { + // FIXME: down should be boolean + RFB.messages.keyEvent(sock, 0x12345678, 0); + let expected = + [ 4, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]; + expect(sock).to.have.sent(new Uint8Array(expected)); + + RFB.messages.keyEvent(sock, 0x90abcdef, 1); + expected = + [ 4, 1, 0, 0, 0x90, 0xab, 0xcd, 0xef]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should send correct data for QEMU keyboard events', function () { + // FIXME: down should be boolean + RFB.messages.QEMUExtendedKeyEvent(sock, 0x12345678, 0, 0x55); + let expected = + [ 255, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x55]; + expect(sock).to.have.sent(new Uint8Array(expected)); + + RFB.messages.QEMUExtendedKeyEvent(sock, 0x90abcdef, 1, 0xe055); + expected = + [ 255, 0, 0, 1, 0x90, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0xd5]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should send correct data for pointer events', function () { + RFB.messages.pointerEvent(sock, 12345, 54321, 0xab); + let expected = + [ 5, 0xab, 0x30, 0x39, 0xd4, 0x31]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Clipboard Events', function () { + it('should send correct data for clipboard events', function () { + RFB.messages.clientCutText(sock, new Uint8Array([ 0x01, 0x23, 0x45, 0x67 ])); + let expected = + [ 6, 0, 0, 0, 0x00, 0x00, 0x00, 0x04, + 0x01, 0x23, 0x45, 0x67 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); }); describe('Extended Clipboard Handling Send', function () { - beforeEach(function () { - sinon.spy(RFB.messages, 'clientCutText'); - }); - - afterEach(function () { - RFB.messages.clientCutText.restore(); - }); - it('should call clientCutText with correct Caps data', function () { let formats = { 0: 2, 2: 4121 }; - let expectedData = new Uint8Array([0x1F, 0x00, 0x00, 0x05, + let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xF4, + 0x1F, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x10, 0x19]); let actions = [ @@ -4196,26 +4797,30 @@ describe('RFB messages', function () { ]; RFB.messages.extendedClipboardCaps(sock, actions, formats); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); + + expect(sock).to.have.sent(expectedData); }); it('should call clientCutText with correct Request data', function () { let formats = new Uint8Array([0x01]); - let expectedData = new Uint8Array([0x02, 0x00, 0x00, 0x01]); + let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFC, + 0x02, 0x00, 0x00, 0x01]); RFB.messages.extendedClipboardRequest(sock, formats); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); + + expect(sock).to.have.sent(expectedData); }); it('should call clientCutText with correct Notify data', function () { let formats = new Uint8Array([0x01]); - let expectedData = new Uint8Array([0x08, 0x00, 0x00, 0x01]); + let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFC, + 0x08, 0x00, 0x00, 0x01]); RFB.messages.extendedClipboardNotify(sock, formats); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); + + expect(sock).to.have.sent(expectedData); }); it('should call clientCutText with correct Provide data', function () { @@ -4225,16 +4830,24 @@ describe('RFB messages', function () { let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data - let expectedData = new Uint8Array(4 + deflatedData.length); - expectedData[0] = 0x10; // The client capabilities - expectedData[1] = 0x00; // Reserved flags - expectedData[2] = 0x00; // Reserved flags - expectedData[3] = 0x01; // The formats client supports - expectedData.set(deflatedData, 4); + let expectedData = new Uint8Array(8 + 4 + deflatedData.length); + expectedData[0] = 0x06; // Message type + expectedData[1] = 0x00; + expectedData[2] = 0x00; + expectedData[3] = 0x00; + expectedData[4] = 0xFF; // Size + expectedData[5] = 0xFF; + expectedData[6] = 0xFF; + expectedData[7] = 256 - (4 + deflatedData.length); + expectedData[8] = 0x10; // The client capabilities + expectedData[9] = 0x00; // Reserved flags + expectedData[10] = 0x00; // Reserved flags + expectedData[11] = 0x01; // The formats client supports + expectedData.set(deflatedData, 12); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); + + expect(sock).to.have.sent(expectedData); }); @@ -4247,16 +4860,24 @@ describe('RFB messages', function () { let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data - let expectedData = new Uint8Array(4 + deflatedData.length); - expectedData[0] = 0x10; // The client capabilities - expectedData[1] = 0x00; // Reserved flags - expectedData[2] = 0x00; // Reserved flags - expectedData[3] = 0x01; // The formats client supports - expectedData.set(deflatedData, 4); + let expectedData = new Uint8Array(8 + 4 + deflatedData.length); + expectedData[0] = 0x06; // Message type + expectedData[1] = 0x00; + expectedData[2] = 0x00; + expectedData[3] = 0x00; + expectedData[4] = 0xFF; // Size + expectedData[5] = 0xFF; + expectedData[6] = 0xFF; + expectedData[7] = 256 - (4 + deflatedData.length); + expectedData[8] = 0x10; // The client capabilities + expectedData[9] = 0x00; // Reserved flags + expectedData[10] = 0x00; // Reserved flags + expectedData[11] = 0x01; // The formats client supports + expectedData.set(deflatedData, 12); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); + + expect(sock).to.have.sent(expectedData); }); it('Carriage return Line feed', function () { @@ -4267,16 +4888,24 @@ describe('RFB messages', function () { let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data - let expectedData = new Uint8Array(4 + deflatedData.length); - expectedData[0] = 0x10; // The client capabilities - expectedData[1] = 0x00; // Reserved flags - expectedData[2] = 0x00; // Reserved flags - expectedData[3] = 0x01; // The formats client supports - expectedData.set(deflatedData, 4); + let expectedData = new Uint8Array(8 + 4 + deflatedData.length); + expectedData[0] = 0x06; // Message type + expectedData[1] = 0x00; + expectedData[2] = 0x00; + expectedData[3] = 0x00; + expectedData[4] = 0xFF; // Size + expectedData[5] = 0xFF; + expectedData[6] = 0xFF; + expectedData[7] = 256 - (4 + deflatedData.length); + expectedData[8] = 0x10; // The client capabilities + expectedData[9] = 0x00; // Reserved flags + expectedData[10] = 0x00; // Reserved flags + expectedData[11] = 0x01; // The formats client supports + expectedData.set(deflatedData, 12); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); + + expect(sock).to.have.sent(expectedData); }); it('Line feed', function () { @@ -4286,16 +4915,24 @@ describe('RFB messages', function () { let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data - let expectedData = new Uint8Array(4 + deflatedData.length); - expectedData[0] = 0x10; // The client capabilities - expectedData[1] = 0x00; // Reserved flags - expectedData[2] = 0x00; // Reserved flags - expectedData[3] = 0x01; // The formats client supports - expectedData.set(deflatedData, 4); + let expectedData = new Uint8Array(8 + 4 + deflatedData.length); + expectedData[0] = 0x06; // Message type + expectedData[1] = 0x00; + expectedData[2] = 0x00; + expectedData[3] = 0x00; + expectedData[4] = 0xFF; // Size + expectedData[5] = 0xFF; + expectedData[6] = 0xFF; + expectedData[7] = 256 - (4 + deflatedData.length); + expectedData[8] = 0x10; // The client capabilities + expectedData[9] = 0x00; // Reserved flags + expectedData[10] = 0x00; // Reserved flags + expectedData[11] = 0x01; // The formats client supports + expectedData.set(deflatedData, 12); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); + + expect(sock).to.have.sent(expectedData); }); it('Carriage return and Line feed mixed', function () { @@ -4305,17 +4942,105 @@ describe('RFB messages', function () { let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data - let expectedData = new Uint8Array(4 + deflatedData.length); - expectedData[0] = 0x10; // The client capabilities - expectedData[1] = 0x00; // Reserved flags - expectedData[2] = 0x00; // Reserved flags - expectedData[3] = 0x01; // The formats client supports - expectedData.set(deflatedData, 4); + let expectedData = new Uint8Array(8 + 4 + deflatedData.length); + expectedData[0] = 0x06; // Message type + expectedData[1] = 0x00; + expectedData[2] = 0x00; + expectedData[3] = 0x00; + expectedData[4] = 0xFF; // Size + expectedData[5] = 0xFF; + expectedData[6] = 0xFF; + expectedData[7] = 256 - (4 + deflatedData.length); + expectedData[8] = 0x10; // The client capabilities + expectedData[9] = 0x00; // Reserved flags + expectedData[10] = 0x00; // Reserved flags + expectedData[11] = 0x01; // The formats client supports + expectedData.set(deflatedData, 12); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); - expect(RFB.messages.clientCutText).to.have.been.calledOnce; - expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); + + expect(sock).to.have.sent(expectedData); }); }); }); + + describe('Screen Layout', function () { + it('should send correct data for screen layout changes', function () { + RFB.messages.setDesktopSize(sock, 12345, 54321, 0x12345678, 0x90abcdef); + let expected = + [ 251, 0, 0x30, 0x39, 0xd4, 0x31, 0x01, 0x00, + 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, + 0x30, 0x39, 0xd4, 0x31, 0x90, 0xab, 0xcd, 0xef ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Fences', function () { + it('should send correct data for fences', function () { + // FIXME: Payload should be a byte array + RFB.messages.clientFence(sock, 0x12345678, "text"); + let expected = + [ 248, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, + 4, 0x74, 0x65, 0x78, 0x74 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Continuous Updates', function () { + it('should send correct data for continuous updates configuration', function () { + // FIXME: enable should be boolean + RFB.messages.enableContinuousUpdates(sock, 0, 12345, 54321, 34343, 18181); + let expected = + [ 150, 0, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Pixel Format', function () { + it('should send correct data for normal depth', function () { + RFB.messages.pixelFormat(sock, 24, true); + let expected = + [ 0, 0, 0, 0, 32, 24, 0, 1, + 0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should send correct data for low depth', function () { + RFB.messages.pixelFormat(sock, 8, true); + let expected = + [ 0, 0, 0, 0, 8, 8, 0, 1, + 0, 3, 0, 3, 0, 3, 0, 2, 4, 0, 0, 0 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Encodings', function () { + it('should send correct data for supported encodings', function () { + RFB.messages.clientEncodings(sock, [ 0x12345678, + 0x90abcdef, + 0x10293847 ]); + let expected = + [ 2, 0, 0, 3, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, + 0xef, 0x10, 0x29, 0x38, 0x47 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('Update request', function () { + it('should send correct data for update request', function () { + RFB.messages.fbUpdateRequest(sock, true, 12345, 54321, 34343, 18181); + let expected = + [ 3, 1, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('XVP operations', function () { + it('should send correct data for XVP operations', function () { + RFB.messages.xvpOp(sock, 123, 45); + let expected = + [ 250, 0, 123, 45 ]; + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); }); diff --git a/tests/test.rre.js b/tests/test.rre.js index 8e006f87..c55d7f39 100644 --- a/tests/test.rre.js +++ b/tests/test.rre.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } function push16(arr, num) { @@ -76,7 +79,7 @@ describe('RRE Decoder', function () { push16(data, 2); // width: 2 push16(data, 2); // height: 2 - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -85,6 +88,7 @@ describe('RRE Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -93,7 +97,10 @@ describe('RRE Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00, 0xff, 0xff, 0xff, 0xff ], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, + [ 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff ], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -102,6 +109,7 @@ describe('RRE Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); }); diff --git a/tests/test.tight.js b/tests/test.tight.js index cc5db36b..b3457a88 100644 --- a/tests/test.tight.js +++ b/tests/test.tight.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('Tight Decoder', function () { @@ -42,9 +45,9 @@ describe('Tight Decoder', function () { }); it('should handle fill rects', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x80, 0xff, 0x88, 0x44], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x80, 0xff, 0x88, 0x44], + display, 24); let targetData = new Uint8Array([ 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, @@ -53,21 +56,31 @@ describe('Tight Decoder', function () { 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle uncompressed copy rects', function () { + let done; let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ]; let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ]; - testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24); - testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24); - testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24); - testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24); - testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24); - testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24); - testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24); - testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24); + done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 2, 3, 2, 1, blueData, display, 24); + expect(done).to.be.true; let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -89,7 +102,7 @@ describe('Tight Decoder', function () { 0x60, 0x82, 0x01, 0x99, 0x8d, 0x29, 0x02, 0xa6, 0x00, 0x7e, 0xbf, 0x0f, 0xf1 ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -98,6 +111,7 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -110,7 +124,7 @@ describe('Tight Decoder', function () { // Pixels 0x30, 0x30, 0xc0, 0xc0 ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -119,6 +133,7 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -135,7 +150,7 @@ describe('Tight Decoder', function () { 0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00, 0x8a, 0x01, 0x21, 0x3c, 0x05, 0xa1 ]; - testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 12, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -152,10 +167,12 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle uncompressed palette rects', function () { + let done; let data1 = [ // Control bytes 0x40, 0x01, @@ -171,8 +188,10 @@ describe('Tight Decoder', function () { // Pixels 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ]; - testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24); - testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24); + done = testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24); + expect(done).to.be.true; + done = testDecodeRect(decoder, 0, 2, 4, 2, data2, display, 24); + expect(done).to.be.true; let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -196,7 +215,7 @@ describe('Tight Decoder', function () { 0x62, 0x08, 0xc9, 0xc0, 0x00, 0x00, 0x00, 0x54, 0x00, 0x09 ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -205,6 +224,7 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -221,7 +241,7 @@ describe('Tight Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00 ], display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -230,6 +250,7 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -238,10 +259,10 @@ describe('Tight Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, - [ 0x40, 0x01, 0x01, - 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff ], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, + [ 0x40, 0x01, 0x01, + 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff ], display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -250,6 +271,7 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); @@ -258,8 +280,9 @@ describe('Tight Decoder', function () { display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); - testDecodeRect(decoder, 1, 2, 0, 0, - [ 0x80, 0xff, 0xff, 0xff ], display, 24); + let done = testDecodeRect(decoder, 1, 2, 0, 0, + [ 0x80, 0xff, 0xff, 0xff ], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -268,10 +291,11 @@ describe('Tight Decoder', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); - it('should handle JPEG rects', function (done) { + it('should handle JPEG rects', async function () { let data = [ // Control bytes 0x90, 0xd6, 0x05, @@ -369,7 +393,8 @@ describe('Tight Decoder', function () { 0x3f, 0xeb, 0xff, 0x00, 0xff, 0xd9, ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + expect(decodeDone).to.be.true; let targetData = new Uint8Array([ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -385,10 +410,7 @@ describe('Tight Decoder', function () { return diff < 5; } - display.onflush = () => { - expect(display).to.have.displayed(targetData, almost); - done(); - }; - display.flush(); + await display.flush(); + expect(display).to.have.displayed(targetData, almost); }); }); diff --git a/tests/test.tightpng.js b/tests/test.tightpng.js index 253400b8..02c66d93 100644 --- a/tests/test.tightpng.js +++ b/tests/test.tightpng.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('TightPng Decoder', function () { @@ -41,7 +44,7 @@ describe('TightPng Decoder', function () { display.resize(4, 4); }); - it('should handle the TightPng encoding', function (done) { + it('should handle the TightPng encoding', async function () { let data = [ // Control bytes 0xa0, 0xb4, 0x04, @@ -119,7 +122,8 @@ describe('TightPng Decoder', function () { 0xae, 0x42, 0x60, 0x82, ]; - testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + let decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + expect(decodeDone).to.be.true; let targetData = new Uint8Array([ 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, @@ -135,10 +139,7 @@ describe('TightPng Decoder', function () { return diff < 30; } - display.onflush = () => { - expect(display).to.have.displayed(targetData, almost); - done(); - }; - display.flush(); + await display.flush(); + expect(display).to.have.displayed(targetData, almost); }); }); diff --git a/tests/test.websock.js b/tests/test.websock.js index 857fdca8..dc361b74 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -6,92 +6,64 @@ import FakeWebSocket from './fake.websocket.js'; describe('Websock', function () { "use strict"; - describe('Queue methods', function () { - let sock; - const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + describe('Receive queue methods', function () { + let sock, websock; beforeEach(function () { sock = new Websock(); - // skip init - sock._allocateBuffers(); - sock._rQ.set(RQ_TEMPLATE); - sock._rQlen = RQ_TEMPLATE.length; - }); - describe('rQlen', function () { - it('should return the length of the receive queue', function () { - sock.rQi = 0; - - expect(sock.rQlen).to.equal(RQ_TEMPLATE.length); - }); - - it("should return the proper length if we read some from the receive queue", function () { - sock.rQi = 1; - - expect(sock.rQlen).to.equal(RQ_TEMPLATE.length - 1); - }); + websock = new FakeWebSocket(); + websock._open(); + sock.attach(websock); }); describe('rQpeek8', function () { it('should peek at the next byte without poping it off the queue', function () { - const befLen = sock.rQlen; - const peek = sock.rQpeek8(); - expect(sock.rQpeek8()).to.equal(peek); - expect(sock.rQlen).to.equal(befLen); + websock._receiveData(new Uint8Array([0xab, 0xcd])); + expect(sock.rQpeek8()).to.equal(0xab); + expect(sock.rQpeek8()).to.equal(0xab); }); }); describe('rQshift8()', function () { it('should pop a single byte from the receive queue', function () { - const peek = sock.rQpeek8(); - const befLen = sock.rQlen; - expect(sock.rQshift8()).to.equal(peek); - expect(sock.rQlen).to.equal(befLen - 1); + websock._receiveData(new Uint8Array([0xab, 0xcd])); + expect(sock.rQshift8()).to.equal(0xab); + expect(sock.rQshift8()).to.equal(0xcd); }); }); describe('rQshift16()', function () { it('should pop two bytes from the receive queue and return a single number', function () { - const befLen = sock.rQlen; - const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; - expect(sock.rQshift16()).to.equal(expected); - expect(sock.rQlen).to.equal(befLen - 2); + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); + expect(sock.rQshift16()).to.equal(0xabcd); + expect(sock.rQshift16()).to.equal(0x1234); }); }); describe('rQshift32()', function () { it('should pop four bytes from the receive queue and return a single number', function () { - const befLen = sock.rQlen; - const expected = (RQ_TEMPLATE[0] << 24) + - (RQ_TEMPLATE[1] << 16) + - (RQ_TEMPLATE[2] << 8) + - RQ_TEMPLATE[3]; - expect(sock.rQshift32()).to.equal(expected); - expect(sock.rQlen).to.equal(befLen - 4); + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + expect(sock.rQshift32()).to.equal(0xabcd1234); + expect(sock.rQshift32()).to.equal(0x88ee1133); }); }); describe('rQshiftStr', function () { it('should shift the given number of bytes off of the receive queue and return a string', function () { - const befLen = sock.rQlen; - const befRQi = sock.rQi; - const shifted = sock.rQshiftStr(3); - expect(shifted).to.be.a('string'); - expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)))); - expect(sock.rQlen).to.equal(befLen - 3); - }); - - it('should shift the entire rest of the queue off if no length is given', function () { - sock.rQshiftStr(); - expect(sock.rQlen).to.equal(0); + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + expect(sock.rQshiftStr(4)).to.equal('\xab\xcd\x12\x34'); + expect(sock.rQshiftStr(4)).to.equal('\x88\xee\x11\x33'); }); it('should be able to handle very large strings', function () { const BIG_LEN = 500000; - const RQ_BIG = new Uint8Array(BIG_LEN); + const incoming = new Uint8Array(BIG_LEN); let expected = ""; let letterCode = 'a'.charCodeAt(0); for (let i = 0; i < BIG_LEN; i++) { - RQ_BIG[i] = letterCode; + incoming[i] = letterCode; expected += String.fromCharCode(letterCode); if (letterCode < 'z'.charCodeAt(0)) { @@ -100,146 +72,282 @@ describe('Websock', function () { letterCode = 'a'.charCodeAt(0); } } - sock._rQ.set(RQ_BIG); - sock._rQlen = RQ_BIG.length; + websock._receiveData(incoming); - const shifted = sock.rQshiftStr(); + const shifted = sock.rQshiftStr(BIG_LEN); expect(shifted).to.be.equal(expected); - expect(sock.rQlen).to.equal(0); }); }); describe('rQshiftBytes', function () { it('should shift the given number of bytes of the receive queue and return an array', function () { - const befLen = sock.rQlen; - const befRQi = sock.rQi; - const shifted = sock.rQshiftBytes(3); - expect(shifted).to.be.an.instanceof(Uint8Array); - expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)); - expect(sock.rQlen).to.equal(befLen - 3); + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); + expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0x88, 0xee, 0x11, 0x33])); }); - it('should shift the entire rest of the queue off if no length is given', function () { - sock.rQshiftBytes(); - expect(sock.rQlen).to.equal(0); + it('should return a shared array if requested', function () { + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + const bytes = sock.rQshiftBytes(4, false); + expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); + expect(bytes.buffer.byteLength).to.not.equal(bytes.length); }); }); - describe('rQslice', function () { - beforeEach(function () { - sock.rQi = 0; - }); - + describe('rQpeekBytes', function () { it('should not modify the receive queue', function () { - const befLen = sock.rQlen; - sock.rQslice(0, 2); - expect(sock.rQlen).to.equal(befLen); + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); + expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); }); - it('should return an array containing the given slice of the receive queue', function () { - const sl = sock.rQslice(0, 2); - expect(sl).to.be.an.instanceof(Uint8Array); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); - }); - - it('should use the rest of the receive queue if no end is given', function () { - const sl = sock.rQslice(1); - expect(sl).to.have.length(RQ_TEMPLATE.length - 1); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1)); - }); - - it('should take the current rQi in to account', function () { - sock.rQi = 1; - expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); + it('should return a shared array if requested', function () { + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); + const bytes = sock.rQpeekBytes(4, false); + expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34])); + expect(bytes.buffer.byteLength).to.not.equal(bytes.length); }); }); describe('rQwait', function () { beforeEach(function () { - sock.rQi = 0; + websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34, + 0x88, 0xee, 0x11, 0x33])); }); it('should return true if there are not enough bytes in the receive queue', function () { - expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true; + expect(sock.rQwait('hi', 9)).to.be.true; }); it('should return false if there are enough bytes in the receive queue', function () { - expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false; + expect(sock.rQwait('hi', 8)).to.be.false; }); it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { - sock.rQi = 5; - expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; - expect(sock.rQi).to.equal(1); + expect(sock.rQshift32()).to.equal(0xabcd1234); + expect(sock.rQwait('hi', 8, 2)).to.be.true; + expect(sock.rQshift32()).to.equal(0x123488ee); }); it('should raise an error if we try to go back more than possible', function () { - sock.rQi = 5; - expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); + expect(sock.rQshift32()).to.equal(0xabcd1234); + expect(() => sock.rQwait('hi', 8, 6)).to.throw(Error); }); it('should not reduce rQi if there are enough bytes', function () { - sock.rQi = 5; - sock.rQwait('hi', 1, 6); - expect(sock.rQi).to.equal(5); + expect(sock.rQshift32()).to.equal(0xabcd1234); + expect(sock.rQwait('hi', 4, 2)).to.be.false; + expect(sock.rQshift32()).to.equal(0x88ee1133); + }); + }); + }); + + describe('Send queue methods', function () { + let sock; + + const bufferSize = 10 * 1024; + + beforeEach(function () { + let websock = new FakeWebSocket(); + websock._open(); + sock = new Websock(); + sock.attach(websock); + }); + + describe('sQpush8()', function () { + it('should send a single byte', function () { + sock.sQpush8(42); + sock.flush(); + expect(sock).to.have.sent(new Uint8Array([42])); + }); + it('should not send any data until flushing', function () { + sock.sQpush8(42); + expect(sock).to.have.sent(new Uint8Array([])); + }); + it('should implicitly flush if the queue is full', function () { + for (let i = 0;i <= bufferSize;i++) { + sock.sQpush8(42); + } + + let expected = []; + for (let i = 0;i < bufferSize;i++) { + expected.push(42); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('sQpush16()', function () { + it('should send a number as two bytes', function () { + sock.sQpush16(420); + sock.flush(); + expect(sock).to.have.sent(new Uint8Array([1, 164])); + }); + it('should not send any data until flushing', function () { + sock.sQpush16(420); + expect(sock).to.have.sent(new Uint8Array([])); + }); + it('should implicitly flush if the queue is full', function () { + for (let i = 0;i <= bufferSize/2;i++) { + sock.sQpush16(420); + } + + let expected = []; + for (let i = 0;i < bufferSize/2;i++) { + expected.push(1); + expected.push(164); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('sQpush32()', function () { + it('should send a number as two bytes', function () { + sock.sQpush32(420420); + sock.flush(); + expect(sock).to.have.sent(new Uint8Array([0, 6, 106, 68])); + }); + it('should not send any data until flushing', function () { + sock.sQpush32(420420); + expect(sock).to.have.sent(new Uint8Array([])); + }); + it('should implicitly flush if the queue is full', function () { + for (let i = 0;i <= bufferSize/4;i++) { + sock.sQpush32(420420); + } + + let expected = []; + for (let i = 0;i < bufferSize/4;i++) { + expected.push(0); + expected.push(6); + expected.push(106); + expected.push(68); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('sQpushString()', function () { + it('should send a string buffer', function () { + sock.sQpushString('\x12\x34\x56\x78\x90'); + sock.flush(); + expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90])); + }); + it('should not send any data until flushing', function () { + sock.sQpushString('\x12\x34\x56\x78\x90'); + expect(sock).to.have.sent(new Uint8Array([])); + }); + it('should implicitly flush if the queue is full', function () { + for (let i = 0;i <= bufferSize/5;i++) { + sock.sQpushString('\x12\x34\x56\x78\x90'); + } + + let expected = []; + for (let i = 0;i < bufferSize/5;i++) { + expected.push(0x12); + expected.push(0x34); + expected.push(0x56); + expected.push(0x78); + expected.push(0x90); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + it('should implicitly split a large buffer', function () { + let str = ''; + for (let i = 0;i <= bufferSize/5;i++) { + str += '\x12\x34\x56\x78\x90'; + } + + sock.sQpushString(str); + + let expected = []; + for (let i = 0;i < bufferSize/5;i++) { + expected.push(0x12); + expected.push(0x34); + expected.push(0x56); + expected.push(0x78); + expected.push(0x90); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + }); + + describe('sQpushBytes()', function () { + it('should send a byte buffer', function () { + sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90])); + sock.flush(); + expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90])); + }); + it('should not send any data until flushing', function () { + sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90])); + expect(sock).to.have.sent(new Uint8Array([])); + }); + it('should implicitly flush if the queue is full', function () { + for (let i = 0;i <= bufferSize/5;i++) { + sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90])); + } + + let expected = []; + for (let i = 0;i < bufferSize/5;i++) { + expected.push(0x12); + expected.push(0x34); + expected.push(0x56); + expected.push(0x78); + expected.push(0x90); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); + }); + it('should implicitly split a large buffer', function () { + let buffer = []; + for (let i = 0;i <= bufferSize/5;i++) { + buffer.push(0x12); + buffer.push(0x34); + buffer.push(0x56); + buffer.push(0x78); + buffer.push(0x90); + } + + sock.sQpushBytes(new Uint8Array(buffer)); + + let expected = []; + for (let i = 0;i < bufferSize/5;i++) { + expected.push(0x12); + expected.push(0x34); + expected.push(0x56); + expected.push(0x78); + expected.push(0x90); + } + + expect(sock).to.have.sent(new Uint8Array(expected)); }); }); describe('flush', function () { - beforeEach(function () { - sock._websocket = { - send: sinon.spy() - }; - }); - it('should actually send on the websocket', function () { - sock._websocket.bufferedAmount = 8; - sock._websocket.readyState = WebSocket.OPEN; sock._sQ = new Uint8Array([1, 2, 3]); sock._sQlen = 3; - const encoded = sock._encodeMessage(); sock.flush(); - expect(sock._websocket.send).to.have.been.calledOnce; - expect(sock._websocket.send).to.have.been.calledWith(encoded); + expect(sock).to.have.sent(new Uint8Array([1, 2, 3])); }); it('should not call send if we do not have anything queued up', function () { sock._sQlen = 0; - sock._websocket.bufferedAmount = 8; sock.flush(); - expect(sock._websocket.send).not.to.have.been.called; - }); - }); - - describe('send', function () { - beforeEach(function () { - sock.flush = sinon.spy(); - }); - - it('should add to the send queue', function () { - sock.send([1, 2, 3]); - const sq = sock.sQ; - expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); - }); - - it('should call flush', function () { - sock.send([1, 2, 3]); - expect(sock.flush).to.have.been.calledOnce; - }); - }); - - describe('sendString', function () { - beforeEach(function () { - sock.send = sinon.spy(); - }); - - it('should call send after converting the string to an array', function () { - sock.sendString("\x01\x02\x03"); - expect(sock.send).to.have.been.calledWith([1, 2, 3]); + expect(sock).to.have.sent(new Uint8Array([])); }); }); }); @@ -450,9 +558,8 @@ describe('Websock', function () { sock._allocateBuffers(); }); - it('should support adding binary Uint8Array data to the receive queue', function () { + it('should support adding data to the receive queue', function () { const msg = { data: new Uint8Array([1, 2, 3]) }; - sock._mode = 'binary'; sock._recvMessage(msg); expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); }); @@ -473,80 +580,49 @@ describe('Websock', function () { expect(sock._eventHandlers.message).not.to.have.been.called; }); - it('should compact the receive queue when a message handler empties it', function () { - sock._eventHandlers.message = () => { sock.rQi = sock._rQlen; }; + it('should compact the receive queue when fully read', function () { sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); sock._rQlen = 6; - sock.rQi = 6; + sock._rQi = 6; const msg = { data: new Uint8Array([1, 2, 3]).buffer }; - sock._mode = 'binary'; sock._recvMessage(msg); - expect(sock._rQlen).to.equal(0); - expect(sock.rQi).to.equal(0); + expect(sock._rQlen).to.equal(3); + expect(sock._rQi).to.equal(0); }); it('should compact the receive queue when we reach the end of the buffer', function () { sock._rQ = new Uint8Array(20); sock._rQbufferSize = 20; sock._rQlen = 20; - sock.rQi = 10; + sock._rQi = 10; const msg = { data: new Uint8Array([1, 2]).buffer }; - sock._mode = 'binary'; sock._recvMessage(msg); expect(sock._rQlen).to.equal(12); - expect(sock.rQi).to.equal(0); + expect(sock._rQi).to.equal(0); }); it('should automatically resize the receive queue if the incoming message is larger than the buffer', function () { sock._rQ = new Uint8Array(20); sock._rQlen = 0; - sock.rQi = 0; + sock._rQi = 0; sock._rQbufferSize = 20; const msg = { data: new Uint8Array(30).buffer }; - sock._mode = 'binary'; sock._recvMessage(msg); expect(sock._rQlen).to.equal(30); - expect(sock.rQi).to.equal(0); + expect(sock._rQi).to.equal(0); expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen }); it('should automatically resize the receive queue if the incoming message is larger than 1/8th of the buffer and we reach the end of the buffer', function () { sock._rQ = new Uint8Array(20); sock._rQlen = 16; - sock.rQi = 16; + sock._rQi = 15; sock._rQbufferSize = 20; const msg = { data: new Uint8Array(6).buffer }; - sock._mode = 'binary'; sock._recvMessage(msg); - expect(sock._rQlen).to.equal(6); - expect(sock.rQi).to.equal(0); - expect(sock._rQ.length).to.equal(48); - }); - }); - - describe('Data encoding', function () { - before(function () { FakeWebSocket.replace(); }); - after(function () { FakeWebSocket.restore(); }); - - describe('as binary data', function () { - let sock; - beforeEach(function () { - sock = new Websock(); - sock.open('ws://', 'binary'); - sock._websocket._open(); - }); - - it('should only send the send queue up to the send queue length', function () { - sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); - sock._sQlen = 3; - const res = sock._encodeMessage(); - expect(res).to.array.equal(new Uint8Array([1, 2, 3])); - }); - - it('should properly pass the encoded data off to the actual WebSocket', function () { - sock.send([1, 2, 3]); - expect(sock._websocket._getSentData()).to.array.equal(new Uint8Array([1, 2, 3])); - }); + expect(sock._rQlen).to.equal(7); + expect(sock._rQi).to.equal(0); + expect(sock._rQ.length).to.equal(56); }); }); }); diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 76aa763a..df8227ae 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -8,39 +8,48 @@ describe('WebUtil', function () { "use strict"; describe('config variables', function () { + let origState, origHref; + beforeEach(function () { + origState = history.state; + origHref = location.href; + }); + afterEach(function () { + history.replaceState(origState, '', origHref); + }); + it('should parse query string variables', function () { // history.pushState() will not cause the browser to attempt loading // the URL, this is exactly what we want here for the tests. - history.pushState({}, '', "test?myvar=myval"); + history.replaceState({}, '', "test?myvar=myval"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval"); }); it('should return default value when no query match', function () { - history.pushState({}, '', "test?myvar=myval"); + history.replaceState({}, '', "test?myvar=myval"); expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def"); }); it('should handle no query match and no default value', function () { - history.pushState({}, '', "test?myvar=myval"); + history.replaceState({}, '', "test?myvar=myval"); expect(WebUtil.getConfigVar("other")).to.be.equal(null); }); it('should parse fragment variables', function () { - history.pushState({}, '', "test#myvar=myval"); + history.replaceState({}, '', "test#myvar=myval"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval"); }); it('should return default value when no fragment match', function () { - history.pushState({}, '', "test#myvar=myval"); + history.replaceState({}, '', "test#myvar=myval"); expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def"); }); it('should handle no fragment match and no default value', function () { - history.pushState({}, '', "test#myvar=myval"); + history.replaceState({}, '', "test#myvar=myval"); expect(WebUtil.getConfigVar("other")).to.be.equal(null); }); it('should handle both query and fragment', function () { - history.pushState({}, '', "test?myquery=1#myhash=2"); + history.replaceState({}, '', "test?myquery=1#myhash=2"); expect(WebUtil.getConfigVar("myquery")).to.be.equal("1"); expect(WebUtil.getConfigVar("myhash")).to.be.equal("2"); }); it('should prioritize fragment if both provide same var', function () { - history.pushState({}, '', "test?myvar=1#myvar=2"); + history.replaceState({}, '', "test?myvar=1#myvar=2"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("2"); }); }); @@ -83,6 +92,11 @@ describe('WebUtil', function () { expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); expect(WebUtil.readSetting('test')).to.equal('value'); }); + + it('should not crash when local storage save fails', function () { + localStorage.setItem.throws(new DOMException()); + expect(WebUtil.writeSetting('test', 'value')).to.not.throw; + }); }); describe('setSetting', function () { @@ -128,6 +142,11 @@ describe('WebUtil', function () { WebUtil.writeSetting('test', 'something else'); expect(WebUtil.readSetting('test')).to.equal('something else'); }); + + it('should not crash when local storage read fails', function () { + localStorage.getItem.throws(new DOMException()); + expect(WebUtil.readSetting('test')).to.not.throw; + }); }); // this doesn't appear to be used anywhere @@ -136,6 +155,11 @@ describe('WebUtil', function () { WebUtil.eraseSetting('test'); expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test'); }); + + it('should not crash when local storage remove fails', function () { + localStorage.removeItem.throws(new DOMException()); + expect(WebUtil.eraseSetting('test')).to.not.throw; + }); }); }); diff --git a/tests/test.zrle.js b/tests/test.zrle.js index e09d208d..be046409 100644 --- a/tests/test.zrle.js +++ b/tests/test.zrle.js @@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js'; function testDecodeRect(decoder, x, y, width, height, data, display, depth) { let sock; + let done = false; sock = new Websock; sock.open("ws://example.com"); sock.on('message', () => { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); }); // Empty messages are filtered at multiple layers, so we need to // do a direct call if (data.length === 0) { - decoder.decodeRect(x, y, width, height, sock, display, depth); + done = decoder.decodeRect(x, y, width, height, sock, display, depth); } else { sock._websocket._receiveData(new Uint8Array(data)); } display.flip(); + + return done; } describe('ZRLE Decoder', function () { @@ -42,9 +45,11 @@ describe('ZRLE Decoder', function () { }); it('should handle the Raw subencoding', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e, 0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e, + 0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12, + 0x02, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, @@ -53,13 +58,16 @@ describe('ZRLE Decoder', function () { 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle the Solid subencoding', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, + 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, + 0x00, 0x00, 0xff, 0xff], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, @@ -68,14 +76,18 @@ describe('ZRLE Decoder', function () { 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle the Palette Tile subencoding', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E, 0x62, 0x62, 0x60, 248, 0xff, 0x9F, 0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E, + 0x62, 0x62, 0x60, 248, 0xff, 0x9F, + 0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00, + 0x00, 0x00, 0xff, 0xff], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, @@ -84,13 +96,16 @@ describe('ZRLE Decoder', function () { 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle the RLE Tile subencoding', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e, 0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e, + 0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00, + 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, @@ -99,13 +114,17 @@ describe('ZRLE Decoder', function () { 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); it('should handle the RLE Palette Tile subencoding', function () { - testDecodeRect(decoder, 0, 0, 4, 4, - [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e, 0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f, 0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00, 0x00, 0xff, 0xff], - display, 24); + let done = testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e, + 0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f, + 0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00, + 0x00, 0xff, 0xff], + display, 24); let targetData = new Uint8Array([ 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, @@ -114,6 +133,7 @@ describe('ZRLE Decoder', function () { 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff ]); + expect(done).to.be.true; expect(display).to.have.displayed(targetData); }); diff --git a/vnc_lite.html b/vnc_lite.html index e725a2d9..eaf75f86 100644 --- a/vnc_lite.html +++ b/vnc_lite.html @@ -107,20 +107,13 @@ // query string. If the variable isn't defined in the URL // it returns the default value instead. function readQueryVariable(name, defaultValue) { - // A URL with a query parameter can look like this (But will most probably get logged on the http server): + // A URL with a query parameter can look like this: // https://www.example.com?myqueryparam=myvalue // - // For privacy (Using a hastag #, the parameters will not be sent to the server) - // the url can be requested in the following way: - // https://www.example.com#myqueryparam=myvalue&password=secreatvalue - // - // Even Mixing public and non public parameters will work: - // https://www.example.com?nonsecretparam=example.com#password=secreatvalue - // // Note that we use location.href instead of location.search // because Firefox < 53 has a bug w.r.t location.search const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = ''.concat(document.location.href, window.location.hash).match(re); + match = document.location.href.match(re); if (match) { // We have to decode the URL since want the cleartext value