From 5b7d2a622ea5441ae750ea4d3e5cf56ee5737ac5 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 2 Jan 2023 14:46:33 +0100 Subject: [PATCH 01/65] Fix positioning of checkbox checkmark Changing the ::after element to be displayed as 'block' lets it be positioned using relative. This means we can remove the confusing "position: relative" from the checkbox. --- app/styles/input.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index c5897ab3c..eaf083c7e 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -86,13 +86,13 @@ option { * Checkboxes */ input[type=checkbox] { - position: relative; 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,7 +103,8 @@ input[type=checkbox]:checked { } input[type=checkbox]:checked::after { content: ""; - position: absolute; + display: block; /* width & height doesn't work on inline elements */ + position: relative; top: 0; left: 3px; width: 3px; From f974b7313762f791ca8376def9525100258ebf46 Mon Sep 17 00:00:00 2001 From: pdlan Date: Fri, 20 Jan 2023 05:54:00 -0500 Subject: [PATCH 02/65] Cleanup for the cryptographic algorithms that are not supported by SubtleCrypto --- core/crypto/aes.js | 178 +++++++++++++++++++ core/crypto/bigint.js | 34 ++++ core/crypto/crypto.js | 90 ++++++++++ core/{ => crypto}/des.js | 72 +++++++- core/crypto/dh.js | 55 ++++++ core/{util => crypto}/md5.js | 13 +- core/crypto/rsa.js | 132 ++++++++++++++ core/ra2.js | 327 ++++------------------------------- core/rfb.js | 122 ++++--------- tests/test.rfb.js | 53 +++--- 10 files changed, 663 insertions(+), 413 deletions(-) create mode 100644 core/crypto/aes.js create mode 100644 core/crypto/bigint.js create mode 100644 core/crypto/crypto.js rename core/{ => crypto}/des.js (87%) create mode 100644 core/crypto/dh.js rename core/{util => crypto}/md5.js (94%) create mode 100644 core/crypto/rsa.js diff --git a/core/crypto/aes.js b/core/crypto/aes.js new file mode 100644 index 000000000..e6aaea7c2 --- /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 000000000..d34432650 --- /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 000000000..cc17da228 --- /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 ba1ebde01..8dab31fb4 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; } +} + +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; + } - // 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))); + _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 000000000..bd705d9bf --- /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 49762ef9e..fcfefff06 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 000000000..68e8e869f --- /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/ra2.js b/core/ra2.js index 81a8a8952..647aea2f1 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(); @@ -417,8 +150,8 @@ 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); @@ -433,10 +166,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { // 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; @@ -449,7 +186,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { // 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; @@ -462,7 +200,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"); } @@ -500,7 +239,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { 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,7 +255,7 @@ 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"); } @@ -564,4 +303,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 6afd7c659..e573cd435 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"; @@ -1681,77 +1680,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) { - - 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(''); - - 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; - } - 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); + const credentials = window.crypto.getRandomValues(new Uint8Array(128)); + for (let i = 0; i < username.length; i++) { + credentials[i] = username.charCodeAt(i); } - let encrypted = new Uint8Array(data.length); - for (let i=0;i { + }) + .then(() => { this.dispatchEvent(new CustomEvent('securityresult')); this._rfbInitState = "SecurityResult"; return true; @@ -1934,15 +1892,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,22 +1911,8 @@ 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); - } + usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes); + passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes); this._sock.send(B); this._sock.send(usernameBytes); this._sock.send(passwordBytes); @@ -2937,7 +2881,9 @@ 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); } } diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 2da381846..4262ee63a 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'; @@ -1270,6 +1271,19 @@ describe('Remote Frame Buffer Protocol Client', function () { }); 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); @@ -1298,35 +1312,30 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._sock).to.have.sent([30]); - function byteArray(length) { - return Array.from(new Uint8Array(length).keys()); - } - - let generator = [127, 255]; - let prime = byteArray(128); - let serverPrivateKey = byteArray(128); - let serverPublicKey = client._modPow(generator, serverPrivateKey, prime); - - let clientPrivateKey = byteArray(128); - let clientPublicKey = client._modPow(generator, clientPrivateKey, prime); - - let padding = Array.from(byteArray(64), byte => String.fromCharCode(65+byte%26)).join(''); + 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); - await client._negotiateARDAuthAsync(generator, 128, prime, serverPublicKey, clientPrivateKey, padding); + await client._negotiateARDAuthAsync(128, serverPublicKey, clientKey); client._negotiateARDAuth(); 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); From 823e7cfca3110e192b14a3760361e316aa82e06c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 20 Jan 2023 13:56:16 +0100 Subject: [PATCH 03/65] Update Swedish translation --- po/sv.po | 189 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 114 insertions(+), 75 deletions(-) diff --git a/po/sv.po b/po/sv.po index 0f0e90b5e..972e40004 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" From 51677f5c70e10aede60c28ff27ce1cf9526172ab Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 20 Jan 2023 13:57:20 +0100 Subject: [PATCH 04/65] Update json files for new translations --- app/locale/fr.json | 48 ++++++++++++++++++++++++++-------------------- app/locale/sv.json | 12 ++++++++++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/app/locale/fr.json b/app/locale/fr.json index 19e8255b3..22531f73b 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 e46df45b5..077ef42c8 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", From 90455eef0692d2e35276fd31286114d0955016b0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 20 Jan 2023 13:58:48 +0100 Subject: [PATCH 05/65] noVNC 1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a1f0b7945..5847887f1 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": { From 9985950bfa7bd712b957be8d3358049b54cd534d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 26 Jan 2023 10:45:26 +0100 Subject: [PATCH 06/65] Upgrade to latest websockify in snap package --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 242442436..02094820d 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 From 6751cc1236699bb648ac0dd24fb0a53a002637c3 Mon Sep 17 00:00:00 2001 From: NNN1590 Date: Tue, 21 Mar 2023 13:15:26 +0900 Subject: [PATCH 07/65] Update Japanese translation --- po/ja.po | 199 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 119 insertions(+), 80 deletions(-) diff --git a/po/ja.po b/po/ja.po index a9b3dcd1b..64da73237 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 "パスワードが必要です" From 4558104196834963cea5748e4bed1750fbc4f6e2 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 10 Jan 2023 16:17:08 +0100 Subject: [PATCH 08/65] Properly center the checkbox checkmark Using a flexbox we can easily center the checkmark without using hard coded positions. --- app/styles/input.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/styles/input.css b/app/styles/input.css index eaf083c7e..dc345aabc 100644 --- a/app/styles/input.css +++ b/app/styles/input.css @@ -86,6 +86,9 @@ option { * Checkboxes */ input[type=checkbox] { + display: inline-flex; + justify-content: center; + align-items: center; background-color: white; background-image: unset; border: 1px solid dimgrey; @@ -104,14 +107,11 @@ input[type=checkbox]:checked { input[type=checkbox]:checked::after { content: ""; display: block; /* width & height doesn't work on inline elements */ - position: relative; - top: 0; - left: 3px; width: 3px; height: 7px; border: 1px solid white; border-width: 0 2px 2px 0; - transform: rotate(40deg); + transform: rotate(40deg) translateY(-1px); } /* From 8decca7353a0a730226842aa33c9018ac5e6e44c Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 23 Mar 2023 11:02:28 +0100 Subject: [PATCH 09/65] Use unitless numbers for line-height values Using type values for line-height can give unexpected inheritance behaviors. If using values, the inherited line-height on children is calculated using the font-size of the parent. What we want is for the line-height of children to be calculated using it's own font-size. By instead using a unitless number, we get the behavior we want. Note that this bug has no effects right now since no children to any of the related elements have different font-sizes. --- app/styles/base.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 06e736a95..f83ad4b93 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{ From 9a06058f66edb31f3d942e21b67319f243228330 Mon Sep 17 00:00:00 2001 From: Mathis Marcotte Date: Mon, 27 Mar 2023 14:23:09 +0000 Subject: [PATCH 10/65] Added english to list of supported languages --- app/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index c1f6776ed..07e090435 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1762,7 +1762,7 @@ 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"]; +const LINGUAS = ["cs", "de", "el", "en", "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(); From c1d2449fb86f162c78abc6146bf001d380fd8ba4 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 4 Apr 2023 17:02:21 +0200 Subject: [PATCH 11/65] Fix playback error message for load failure We expect the promise to be rejected with a string for display to the user. --- tests/playback-ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/playback-ui.js b/tests/playback-ui.js index d76adb4fa..6d71714ba 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; }); From 747603c0d5bbdc8ac31b81f7a1b31291a397d280 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 4 Apr 2023 17:02:57 +0200 Subject: [PATCH 12/65] Also re-enable playback on failures Allows easier testing by being able to run the test multiple times. --- tests/playback-ui.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/playback-ui.js b/tests/playback-ui.js index 6d71714ba..b1f263ea8 100644 --- a/tests/playback-ui.js +++ b/tests/playback-ui.js @@ -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) => { From a4453c9a260b6c4003c0ebd0aa460f61b93a499a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 5 Apr 2023 12:46:17 +0200 Subject: [PATCH 13/65] Special case English translation fallback We should not be listing this in LINGUAS as that gives the impression that English has en explicit translation. Instead, it is a special case that the code needs to be explicitly aware of. This reverts 9a06058 in favour of a more robust fix. --- app/localization.js | 13 ++++++------- app/ui.js | 2 +- tests/test.localization.js | 10 ++++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/localization.js b/app/localization.js index 84341da6f..73f66c51d 100644 --- a/app/localization.js +++ b/app/localization.js @@ -40,12 +40,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 +58,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() diff --git a/app/ui.js b/app/ui.js index 07e090435..c1f6776ed 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1762,7 +1762,7 @@ const UI = { }; // Set up translations -const LINGUAS = ["cs", "de", "el", "en", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"]; +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(); diff --git a/tests/test.localization.js b/tests/test.localization.js index 7e8e6c13e..36e8d0455 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -27,6 +27,16 @@ describe('Localization', function () { l10n.setup(["es", "fr"]); expect(l10n.language).to.equal('en'); }); + it('should fall back to generic English for other English', function () { + window.navigator.languages = ["en-AU", "de"]; + l10n.setup(["de", "fr", "en-GB"]); + expect(l10n.language).to.equal('en'); + }); + it('should prefer specific English over generic', function () { + window.navigator.languages = ["en-GB", "de"]; + l10n.setup(["de", "en-AU", "en-GB"]); + expect(l10n.language).to.equal('en-GB'); + }); it('should use the most preferred user language', function () { window.navigator.languages = ["nl", "de", "fr"]; l10n.setup(["es", "fr", "de"]); From 681632bc9fc38b4e4f0f9bf4a6b7d675fb991e70 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 6 Apr 2023 11:06:06 +0200 Subject: [PATCH 14/65] Avoid running tests on l10n singleton We want tests to be independent, so we cannot have them modify a shared state, such as the l10n singleton. Make sure each test instantiates its own object instead. --- tests/test.localization.js | 61 +++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/tests/test.localization.js b/tests/test.localization.js index 36e8d0455..1db2cb9bf 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -1,9 +1,20 @@ const expect = chai.expect; -import { l10n } from '../app/localization.js'; +import _, { Localizer, l10n } from '../app/localization.js'; describe('Localization', function () { "use strict"; + describe('Singleton', function () { + it('should export a singleton object', function () { + expect(l10n).to.be.instanceOf(Localizer); + }); + it('should export a singleton translation function', function () { + // FIXME: Can we use some spy instead? + l10n.dictionary = { "Foobar": "gazonk" }; + expect(_("Foobar")).to.equal("gazonk"); + }); + }); + describe('language selection', function () { let origNavigator; beforeEach(function () { @@ -20,52 +31,62 @@ describe('Localization', function () { }); 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 () { window.navigator.languages = ["nl", "de"]; - l10n.setup(["es", "fr"]); - expect(l10n.language).to.equal('en'); + let lclz = new Localizer(); + lclz.setup(["es", "fr"]); + expect(lclz.language).to.equal('en'); }); it('should fall back to generic English for other English', function () { window.navigator.languages = ["en-AU", "de"]; - l10n.setup(["de", "fr", "en-GB"]); - expect(l10n.language).to.equal('en'); + let lclz = new Localizer(); + lclz.setup(["de", "fr", "en-GB"]); + expect(lclz.language).to.equal('en'); }); it('should prefer specific English over generic', function () { window.navigator.languages = ["en-GB", "de"]; - l10n.setup(["de", "en-AU", "en-GB"]); - expect(l10n.language).to.equal('en-GB'); + let lclz = new Localizer(); + lclz.setup(["de", "en-AU", "en-GB"]); + expect(lclz.language).to.equal('en-GB'); }); it('should use the most preferred user language', function () { window.navigator.languages = ["nl", "de", "fr"]; - l10n.setup(["es", "fr", "de"]); - expect(l10n.language).to.equal('de'); + let lclz = new Localizer(); + lclz.setup(["es", "fr", "de"]); + expect(lclz.language).to.equal('de'); }); it('should prefer sub-languages languages', function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt", "pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); + let lclz = new Localizer(); + lclz.setup(["pt", "pt-BR"]); + expect(lclz.language).to.equal('pt-BR'); }); it('should fall back to language "parents"', function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["fr", "pt", "de"]); - expect(l10n.language).to.equal('pt'); + let lclz = new Localizer(); + 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 () { window.navigator.languages = ["pt", "de"]; - l10n.setup(["fr", "pt-BR", "de"]); - expect(l10n.language).to.equal('de'); + let lclz = new Localizer(); + lclz.setup(["fr", "pt-BR", "de"]); + expect(lclz.language).to.equal('de'); }); it('should handle underscore as a separator', function () { window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt_BR"]); - expect(l10n.language).to.equal('pt_BR'); + let lclz = new Localizer(); + lclz.setup(["pt_BR"]); + expect(lclz.language).to.equal('pt_BR'); }); it('should handle difference in case', function () { window.navigator.languages = ["pt-br"]; - l10n.setup(["pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); + let lclz = new Localizer(); + lclz.setup(["pt-BR"]); + expect(lclz.language).to.equal('pt-BR'); }); }); }); From b16f19f9ceb464a39735b8a2c799a1d7c3212c0c Mon Sep 17 00:00:00 2001 From: Lew Ayotte Date: Thu, 6 Apr 2023 15:00:21 -0500 Subject: [PATCH 15/65] Set _rfbVeNCryptState = 4 not == 4 --- core/rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/rfb.js b/core/rfb.js index e573cd435..d8e2de74b 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1593,7 +1593,7 @@ export default class RFB extends EventTargetMixin { this._rfbAuthScheme >> 8, this._rfbAuthScheme]); - this._rfbVeNCryptState == 4; + this._rfbVeNCryptState = 4; return true; } } From 2a675b33948f2b07f84321146fe5bbd9780e024b Mon Sep 17 00:00:00 2001 From: Ryo Ota Date: Sun, 30 Apr 2023 02:04:00 +0900 Subject: [PATCH 16/65] fix typo --- core/websock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/websock.js b/core/websock.js index 37b33fcc5..e07e31ba6 100644 --- a/core/websock.js +++ b/core/websock.js @@ -293,7 +293,7 @@ export default class Websock { // 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 From 2a21bee245aee7827eb00925307d2e107b6db646 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 6 Apr 2023 17:11:47 +0200 Subject: [PATCH 17/65] Revert broken Add support for URL fragment parameters This is a revert of the code changes in commit f796b05e42cfac7044cca9603e59f258605228f3 as it served no functional purpose. Fragments were already respected for setting parameters, via a different function. Thus it is unclear what that commit tried to fix. It also complicated things by mixing the document location with the window location. The comment changes are useful, though, so those are kept. --- app/webutil.js | 2 +- vnc_lite.html | 11 ++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/app/webutil.js b/app/webutil.js index 084c69f63..a2eab1939 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -32,7 +32,7 @@ export function initLogging(level) { 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) { diff --git a/vnc_lite.html b/vnc_lite.html index e725a2d94..eaf75f869 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 From 05b6d2ad67fbe6f3a7d7cbf1fbe0e0cec1362929 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 10 May 2023 12:24:53 +0200 Subject: [PATCH 18/65] Fix typos in query variable comment --- app/webutil.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/webutil.js b/app/webutil.js index a2eab1939..b94f035d3 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -25,10 +25,10 @@ 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 + '=([^&#]*)'), From cd1a63b737b1ffa0fb5a343de7381bedf562c5e4 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 6 Apr 2023 17:49:44 +0200 Subject: [PATCH 19/65] Restore history state after tests We don't want to mess up anything permanent in each test or the tests might start affecting each other. --- tests/test.webutil.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 76aa763a9..6f460a3fc 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"); }); }); From 0374b4c0fc28139aed1a06d9ec2cd5b66198cd94 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 6 Apr 2023 13:21:47 +0200 Subject: [PATCH 20/65] Handle translation loading in translation class Let's try to keep as much as possible of the translation handling in a single place for clarity. --- app/localization.js | 36 +++++++++-- app/ui.js | 18 +----- tests/test.localization.js | 122 ++++++++++++++++++++++++++----------- 3 files changed, 123 insertions(+), 53 deletions(-) diff --git a/app/localization.js b/app/localization.js index 73f66c51d..7d7e6e6af 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 @@ -83,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/ui.js b/app/ui.js index c1f6776ed..85695ca2e 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/tests/test.localization.js b/tests/test.localization.js index 1db2cb9bf..916ff8462 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -4,89 +4,143 @@ 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', function () { + it('should export a singleton translation function', async function () { // FIXME: Can we use some spy instead? - l10n.dictionary = { "Foobar": "gazonk" }; + 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 () { 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"]; let lclz = new Localizer(); - lclz.setup(["es", "fr"]); + await lclz.setup(["es", "fr"]); expect(lclz.language).to.equal('en'); }); - it('should fall back to generic English for other English', function () { + it('should fall back to generic English for other English', async function () { window.navigator.languages = ["en-AU", "de"]; let lclz = new Localizer(); - lclz.setup(["de", "fr", "en-GB"]); + await lclz.setup(["de", "fr", "en-GB"]); expect(lclz.language).to.equal('en'); }); - it('should prefer specific English over generic', function () { + it('should prefer specific English over generic', async function () { window.navigator.languages = ["en-GB", "de"]; let lclz = new Localizer(); - lclz.setup(["de", "en-AU", "en-GB"]); + await lclz.setup(["de", "en-AU", "en-GB"]); expect(lclz.language).to.equal('en-GB'); }); - it('should use the most preferred user language', function () { + it('should use the most preferred user language', async function () { window.navigator.languages = ["nl", "de", "fr"]; let lclz = new Localizer(); - lclz.setup(["es", "fr", "de"]); + 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"]; let lclz = new Localizer(); - lclz.setup(["pt", "pt-BR"]); + 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"]; let lclz = new Localizer(); - lclz.setup(["fr", "pt", "de"]); + 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"]; let lclz = new Localizer(); - lclz.setup(["fr", "pt-BR", "de"]); + 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"]; let lclz = new Localizer(); - lclz.setup(["pt_BR"]); + 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"]; let lclz = new Localizer(); - lclz.setup(["pt-BR"]); + 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; + }); + }); }); From 91307951d349ffac5295db00feb9386be8af2593 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 15 May 2023 12:55:04 +0200 Subject: [PATCH 21/65] Fix cached JPEG test This test didn't really check anything useful as the end result would be the same if the second JPEG failed to render. Fix this by clearing the canvas between the images, so we can tell if the second image actually rendered or not. --- tests/test.jpeg.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test.jpeg.js b/tests/test.jpeg.js index 6834f03d4..7580c6171 100644 --- a/tests/test.jpeg.js +++ b/tests/test.jpeg.js @@ -246,6 +246,8 @@ describe('JPEG Decoder', function () { testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24); + display.fillRect(0, 0, 4, 4, [128, 128, 128, 255]); + let data2 = [ // JPEG data 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, From 0dd9678e64842121ccbe89b5c91ff25df008abca Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sat, 13 May 2023 13:29:09 +0200 Subject: [PATCH 22/65] Harmonise extended clipboard tests Let them all follow the same pattern to make things more clear. --- tests/test.rfb.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 4262ee63a..de2c13dfb 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2528,17 +2528,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); @@ -2562,15 +2559,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); @@ -2589,15 +2583,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); From 775ccaa74cca90d1270fcd172e1507897aee270d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 14 May 2023 17:58:06 +0200 Subject: [PATCH 23/65] Handle immediate responses in RSA-AES authentication The event handlers might provide the needed response right away, before even existing the event handler. We should be prepared to handle this case. --- core/ra2.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/ra2.js b/core/ra2.js index 647aea2f1..9557054ff 100644 --- a/core/ra2.js +++ b/core/ra2.js @@ -158,10 +158,11 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { 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; @@ -260,6 +261,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { 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) { @@ -276,7 +278,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); From 9b115a4485eb6bebc3cdec799ad304749e0bc0fb Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 15 May 2023 20:01:10 +0200 Subject: [PATCH 24/65] Send ArrayBuffer, not Uint8Array in tests This matches the true contents of a WebSocket 'message' event, so should be a more realistic test. --- tests/fake.websocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index 8fb02c57a..e5a2b2d66 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -58,8 +58,8 @@ export default class FakeWebSocket { // 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 })); + let buf = data.slice(i, i+1); + this.onmessage(new MessageEvent("message", { 'data': buf.buffer })); } } } From da75689f4c3fcff11056592771495fa3806d1374 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 15 May 2023 20:50:40 +0200 Subject: [PATCH 25/65] Fix data for empty RRE rect test The given data was not a correct RRE rect. --- tests/test.rre.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test.rre.js b/tests/test.rre.js index 8e006f877..ac3aabbb1 100644 --- a/tests/test.rre.js +++ b/tests/test.rre.js @@ -93,7 +93,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); + 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, From 3ef57d16005e89ed0ed93a27efff0a66d8662bbc Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 10:37:41 +0200 Subject: [PATCH 26/65] Fix security to authentication state test The "None" authentication will directly progress past authentication, so it's not a good type for this test. --- tests/test.rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index de2c13dfb..eeaa1caa7 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1177,7 +1177,7 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should transition to the Authentication state and continue on successful negotiation', function () { - const authSchemes = [1, 1]; + const authSchemes = [1, 2]; client._negotiateAuthentication = sinon.spy(); client._sock._websocket._receiveData(new Uint8Array(authSchemes)); expect(client._rfbInitState).to.equal('Authentication'); From cd231e53ed5228380b1b1faac0494039f282d533 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 15:03:18 +0200 Subject: [PATCH 27/65] Don't overwrite methods with spies Spies should just attach without modifying the real method, or we might get unwanted side effects. --- tests/test.rfb.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index eeaa1caa7..27f6392fa 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -301,7 +301,7 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should call initMsg "soon"', function () { - client._initMsg = sinon.spy(); + sinon.spy(client, "_initMsg"); client.sendCredentials({ password: 'pass' }); this.clock.tick(5); expect(client._initMsg).to.have.been.calledOnce; @@ -393,13 +393,13 @@ describe('Remote Frame Buffer Protocol Client', function () { 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}); @@ -408,7 +408,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; }); @@ -1178,7 +1178,7 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should transition to the Authentication state and continue on successful negotiation', function () { const authSchemes = [1, 2]; - client._negotiateAuthentication = sinon.spy(); + sinon.spy(client, "_negotiateAuthentication"); client._sock._websocket._receiveData(new Uint8Array(authSchemes)); expect(client._rfbInitState).to.equal('Authentication'); expect(client._negotiateAuthentication).to.have.been.calledOnce; @@ -1430,7 +1430,7 @@ describe('Remote Frame Buffer Protocol Client', function () { client._rfbCredentials = { username: 'user', target: 'target', password: 'password' }; - client._negotiateStdVNCAuth = sinon.spy(); + sinon.spy(client, "_negotiateStdVNCAuth"); sendSecurity(22, client); expect(client._negotiateStdVNCAuth).to.have.been.calledOnce; }); @@ -1461,8 +1461,6 @@ describe('Remote Frame Buffer Protocol Client', function () { client._rfbCredentials = { username: 'user', target: 'target', password: 'password' }; - client._negotiateStdVNCAuth = sinon.spy(); - sendSecurity(22, client); const expected = [22, 4, 6]; // auth selection, len user, len target @@ -1526,7 +1524,7 @@ describe('Remote Frame Buffer Protocol Client', function () { /*it('should attempt to use VNC auth over no auth when possible', function () { client._rfbTightVNC = true; - client._negotiateStdVNCAuth = sinon.spy(); + sinon.spy(client, "_negotiateStdVNCAuth"); 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; @@ -1542,7 +1540,7 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should accept VNC authentication and transition to that', function () { client._rfbTightVNC = true; - client._negotiateStdVNCAuth = sinon.spy(); + 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; @@ -1651,7 +1649,7 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, client._rfbCredentials.password); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - client._initMsg = sinon.spy(); + sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); @@ -1674,7 +1672,7 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, client._rfbCredentials.password); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - client._initMsg = sinon.spy(); + sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); @@ -1697,7 +1695,7 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, client._rfbCredentials.password); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - client._initMsg = sinon.spy(); + sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); @@ -3882,13 +3880,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 +3894,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; }); From c7c293279b0a0b7e3545d27f29aae8692979748a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 12:29:48 +0200 Subject: [PATCH 28/65] Remove commented out Tight test case This is not something we intend to implement, so remove this never used test case. --- tests/test.rfb.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 27f6392fa..6310d8e46 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1522,15 +1522,6 @@ 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; - sinon.spy(client, "_negotiateStdVNCAuth"); - 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([[1, 'STDV', 'NOAUTH__']], client); From 29a50620ffacc9f40eb2e4f0f5f437e3db1858e5 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 12:30:36 +0200 Subject: [PATCH 29/65] Avoid touching internals in Tight auth tests We should test using only external manipulation so we don't assume a specific implementation. --- tests/test.rfb.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 6310d8e46..548c82ab3 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1523,14 +1523,16 @@ describe('Remote Frame Buffer Protocol Client', function () { }); 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; + 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])); @@ -1540,7 +1542,8 @@ 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; + 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; }); From 0679c8a80176b023912be1f338d59e84801ee122 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 16:45:36 +0200 Subject: [PATCH 30/65] Test credentials using normal API Avoid poking around in the internals and instead test things using the official methods and events. This should give us more realistic and robust tests. --- tests/test.rfb.js | 90 +++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 548c82ab3..0a6263438 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -287,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 () { - sinon.spy(client, "_initMsg"); - client.sendCredentials({ password: 'pass' }); - this.clock.tick(5); - expect(client._initMsg).to.have.been.calledOnce; - }); - }); }); describe('Public API Basic Behavior', function () { @@ -1240,31 +1220,36 @@ 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'); }); @@ -1287,10 +1272,8 @@ describe('Remote Frame Buffer Protocol Client', function () { 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"]); }); @@ -1298,7 +1281,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; @@ -1306,8 +1289,10 @@ 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]); @@ -1406,8 +1391,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]); @@ -1419,6 +1406,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'); @@ -1427,21 +1415,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.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"]); }); @@ -1449,8 +1438,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; @@ -1458,10 +1447,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.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); } @@ -1626,7 +1618,9 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should support Plain authentication', function () { - client._rfbCredentials = { username: 'username', password: 'password' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'username', password: 'password' }); + }); // VeNCrypt version client._sock._websocket._receiveData(new Uint8Array([0, 2])); expect(client._sock).to.have.sent(new Uint8Array([0, 2])); @@ -1635,6 +1629,8 @@ describe('Remote Frame Buffer Protocol Client', function () { // Subtype list. client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + clock.tick(); + const expectedResponse = []; push32(expectedResponse, 256); // Chosen subtype. push32(expectedResponse, client._rfbCredentials.username.length); @@ -1649,7 +1645,9 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should support Plain authentication with an empty password', function () { - client._rfbCredentials = { username: 'username', password: '' }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'username', password: '' }); + }); // VeNCrypt version client._sock._websocket._receiveData(new Uint8Array([0, 2])); expect(client._sock).to.have.sent(new Uint8Array([0, 2])); @@ -1658,6 +1656,8 @@ describe('Remote Frame Buffer Protocol Client', function () { // Subtype list. client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + clock.tick(); + const expectedResponse = []; push32(expectedResponse, 256); // Chosen subtype. push32(expectedResponse, client._rfbCredentials.username.length); @@ -1672,7 +1672,9 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should support Plain authentication with a very long username and password', function () { - client._rfbCredentials = { username: 'a'.repeat(300), password: 'a'.repeat(300) }; + client.addEventListener("credentialsrequired", () => { + client.sendCredentials({ username: 'a'.repeat(300), password: 'b'.repeat(300) }); + }); // VeNCrypt version client._sock._websocket._receiveData(new Uint8Array([0, 2])); expect(client._sock).to.have.sent(new Uint8Array([0, 2])); @@ -1681,6 +1683,8 @@ describe('Remote Frame Buffer Protocol Client', function () { // Subtype list. client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0])); + clock.tick(); + const expectedResponse = []; push32(expectedResponse, 256); // Chosen subtype. push32(expectedResponse, client._rfbCredentials.username.length); From 79f099108fb03bccf5f9541ebf5884753e19cecc Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 12:34:26 +0200 Subject: [PATCH 31/65] Split Plain authentication tests to own section VeNCrypt is a multiplexer for many authentication methods, not just Plain. So let's split it to its own section, just like other types. --- tests/test.rfb.js | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 0a6263438..5bb4001d9 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1616,23 +1616,29 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); }); + }); - it('should support Plain authentication', function () { - client.addEventListener("credentialsrequired", () => { - client.sendCredentials({ 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); @@ -1648,18 +1654,12 @@ describe('Remote Frame Buffer Protocol Client', function () { client.addEventListener("credentialsrequired", () => { client.sendCredentials({ 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._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); @@ -1675,18 +1675,12 @@ describe('Remote Frame Buffer Protocol Client', function () { client.addEventListener("credentialsrequired", () => { client.sendCredentials({ username: 'a'.repeat(300), password: 'b'.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._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); From 71bb6f02cdf993072172eefe138127e66943ca03 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 15:04:13 +0200 Subject: [PATCH 32/65] Fix Plain authentication test checks We should have constants local for the test function when doing comparisons or we might have false positives because we compare with buggy values in the code under test. --- tests/test.rfb.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 5bb4001d9..f5410eb08 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1639,10 +1639,10 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(); const expectedResponse = []; - 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)); sinon.spy(client, "_initMsg"); @@ -1660,10 +1660,10 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(); const expectedResponse = []; - 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)); sinon.spy(client, "_initMsg"); @@ -1681,10 +1681,10 @@ describe('Remote Frame Buffer Protocol Client', function () { clock.tick(); const expectedResponse = []; - 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)); sinon.spy(client, "_initMsg"); From 0ee0e96f34a3fa383e855b1f0b0e0b4b630e9311 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 19:23:25 +0200 Subject: [PATCH 33/65] Fix ARD authentication test to send real data Stop bypassing the data handling steps in the test as that means those parts don't get tested. --- tests/test.rfb.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index f5410eb08..c76ebf19d 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1306,9 +1306,27 @@ describe('Remote Frame Buffer Protocol Client', function () { const serverPublicKey = legacyCrypto.exportKey("raw", serverKey.publicKey); const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey); - await client._negotiateARDAuthAsync(128, serverPublicKey, clientKey); + let data = []; + + data = data.concat(Array.from(generator)); + push16(data, prime.length); + data = data.concat(Array.from(prime)); + data = data.concat(Array.from(serverPublicKey)); + + client._sock._websocket._receiveData(new Uint8Array(data)); - 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'); From 458405e05df5c9c04e6ec36a7e38725b04986c14 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 14 May 2023 17:59:01 +0200 Subject: [PATCH 34/65] Merge RSA-AES tests in to RFB tests These test the RFB class, so they should be with all other tests for that class. --- tests/test.ra2.js | 357 ------------------------------------------ tests/test.rfb.js | 385 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+), 357 deletions(-) delete mode 100644 tests/test.ra2.js diff --git a/tests/test.ra2.js b/tests/test.ra2.js deleted file mode 100644 index cc505b1fe..000000000 --- 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.rfb.js b/tests/test.rfb.js index c76ebf19d..ef2c6491f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1255,6 +1255,391 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); + 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 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, + ]); + + it('should fire the serververification event', function (done) { + client.addEventListener("serververification", (e) => { + expect(e.detail.publickey).to.eql(receiveData.slice(0, 516)); + done(); + }); + client._sock._websocket._receiveData(receiveData); + }); + + it('should handle approveServer and fire the credentialsrequired event', function (done) { + client.addEventListener("serververification", (e) => { + client.approveServer(); + }); + client.addEventListener("credentialsrequired", (e) => { + expect(e.detail.types).to.eql(["password"]); + done(); + }); + client._sock._websocket._receiveData(receiveData); + }); + + it('should match sendData after sending credentials', function (done) { + client.addEventListener("serververification", (e) => { + client.approveServer(); + }); + client.addEventListener("credentialsrequired", (e) => { + client.sendCredentials({ "password": "123456" }); + clock.tick(); + }); + client.addEventListener("securityresult", (event) => { + expect(client._sock).to.have.sent(sendData); + done(); + }); + client._sock._websocket._receiveData(receiveData); + }); + }); + describe('ARD Authentication (type 30) Handler', function () { let byteArray = new Uint8Array(Array.from(new Uint8Array(128).keys())); function fakeGetRandomValues(arr) { From afbb1da4d57f91fd0220c5282505ca752d25bdb3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 19:38:33 +0200 Subject: [PATCH 35/65] Remove custom RSA-AES event We shouldn't add extra, undocumented, API just for the tests. They need to figure out less invasive way to probe things. --- core/rfb.js | 1 - tests/test.rfb.js | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index d8e2de74b..ea8dc0ffb 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1864,7 +1864,6 @@ export default class RFB extends EventTargetMixin { } }) .then(() => { - this.dispatchEvent(new CustomEvent('securityresult')); this._rfbInitState = "SecurityResult"; return true; }).finally(() => { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index ef2c6491f..f3939151f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1631,10 +1631,20 @@ describe('Remote Frame Buffer Protocol Client', function () { client.addEventListener("credentialsrequired", (e) => { client.sendCredentials({ "password": "123456" }); clock.tick(); - }); - client.addEventListener("securityresult", (event) => { - expect(client._sock).to.have.sent(sendData); - done(); + // 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 + new Promise((resolve, reject) => { + sinon.stub(client._sock._websocket, "send") + .callsFake((data) => { + FakeWebSocket.prototype.send.call(client._sock._websocket, data); + resolve(); + }); + }).then(() => { + expect(client._sock).to.have.sent(sendData); + done(); + }); }); client._sock._websocket._receiveData(receiveData); }); From 42bc251eb4ce8bff00db58a7972061590f79e94e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 19 May 2023 16:00:23 +0200 Subject: [PATCH 36/65] Make RSA-AES tests more asynchronous The code tested here makes heavy use of promises, so it is easier to test things also using promise centric code. --- tests/test.rfb.js | 82 ++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index f3939151f..3825cebd2 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1605,48 +1605,72 @@ describe('Remote Frame Buffer Protocol Client', function () { 0x91, 0x38, ]); - it('should fire the serververification event', function (done) { - client.addEventListener("serververification", (e) => { - expect(e.detail.publickey).to.eql(receiveData.slice(0, 516)); - done(); + 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(receiveData); + + expect(await verification).to.deep.equal(receiveData.slice(0, 516)); }); - it('should handle approveServer and fire the credentialsrequired event', function (done) { - client.addEventListener("serververification", (e) => { - client.approveServer(); + 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); + }); }); - client.addEventListener("credentialsrequired", (e) => { - expect(e.detail.types).to.eql(["password"]); - done(); + let credentials = new Promise((resolve, reject) => { + client.addEventListener("credentialsrequired", (e) => { + resolve(e.detail.types); + }); }); + client._sock._websocket._receiveData(receiveData); + + await verification; + client.approveServer(); + + expect(await credentials).to.have.members(["password"]); }); - it('should match sendData after sending credentials', function (done) { - client.addEventListener("serververification", (e) => { - client.approveServer(); + it('should send credentials to server', async function () { + let verification = new Promise((resolve, reject) => { + client.addEventListener("serververification", (e) => { + resolve(e.detail.publickey); + }); }); - client.addEventListener("credentialsrequired", (e) => { - 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 - new Promise((resolve, reject) => { - sinon.stub(client._sock._websocket, "send") - .callsFake((data) => { - FakeWebSocket.prototype.send.call(client._sock._websocket, data); - resolve(); - }); - }).then(() => { - expect(client._sock).to.have.sent(sendData); - done(); + let credentials = new Promise((resolve, reject) => { + client.addEventListener("credentialsrequired", (e) => { + resolve(e.detail.types); }); }); + client._sock._websocket._receiveData(receiveData); + + await verification; + client.approveServer(); + + await credentials; + 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(sendData); }); }); From 549ccc7121a0ea4550c1854dc96b589a01be2ffd Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 17 May 2023 13:15:11 +0200 Subject: [PATCH 37/65] Split RSA-AES test data Make the tests more clear what data is expected in the different stages of the handshake. --- tests/test.rfb.js | 60 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 3825cebd2..bd91e3c9d 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1378,8 +1378,7 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._sock).to.have.sent(new Uint8Array([6])); }); - const receiveData = new Uint8Array([ - // server public key + 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, @@ -1445,7 +1444,9 @@ describe('Remote Frame Buffer Protocol Client', function () { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, - // server random + ]; + + 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, @@ -1479,20 +1480,23 @@ describe('Remote Frame Buffer Protocol Client', function () { 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, 0xdf, 0xcb, - // server hash + ]; + + 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, - // subtype + ]; + + const 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 + 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, @@ -1558,7 +1562,9 @@ describe('Remote Frame Buffer Protocol Client', function () { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, - // client random + ]; + + 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, @@ -1592,18 +1598,22 @@ describe('Remote Frame Buffer Protocol Client', function () { 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, 0xed, 0x84, - // client hash + ]; + + 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, - // credentials + ]; + + 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) => { @@ -1612,9 +1622,10 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - client._sock._websocket._receiveData(receiveData); + client._sock._websocket._receiveData(new Uint8Array(serverPublicKey)); + client._sock._websocket._receiveData(new Uint8Array(serverRandom)); - expect(await verification).to.deep.equal(receiveData.slice(0, 516)); + expect(await verification).to.deep.equal(new Uint8Array(serverPublicKey)); }); it('should handle approveServer and fire the credentialsrequired event', async function () { @@ -1629,11 +1640,15 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - client._sock._websocket._receiveData(receiveData); + 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"]); }); @@ -1649,12 +1664,23 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - client._sock._websocket._receiveData(receiveData); + 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(); @@ -1670,7 +1696,7 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - expect(client._sock).to.have.sent(sendData); + expect(client._sock).to.have.sent(new Uint8Array(credentialsData)); }); }); From 13fa6b5908c84cf53ec27252e83709df3e0b48c9 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 22 May 2023 13:05:10 +0200 Subject: [PATCH 38/65] Fix last rect test Avoid poking in to internals and instead test that the RFB object responds correctly to new messages. --- tests/test.rfb.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index bd91e3c9d..8cb29bed9 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2873,7 +2873,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 () { From 0c80c68e92587ed739817027581726d3962c40cd Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 22 May 2023 21:21:43 +0200 Subject: [PATCH 39/65] Avoid hooking in to RFB._fail for tests This is an internal function so we should not be examining it in the tests. Instead use the well defined public APIs to check for correct behaviour. --- tests/test.rfb.js | 97 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 8cb29bed9..9936716ec 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1081,9 +1081,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; }); }); @@ -1140,20 +1144,26 @@ 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 () { @@ -1177,11 +1187,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 () { @@ -1205,9 +1218,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 () { @@ -1954,9 +1971,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 () { @@ -1996,11 +2017,15 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should fail if there are no supported auth types', function () { - sinon.spy(client, "_fail"); + 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; }); }); @@ -2011,9 +2036,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 () { @@ -2023,9 +2052,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 () { @@ -2460,10 +2491,14 @@ describe('Remote Frame Buffer Protocol Client', function () { }); 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 () { @@ -2909,9 +2944,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; }); }); @@ -3216,9 +3255,13 @@ describe('Remote Frame Buffer Protocol Client', function () { }); 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; }); }); From 336ec86997bdf639d5799c40a262a979019c9471 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 23 May 2023 07:22:10 +0200 Subject: [PATCH 40/65] Remove internal monitoring from Plain tests Tests should avoid poking in to the internals and should only look at external behaviour. --- tests/test.rfb.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 9936716ec..5422b9953 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -2138,10 +2138,6 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, 'username'); pushString(expectedResponse, 'password'); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - sinon.spy(client, "_initMsg"); - 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 () { @@ -2159,10 +2155,6 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, 'username'); pushString(expectedResponse, ''); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - sinon.spy(client, "_initMsg"); - 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 () { @@ -2180,10 +2172,6 @@ describe('Remote Frame Buffer Protocol Client', function () { pushString(expectedResponse, 'a'.repeat(300)); pushString(expectedResponse, 'b'.repeat(300)); expect(client._sock).to.have.sent(new Uint8Array(expectedResponse)); - - sinon.spy(client, "_initMsg"); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); - expect(client._initMsg).to.have.been.called; }); }); }); From e07ca6a8e2428939de72348a1edfa7b749dec00b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 28 May 2023 16:30:41 +0200 Subject: [PATCH 41/65] Fix Websock send tests Avoid poking around in the internals and instead test what is actually sent out on the WebSocket. --- tests/test.websock.js | 49 ++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/tests/test.websock.js b/tests/test.websock.js index 857fdca80..1a0ba233f 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -6,7 +6,7 @@ import FakeWebSocket from './fake.websocket.js'; describe('Websock', function () { "use strict"; - describe('Queue methods', function () { + describe('Receive queue methods', function () { let sock; const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); @@ -185,61 +185,48 @@ describe('Websock', function () { expect(sock.rQi).to.equal(5); }); }); + }); - describe('flush', function () { - beforeEach(function () { - sock._websocket = { - send: sinon.spy() - }; - }); + describe('Send queue methods', function () { + let sock; + beforeEach(function () { + let websock = new FakeWebSocket(); + websock._open(); + sock = new Websock(); + sock.attach(websock); + }); + + describe('flush', function () { 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(encoded); }); 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; + expect(sock).to.have.sent(new Uint8Array([])); }); }); 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 () { + it('should send the given data immediately', function () { sock.send([1, 2, 3]); - expect(sock.flush).to.have.been.calledOnce; + expect(sock).to.have.sent(new Uint8Array([1, 2, 3])); }); }); describe('sendString', function () { - beforeEach(function () { - sock.send = sinon.spy(); - }); - - it('should call send after converting the string to an array', function () { + it('should 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([1, 2, 3])); }); }); }); From 9c7576a5876750c1413a712ae71825e250fdeb10 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 29 May 2023 09:30:26 +0200 Subject: [PATCH 42/65] Remove bad Websock mock in tests This small object will not properly fake a Websock in more complex cases, so let's avoid it and create a real Websock instead. --- tests/test.rfb.js | 173 +++++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 62 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 5422b9953..7eae56056 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -297,77 +297,102 @@ 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); }); }); @@ -2442,23 +2467,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])); @@ -2467,7 +2500,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 () { @@ -2475,7 +2508,7 @@ 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 () { @@ -3181,41 +3214,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"; - // ClientFence and ServerFence are identical in structure - RFB.messages.clientFence(expectedMsg, (1<<0) | (1<<1), payload); - RFB.messages.clientFence(incomingMsg, 0xffffffff, payload); + let esock = new Websock(); + let ews = new FakeWebSocket(); + ews._open(); + esock.attach(ews); - client._sock._websocket._receiveData(incomingMsg._sQ); + // ClientFence and ServerFence are identical in structure + RFB.messages.clientFence(esock, (1<<0) | (1<<1), payload); + let expected = ews._getSentData(); + RFB.messages.clientFence(esock, 0xffffffff, payload); + let incoming = ews._getSentData(); - expect(client._sock).to.have.sent(expectedMsg._sQ); + client._sock._websocket._receiveData(incoming); - expectedMsg._sQlen = 0; - incomingMsg._sQlen = 0; + expect(client._sock).to.have.sent(expected); - RFB.messages.clientFence(expectedMsg, (1<<0), payload); - RFB.messages.clientFence(incomingMsg, (1<<0) | (1<<31), payload); + RFB.messages.clientFence(esock, (1<<0), payload); + expected = ews._getSentData(); + RFB.messages.clientFence(esock, (1<<0) | (1<<31), payload); + 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); }); 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 () { @@ -3228,18 +3267,22 @@ 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 () { @@ -3662,10 +3705,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 () { From 9e02f4d01d85390b565cb041ad8f6c1a033d1740 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 29 May 2023 09:33:30 +0200 Subject: [PATCH 43/65] Return a copy of the data from FakeWebSocket The caller might hang on to the data for multiple calls, so we make sure the returned buffer might not get overwritten. --- tests/fake.websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index e5a2b2d66..a929a71fa 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; } From 8ae789daf04b06c4ddcc6a6663476b03ccd6df58 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 30 May 2023 07:05:43 +0200 Subject: [PATCH 44/65] Add missing tests for message encodings All of these functions should have units tests, even if they are fairly minimal. --- tests/test.rfb.js | 132 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 7eae56056..14fd39248 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -4697,14 +4697,56 @@ 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 () { @@ -4855,4 +4897,84 @@ describe('RFB messages', function () { }); }); }); + + 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)); + }); + }); }); From d33f5ce77fa8187dff1aeed55336ee56cb816739 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 30 May 2023 18:51:19 +0200 Subject: [PATCH 45/65] Make extended clipboard tests independent Let's test the full final result instead of assuming specific internal calls. --- tests/test.rfb.js | 152 +++++++++++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 57 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 14fd39248..01169cad4 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -4750,20 +4750,14 @@ describe('RFB messages', function () { }); 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 = [ @@ -4775,26 +4769,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 () { @@ -4804,16 +4802,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); }); @@ -4826,16 +4832,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 () { @@ -4846,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('Line feed', function () { @@ -4865,16 +4887,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 () { @@ -4884,16 +4914,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); }); }); }); From eb0ad829d23971377dc001dbe0fbdf47a0ea2a0f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sat, 3 Jun 2023 15:36:29 +0200 Subject: [PATCH 46/65] Check that decoders consume all data This is extra important in the tests where we expect no changes to the display, as otherwise we can't tell the difference between success and a decoder that is simply waiting for more data. --- tests/test.copyrect.js | 27 ++++++++----- tests/test.hextile.js | 28 ++++++++----- tests/test.jpeg.js | 18 ++++++--- tests/test.raw.js | 89 +++++++++++++++++++++++++++--------------- tests/test.rre.js | 19 +++++---- tests/test.tight.js | 79 ++++++++++++++++++++++++------------- tests/test.tightpng.js | 10 +++-- tests/test.zrle.js | 54 +++++++++++++++++-------- 8 files changed, 215 insertions(+), 109 deletions(-) diff --git a/tests/test.copyrect.js b/tests/test.copyrect.js index 90ba0c683..a10cddce7 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.hextile.js b/tests/test.hextile.js index a7034f058..cbe6f7b5a 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 7580c6171..5211cc7c4 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 () { @@ -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, @@ -244,7 +248,10 @@ 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]); @@ -265,7 +272,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, diff --git a/tests/test.raw.js b/tests/test.raw.js index bc7adc78e..4a634ccd0 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.rre.js b/tests/test.rre.js index ac3aabbb1..c55d7f397 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,10 +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, 0x00, 0x00, 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, @@ -105,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 cc5db36b5..cc92c1a23 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,6 +291,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); }); @@ -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, diff --git a/tests/test.tightpng.js b/tests/test.tightpng.js index 253400b87..c72c20d79 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 () { @@ -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, diff --git a/tests/test.zrle.js b/tests/test.zrle.js index e09d208da..be0464093 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); }); From 87143b361e78dda79b2f58b59d82d4696ba66163 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 15 May 2023 12:57:59 +0200 Subject: [PATCH 47/65] Reduce kept state in JPEG decoder We don't have to keep track of this much data between rects, so restructure things to make it more simple. This allows the JPEG parsing code to be a pure function which only depends on the input. --- core/decoders/jpeg.js | 217 +++++++++++++++++++++--------------------- 1 file changed, 111 insertions(+), 106 deletions(-) diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js index e1f2bdf87..157bf4f36 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"); + while (true) { + let segment = this._readSegment(sock); + if (segment === null) { + return false; } - 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; + this._segments.push(segment); + // End of image? + if (segment[1] === 0xD9) { + break; } - const data = new Uint8Array(length); - length = 0; - for (let i = 0; i < segments.length; i++) { - data.set(segments[i], length); - length += segments[i].length; + } + + 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); } - display.imageRect(x, y, width, height, "image/jpeg", data); - return true; } - } - _parseJPEG(buffer) { - if (this._quantTables.length != 0) { - this._cachedQuantTables = this._quantTables; + 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 (this._huffmanTables.length != 0) { - this._cachedHuffmanTables = this._huffmanTables; + if (quantTables.length !== 0) { + this._cachedQuantTables = quantTables; } - this._quantTables = []; - this._huffmanTables = []; + this._segments = []; - let i = 0; - let bufferLength = buffer.length; - while (true) { - let j = i; - if (j + 2 > bufferLength) { - 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; - } + + 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; } - if (!hasFoundEndOfScan) { - return false; + let data = sock.rQslice(0, length-2+extra); + if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && + !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { + extra -= 2; + break; } - 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; + extra++; } - 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; } + + 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), 4); + + return segment; } } From ae9b042df1c17bb702ca78cda2aa2ce1a30002f0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 11 May 2023 12:33:22 +0200 Subject: [PATCH 48/65] Change rQslice() to rQpeekBytes() We don't need any full slice functionality, so let's change this to better march rQpeek8() and rQshiftBytes(). --- core/decoders/jpeg.js | 2 +- core/ra2.js | 2 +- core/rfb.js | 2 +- core/websock.js | 5 +++-- tests/test.websock.js | 16 ++++++++-------- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js index 157bf4f36..5f5cc2654 100644 --- a/core/decoders/jpeg.js +++ b/core/decoders/jpeg.js @@ -124,7 +124,7 @@ export default class JPEGDecoder { if (sock.rQwait("JPEG", length-2+extra, 4)) { return null; } - let data = sock.rQslice(0, length-2+extra); + let data = sock.rQpeekBytes(length-2+extra); if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 && !(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) { extra -= 2; diff --git a/core/ra2.js b/core/ra2.js index 9557054ff..b2bfb50ab 100644 --- a/core/ra2.js +++ b/core/ra2.js @@ -139,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); diff --git a/core/rfb.js b/core/rfb.js index ea8dc0ffb..eba2e1dad 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2455,7 +2455,7 @@ 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; } } diff --git a/core/websock.js b/core/websock.js index e07e31ba6..3813af1c0 100644 --- a/core/websock.js +++ b/core/websock.js @@ -168,8 +168,9 @@ export default class Websock { this._rQi += len; } - rQslice(start, end = this.rQlen) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + rQpeekBytes(len) { + if (typeof(len) === 'undefined') { len = this.rQlen; } + return new Uint8Array(this._rQ.buffer, this._rQi, len); } // Check to see if we must wait for 'num' bytes (default to FBU.bytes) diff --git a/tests/test.websock.js b/tests/test.websock.js index 1a0ba233f..d2e20e0db 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -126,32 +126,32 @@ describe('Websock', function () { }); }); - describe('rQslice', function () { + describe('rQpeekBytes', function () { beforeEach(function () { sock.rQi = 0; }); it('should not modify the receive queue', function () { const befLen = sock.rQlen; - sock.rQslice(0, 2); + sock.rQpeekBytes(2); expect(sock.rQlen).to.equal(befLen); }); - it('should return an array containing the given slice of the receive queue', function () { - const sl = sock.rQslice(0, 2); + it('should return an array containing the requested bytes of the receive queue', function () { + const sl = sock.rQpeekBytes(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)); + const sl = sock.rQpeekBytes(); + expect(sl).to.have.length(RQ_TEMPLATE.length); + expect(sl).to.array.equal(RQ_TEMPLATE); }); 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)); + expect(sock.rQpeekBytes(2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); }); }); From fb3c8f64e99674c48a1f317633d337b14e1e3471 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 11 May 2023 22:32:13 +0200 Subject: [PATCH 49/65] Switch Display.flush() to use a promise That is the modern way to handle operations that cannot complete immediately. --- core/display.js | 23 +++++++++++++---------- core/rfb.js | 18 ++++++++---------- docs/API-internal.md | 6 ------ tests/playback.js | 19 ++++++------------- tests/test.display.js | 18 +++++++----------- tests/test.jpeg.js | 18 ++++++------------ tests/test.tight.js | 9 +++------ tests/test.tightpng.js | 9 +++------ 8 files changed, 46 insertions(+), 74 deletions(-) diff --git a/core/display.js b/core/display.js index bf8d5fab6..fcd626999 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/rfb.js b/core/rfb.js index eba2e1dad..c47f9ce40 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -257,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); @@ -2460,14 +2459,6 @@ export default class RFB extends EventTargetMixin { } } - _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; } @@ -2478,7 +2469,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.rQlen > 0) { + this._handleMessage(); + } + }); return false; } } diff --git a/docs/API-internal.md b/docs/API-internal.md index 2cac8ddf4..c41e0f326 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/tests/playback.js b/tests/playback.js index 19ab2c340..955df0ee3 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.display.js b/tests/test.display.js index 0604997c9..e6c0406f9 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.jpeg.js b/tests/test.jpeg.js index 5211cc7c4..8dee48912 100644 --- a/tests/test.jpeg.js +++ b/tests/test.jpeg.js @@ -44,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, @@ -151,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, @@ -289,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.tight.js b/tests/test.tight.js index cc92c1a23..b3457a88b 100644 --- a/tests/test.tight.js +++ b/tests/test.tight.js @@ -295,7 +295,7 @@ describe('Tight Decoder', function () { 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, @@ -410,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 c72c20d79..02c66d93b 100644 --- a/tests/test.tightpng.js +++ b/tests/test.tightpng.js @@ -44,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, @@ -139,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); }); }); From 0180bc81c14bb3b9a23eae679c9fad075f53069c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 11 May 2023 23:06:34 +0200 Subject: [PATCH 50/65] Stop direct access to socket buffer Use proper accessor functions instead of poking around in internal buffers. --- core/decoders/hextile.js | 34 +++++++++++------------------ core/decoders/raw.js | 15 ++++++------- core/decoders/tight.js | 8 ++----- core/websock.js | 16 -------------- tests/test.websock.js | 46 ++++++++++++++++++++-------------------- 5 files changed, 43 insertions(+), 76 deletions(-) diff --git a/core/decoders/hextile.js b/core/decoders/hextile.js index ac21eff03..6d96927f7 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); // 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/raw.js b/core/decoders/raw.js index d08f7ba95..c69ec7ac0 100644 --- a/core/decoders/raw.js +++ b/core/decoders/raw.js @@ -33,29 +33,26 @@ export default class RawDecoder { Math.floor(sock.rQlen / bytesPerLine)); const pixels = width * currHeight; - let data = sock.rQ; - let index = sock.rQi; + let data = sock.rQshiftBytes(currHeight * bytesPerLine); // 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 + 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; - index = 0; } // Max sure the image is fully opaque for (let i = 0; i < pixels; i++) { - data[index + i * 4 + 3] = 255; + data[i * 4 + 3] = 255; } - display.blitImage(x, curY, width, currHeight, data, index); - sock.rQskipBytes(currHeight * bytesPerLine); + display.blitImage(x, curY, width, currHeight, data, 0); this._lines -= currHeight; if (this._lines > 0) { return false; diff --git a/core/decoders/tight.js b/core/decoders/tight.js index 7952707c5..5eaed5452 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; } diff --git a/core/websock.js b/core/websock.js index 3813af1c0..d5f03d28c 100644 --- a/core/websock.js +++ b/core/websock.js @@ -94,22 +94,6 @@ 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; diff --git a/tests/test.websock.js b/tests/test.websock.js index d2e20e0db..4b6fe2309 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -19,13 +19,13 @@ describe('Websock', function () { }); describe('rQlen', function () { it('should return the length of the receive queue', function () { - sock.rQi = 0; + 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; + sock._rQi = 1; expect(sock.rQlen).to.equal(RQ_TEMPLATE.length - 1); }); @@ -72,8 +72,8 @@ describe('Websock', function () { 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 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)))); @@ -112,8 +112,8 @@ describe('Websock', function () { 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 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)); @@ -128,7 +128,7 @@ describe('Websock', function () { describe('rQpeekBytes', function () { beforeEach(function () { - sock.rQi = 0; + sock._rQi = 0; }); it('should not modify the receive queue', function () { @@ -150,14 +150,14 @@ describe('Websock', function () { }); it('should take the current rQi in to account', function () { - sock.rQi = 1; + sock._rQi = 1; expect(sock.rQpeekBytes(2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); }); }); describe('rQwait', function () { beforeEach(function () { - sock.rQi = 0; + sock._rQi = 0; }); it('should return true if there are not enough bytes in the receive queue', function () { @@ -169,20 +169,20 @@ describe('Websock', function () { }); it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { - sock.rQi = 5; + sock._rQi = 5; expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; - expect(sock.rQi).to.equal(1); + expect(sock._rQi).to.equal(1); }); it('should raise an error if we try to go back more than possible', function () { - sock.rQi = 5; + sock._rQi = 5; expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); }); it('should not reduce rQi if there are enough bytes', function () { - sock.rQi = 5; + sock._rQi = 5; sock.rQwait('hi', 1, 6); - expect(sock.rQi).to.equal(5); + expect(sock._rQi).to.equal(5); }); }); }); @@ -461,52 +461,52 @@ describe('Websock', function () { }); it('should compact the receive queue when a message handler empties it', function () { - sock._eventHandlers.message = () => { sock.rQi = sock._rQlen; }; + sock._eventHandlers.message = () => { sock._rQi = sock._rQlen; }; 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._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 = 16; 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._rQi).to.equal(0); expect(sock._rQ.length).to.equal(48); }); }); From 55ffe8fc513b720a8bf84178d3c81af7a1ce314c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 14 May 2023 18:56:19 +0200 Subject: [PATCH 51/65] Stop exposing Websock queue length Callers should be using rQwait() to ensure sufficient data is present, and not poke around in the internal buffering. --- core/decoders/raw.js | 52 ++++++++++++++++++++----------------------- core/rfb.js | 6 ++--- core/websock.js | 18 ++++++--------- tests/test.websock.js | 43 +++++++++++++---------------------- 4 files changed, 49 insertions(+), 70 deletions(-) diff --git a/core/decoders/raw.js b/core/decoders/raw.js index c69ec7ac0..488ac1af6 100644 --- a/core/decoders/raw.js +++ b/core/decoders/raw.js @@ -24,38 +24,34 @@ export default class RawDecoder { const pixelSize = depth == 8 ? 1 : 4; const bytesPerLine = width * pixelSize; - if (sock.rQwait("RAW", bytesPerLine)) { - return false; - } + while (this._lines > 0) { + if (sock.rQwait("RAW", bytesPerLine)) { + return false; + } + + const curY = y + (height - this._lines); + + let data = sock.rQshiftBytes(bytesPerLine); - const curY = y + (height - this._lines); - const currHeight = Math.min(this._lines, - Math.floor(sock.rQlen / bytesPerLine)); - const pixels = width * currHeight; - - let data = sock.rQshiftBytes(currHeight * bytesPerLine); - - // 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[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; + // 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; } - data = newdata; - } - // Max sure the image is fully opaque - for (let i = 0; i < pixels; i++) { - data[i * 4 + 3] = 255; - } + // Max sure the image is fully opaque + for (let i = 0; i < width; i++) { + data[i * 4 + 3] = 255; + } - display.blitImage(x, curY, width, currHeight, data, 0); - this._lines -= currHeight; - if (this._lines > 0) { - return false; + display.blitImage(x, curY, width, 1, data, 0); + this._lines--; } return true; diff --git a/core/rfb.js b/core/rfb.js index c47f9ce40..0f33057dc 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -958,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; } @@ -975,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; } } @@ -2473,7 +2473,7 @@ export default class RFB extends EventTargetMixin { .then(() => { this._flushing = false; // Resume processing - if (this._sock.rQlen > 0) { + if (!this._sock.rQwait("message", 1)) { this._handleMessage(); } }); diff --git a/core/websock.js b/core/websock.js index d5f03d28c..7b770d4b5 100644 --- a/core/websock.js +++ b/core/websock.js @@ -95,10 +95,6 @@ export default class Websock { } // Receive Queue - get rQlen() { - return this._rQlen - this._rQi; - } - rQpeek8() { return this._rQ[this._rQi]; } @@ -129,7 +125,7 @@ export default class Websock { } rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } let str = ""; // Handle large arrays in steps to avoid long strings on the stack for (let i = 0; i < len; i += 4096) { @@ -140,20 +136,20 @@ export default class Websock { } rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } this._rQi += len; return new Uint8Array(this._rQ.buffer, this._rQi - len, len); } rQshiftTo(target, len) { - if (len === undefined) { len = this.rQlen; } + if (len === undefined) { len = this._rQlen - this._rQi; } // 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; } rQpeekBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen; } + if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } return new Uint8Array(this._rQ.buffer, this._rQi, len); } @@ -161,7 +157,7 @@ export default class Websock { // 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"); @@ -294,7 +290,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"); } } @@ -323,7 +319,7 @@ export default class Websock { _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 diff --git a/tests/test.websock.js b/tests/test.websock.js index 4b6fe2309..91f69865d 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -17,56 +17,43 @@ describe('Websock', function () { 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); - }); - }); describe('rQpeek8', function () { it('should peek at the next byte without poping it off the queue', function () { - const befLen = sock.rQlen; + const befLen = sock._rQlen - sock._rQi; const peek = sock.rQpeek8(); expect(sock.rQpeek8()).to.equal(peek); - expect(sock.rQlen).to.equal(befLen); + expect(sock._rQlen - sock._rQi).to.equal(befLen); }); }); describe('rQshift8()', function () { it('should pop a single byte from the receive queue', function () { const peek = sock.rQpeek8(); - const befLen = sock.rQlen; + const befLen = sock._rQlen - sock._rQi; expect(sock.rQshift8()).to.equal(peek); - expect(sock.rQlen).to.equal(befLen - 1); + expect(sock._rQlen - sock._rQi).to.equal(befLen - 1); }); }); describe('rQshift16()', function () { it('should pop two bytes from the receive queue and return a single number', function () { - const befLen = sock.rQlen; + const befLen = sock._rQlen - sock._rQi; const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; expect(sock.rQshift16()).to.equal(expected); - expect(sock.rQlen).to.equal(befLen - 2); + expect(sock._rQlen - sock._rQi).to.equal(befLen - 2); }); }); describe('rQshift32()', function () { it('should pop four bytes from the receive queue and return a single number', function () { - const befLen = sock.rQlen; + const befLen = sock._rQlen - sock._rQi; 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); + expect(sock._rQlen - sock._rQi).to.equal(befLen - 4); }); }); @@ -77,12 +64,12 @@ describe('Websock', function () { 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); + expect(sock._rQlen - sock._rQi).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); + expect(sock._rQlen - sock._rQi).to.equal(0); }); it('should be able to handle very large strings', function () { @@ -106,7 +93,7 @@ describe('Websock', function () { const shifted = sock.rQshiftStr(); expect(shifted).to.be.equal(expected); - expect(sock.rQlen).to.equal(0); + expect(sock._rQlen - sock._rQi).to.equal(0); }); }); @@ -117,12 +104,12 @@ describe('Websock', function () { 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); + expect(sock._rQlen - sock._rQi).to.equal(befLen - 3); }); it('should shift the entire rest of the queue off if no length is given', function () { sock.rQshiftBytes(); - expect(sock.rQlen).to.equal(0); + expect(sock._rQlen - sock._rQi).to.equal(0); }); }); @@ -132,9 +119,9 @@ describe('Websock', function () { }); it('should not modify the receive queue', function () { - const befLen = sock.rQlen; + const befLen = sock._rQlen - sock._rQi; sock.rQpeekBytes(2); - expect(sock.rQlen).to.equal(befLen); + expect(sock._rQlen - sock._rQi).to.equal(befLen); }); it('should return an array containing the requested bytes of the receive queue', function () { From e01dd27be49940e3a49e6cc90bd6ae7d0d1d7abd Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 14 May 2023 20:15:12 +0200 Subject: [PATCH 52/65] Remove Websock implicit read length Callers should be properly aware of how much data they need, as they need to call rQwait() first to ensure the data is present. --- core/websock.js | 4 ---- tests/test.websock.js | 18 +----------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/core/websock.js b/core/websock.js index 7b770d4b5..b6891329e 100644 --- a/core/websock.js +++ b/core/websock.js @@ -125,7 +125,6 @@ export default class Websock { } rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } let str = ""; // Handle large arrays in steps to avoid long strings on the stack for (let i = 0; i < len; i += 4096) { @@ -136,20 +135,17 @@ export default class Websock { } rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } this._rQi += len; return new Uint8Array(this._rQ.buffer, this._rQi - len, len); } rQshiftTo(target, len) { - if (len === undefined) { len = this._rQlen - this._rQi; } // 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; } rQpeekBytes(len) { - if (typeof(len) === 'undefined') { len = this._rQlen - this._rQi; } return new Uint8Array(this._rQ.buffer, this._rQi, len); } diff --git a/tests/test.websock.js b/tests/test.websock.js index 91f69865d..2034bd6e7 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -67,11 +67,6 @@ describe('Websock', function () { expect(sock._rQlen - sock._rQi).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 - sock._rQi).to.equal(0); - }); - it('should be able to handle very large strings', function () { const BIG_LEN = 500000; const RQ_BIG = new Uint8Array(BIG_LEN); @@ -90,7 +85,7 @@ describe('Websock', function () { sock._rQ.set(RQ_BIG); sock._rQlen = RQ_BIG.length; - const shifted = sock.rQshiftStr(); + const shifted = sock.rQshiftStr(BIG_LEN); expect(shifted).to.be.equal(expected); expect(sock._rQlen - sock._rQi).to.equal(0); @@ -106,11 +101,6 @@ describe('Websock', function () { expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)); expect(sock._rQlen - sock._rQi).to.equal(befLen - 3); }); - - it('should shift the entire rest of the queue off if no length is given', function () { - sock.rQshiftBytes(); - expect(sock._rQlen - sock._rQi).to.equal(0); - }); }); describe('rQpeekBytes', function () { @@ -130,12 +120,6 @@ describe('Websock', function () { 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.rQpeekBytes(); - expect(sl).to.have.length(RQ_TEMPLATE.length); - expect(sl).to.array.equal(RQ_TEMPLATE); - }); - it('should take the current rQi in to account', function () { sock._rQi = 1; expect(sock.rQpeekBytes(2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); From aaa4eb8c3c358feb08c52991c331684ccd63b34c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 18:19:02 +0200 Subject: [PATCH 53/65] Use proper socket helpers for FBU header Let's not duplicate this stuff when we have convenience functions. --- core/rfb.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 0f33057dc..8ac38aabf 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2486,13 +2486,11 @@ 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(); } if (!this._handleRect()) { From d0203a5995ef27c4f49c7d22da89438bdaee9a68 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 16 May 2023 19:06:10 +0200 Subject: [PATCH 54/65] Always return copy of data from socket We don't know how long the caller will hang on to this data, so we need to be safe by default and assume it will kept indefinitely. That means we can't return a reference to the internal buffer, as that will get overwritten with future messages. We want to avoid unnecessary copying in performance critical code, though. So allow code to explicitly ask for a shared buffer, assuming they know the data needs to be consumed immediately. --- core/decoders/hextile.js | 2 +- core/decoders/jpeg.js | 4 ++-- core/decoders/raw.js | 2 +- core/decoders/tight.js | 2 +- core/decoders/zrle.js | 2 +- core/websock.js | 18 +++++++++++++----- tests/test.websock.js | 12 ++++++++++++ 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/core/decoders/hextile.js b/core/decoders/hextile.js index 6d96927f7..cc33e0e10 100644 --- a/core/decoders/hextile.js +++ b/core/decoders/hextile.js @@ -86,7 +86,7 @@ export default class HextileDecoder { } } else if (subencoding & 0x01) { // Raw let pixels = tw * th; - let data = sock.rQshiftBytes(pixels * 4); + let data = sock.rQshiftBytes(pixels * 4, false); // Max sure the image is fully opaque for (let i = 0;i < pixels;i++) { data[i * 4 + 3] = 255; diff --git a/core/decoders/jpeg.js b/core/decoders/jpeg.js index 5f5cc2654..feb2aeb6c 100644 --- a/core/decoders/jpeg.js +++ b/core/decoders/jpeg.js @@ -124,7 +124,7 @@ export default class JPEGDecoder { if (sock.rQwait("JPEG", length-2+extra, 4)) { return null; } - let data = sock.rQpeekBytes(length-2+extra); + 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; @@ -139,7 +139,7 @@ export default class JPEGDecoder { segment[1] = type; segment[2] = length >> 8; segment[3] = length; - segment.set(sock.rQshiftBytes(length-2+extra), 4); + segment.set(sock.rQshiftBytes(length-2+extra, false), 4); return segment; } diff --git a/core/decoders/raw.js b/core/decoders/raw.js index 488ac1af6..3c1661425 100644 --- a/core/decoders/raw.js +++ b/core/decoders/raw.js @@ -31,7 +31,7 @@ export default class RawDecoder { const curY = y + (height - this._lines); - let data = sock.rQshiftBytes(bytesPerLine); + let data = sock.rQshiftBytes(bytesPerLine, false); // Convert data if needed if (depth == 8) { diff --git a/core/decoders/tight.js b/core/decoders/tight.js index 5eaed5452..45622080f 100644 --- a/core/decoders/tight.js +++ b/core/decoders/tight.js @@ -312,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 97fbd58e7..49128e798 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/websock.js b/core/websock.js index b6891329e..7cd87091f 100644 --- a/core/websock.js +++ b/core/websock.js @@ -128,15 +128,19 @@ export default class Websock { 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) { + 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) { @@ -145,8 +149,12 @@ export default class Websock { this._rQi += len; } - rQpeekBytes(len) { - return new Uint8Array(this._rQ.buffer, this._rQi, len); + 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) diff --git a/tests/test.websock.js b/tests/test.websock.js index 2034bd6e7..d501021a3 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -101,6 +101,12 @@ describe('Websock', function () { expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)); expect(sock._rQlen - sock._rQi).to.equal(befLen - 3); }); + it('should return a shared array if requested', function () { + const befRQi = sock._rQi; + const shifted = sock.rQshiftBytes(3, false); + expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)); + expect(shifted.buffer.byteLength).to.not.equal(shifted.length); + }); }); describe('rQpeekBytes', function () { @@ -124,6 +130,12 @@ describe('Websock', function () { sock._rQi = 1; expect(sock.rQpeekBytes(2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); }); + + it('should return a shared array if requested', function () { + const sl = sock.rQpeekBytes(2, false); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); + expect(sl.buffer.byteLength).to.not.equal(sl.length); + }); }); describe('rQwait', function () { From 0ccc679d322947a05f2fc169da22d915930a75a6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 14:50:35 +0200 Subject: [PATCH 55/65] Return unsigned values from rQshift32() This is what we almost always want, and this makes it consistent with rQshift8() and rQshift16(). --- core/rfb.js | 2 ++ core/websock.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/rfb.js b/core/rfb.js index 8ac38aabf..b4045e527 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2491,6 +2491,8 @@ export default class RFB extends EventTargetMixin { 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()) { diff --git a/core/websock.js b/core/websock.js index 7cd87091f..2b9b519a1 100644 --- a/core/websock.js +++ b/core/websock.js @@ -121,7 +121,7 @@ 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) { From 12d2e7832d15d10e6c105c709b86d4adc040548d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 14:54:40 +0200 Subject: [PATCH 56/65] Properly decode ExtendedDesktopSize fields We are expected to preserve these and use them in our requests back to the server. We can't do that if we don't actually decode them correctly. --- core/rfb.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index b4045e527..d89f029ab 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2740,12 +2740,12 @@ export default class RFB extends EventTargetMixin { 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); } From 45cedea78fc5f05b32c8514132349710707081b7 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 14:55:35 +0200 Subject: [PATCH 57/65] Don't send SetDesktopSize too early We don't know the server layout yet, so we can't preserve the screen id or flags yet at this point. Move it until after we've parsed everything. --- core/rfb.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index d89f029ab..6857ed71f 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -2726,14 +2726,6 @@ 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 @@ -2783,6 +2775,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; } From 2a7db6f647954ebb0dc2d5734b2cb7d44f323db6 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 14:56:48 +0200 Subject: [PATCH 58/65] Make ExtendedDesktopSize tests more realistic Send real messages and avoid poking around in internals, as we weren't testing things correctly that way. --- tests/test.rfb.js | 60 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 01169cad4..bf12a4600 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -814,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"); }); @@ -833,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 @@ -846,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 @@ -866,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(); @@ -884,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 () { @@ -895,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, @@ -934,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 () { @@ -974,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. @@ -996,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); }); }); From b298bf9e901ea4264a83abd498415da49388709e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 22:31:27 +0200 Subject: [PATCH 59/65] Don't split large WebSocket data in tests It takes too much time and can make the tests fail. --- tests/fake.websocket.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index a929a71fa..d273fe07a 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -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.slice(i, i+1); - this.onmessage(new MessageEvent("message", { 'data': buf.buffer })); + 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 })); } } } From b146de6d69b65588a74a7142bd332bf3e08f9202 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 4 Jun 2023 14:52:36 +0200 Subject: [PATCH 60/65] Avoid internal variables in recv queue tests Makes for more robust and realistic tests. --- tests/test.websock.js | 133 +++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/tests/test.websock.js b/tests/test.websock.js index d501021a3..e72ba272e 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -7,73 +7,63 @@ describe('Websock', function () { "use strict"; describe('Receive queue methods', function () { - let sock; - const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + let sock, websock; beforeEach(function () { sock = new Websock(); - // skip init - sock._allocateBuffers(); - sock._rQ.set(RQ_TEMPLATE); - sock._rQlen = RQ_TEMPLATE.length; + 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 - sock._rQi; - const peek = sock.rQpeek8(); - expect(sock.rQpeek8()).to.equal(peek); - expect(sock._rQlen - sock._rQi).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 - sock._rQi; - expect(sock.rQshift8()).to.equal(peek); - expect(sock._rQlen - sock._rQi).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 - sock._rQi; - const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; - expect(sock.rQshift16()).to.equal(expected); - expect(sock._rQlen - sock._rQi).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 - sock._rQi; - 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 - sock._rQi).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 - sock._rQi).to.equal(befLen - 3); + 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)) { @@ -82,90 +72,77 @@ describe('Websock', function () { letterCode = 'a'.charCodeAt(0); } } - sock._rQ.set(RQ_BIG); - sock._rQlen = RQ_BIG.length; + websock._receiveData(incoming); const shifted = sock.rQshiftStr(BIG_LEN); expect(shifted).to.be.equal(expected); - expect(sock._rQlen - sock._rQi).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 - sock._rQi).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 return a shared array if requested', function () { - const befRQi = sock._rQi; - const shifted = sock.rQshiftBytes(3, false); - expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3)); - expect(shifted.buffer.byteLength).to.not.equal(shifted.length); + 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('rQpeekBytes', function () { - beforeEach(function () { - sock._rQi = 0; - }); - it('should not modify the receive queue', function () { - const befLen = sock._rQlen - sock._rQi; - sock.rQpeekBytes(2); - expect(sock._rQlen - sock._rQi).to.equal(befLen); - }); - - it('should return an array containing the requested bytes of the receive queue', function () { - const sl = sock.rQpeekBytes(2); - expect(sl).to.be.an.instanceof(Uint8Array); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); - }); - - it('should take the current rQi in to account', function () { - sock._rQi = 1; - expect(sock.rQpeekBytes(2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); + 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 a shared array if requested', function () { - const sl = sock.rQpeekBytes(2, false); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); - expect(sl.buffer.byteLength).to.not.equal(sl.length); + 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); }); }); }); From 3fc0cb0cb70ae1cc6e07976e630660f36c020085 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 15 May 2023 13:33:11 +0200 Subject: [PATCH 61/65] Remove Base64 WebSocket remnants There is no encoding/decoding in modern WebSockets, so let's clean up some of the old crud that no longer serves a purpose. --- core/websock.js | 14 +++----------- tests/test.websock.js | 36 ++---------------------------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/core/websock.js b/core/websock.js index 2b9b519a1..711ea02bb 100644 --- a/core/websock.js +++ b/core/websock.js @@ -177,7 +177,7 @@ export default class Websock { 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; } } @@ -268,11 +268,6 @@ 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. @@ -312,17 +307,14 @@ export default class Websock { } // push arraybuffer values onto the end of the receive que - _DecodeMessage(data) { - const u8 = new Uint8Array(data); + _recvMessage(e) { + 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 - this._rQi > 0) { this._eventHandlers.message(); if (this._rQlen == this._rQi) { diff --git a/tests/test.websock.js b/tests/test.websock.js index e72ba272e..de7fe0d18 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -161,10 +161,9 @@ describe('Websock', function () { it('should actually send on the websocket', function () { sock._sQ = new Uint8Array([1, 2, 3]); sock._sQlen = 3; - const encoded = sock._encodeMessage(); sock.flush(); - expect(sock).to.have.sent(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 () { @@ -397,9 +396,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'); }); @@ -426,7 +424,6 @@ describe('Websock', function () { sock._rQlen = 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); @@ -438,7 +435,6 @@ describe('Websock', function () { sock._rQlen = 20; 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); @@ -450,7 +446,6 @@ describe('Websock', function () { 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); @@ -463,37 +458,10 @@ describe('Websock', function () { sock._rQi = 16; 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])); - }); - }); - }); }); From 7356d4e60b689fc098b55f02ae3d7688e166194f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 21 May 2023 20:16:59 +0200 Subject: [PATCH 62/65] Move WebSocket queue index reset to receive It's more robust to do this just before we need the space, rather than assume when the queue will be read and adjust things right after. --- core/websock.js | 12 ++++++------ tests/test.websock.js | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/core/websock.js b/core/websock.js index 711ea02bb..75be044a3 100644 --- a/core/websock.js +++ b/core/websock.js @@ -308,6 +308,12 @@ export default class Websock { // push arraybuffer values onto the end of the receive que _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); @@ -317,12 +323,6 @@ export default class Websock { 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/tests/test.websock.js b/tests/test.websock.js index de7fe0d18..f3cc42247 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -418,14 +418,13 @@ 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; const msg = { data: new Uint8Array([1, 2, 3]).buffer }; sock._recvMessage(msg); - expect(sock._rQlen).to.equal(0); + expect(sock._rQlen).to.equal(3); expect(sock._rQi).to.equal(0); }); @@ -455,13 +454,13 @@ describe('Websock', function () { 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._recvMessage(msg); - expect(sock._rQlen).to.equal(6); + expect(sock._rQlen).to.equal(7); expect(sock._rQi).to.equal(0); - expect(sock._rQ.length).to.equal(48); + expect(sock._rQ.length).to.equal(56); }); }); }); From f8b65f9fe140ef194e166ca4867d2ae49e8daab3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Sun, 28 May 2023 16:40:09 +0200 Subject: [PATCH 63/65] Add Websock send queue helpers Callers shouldn't be poking around directly in to the send queue, but should use accessor functions like for the read queue. --- core/ra2.js | 12 +- core/rfb.js | 350 +++++++++++++++--------------------------- core/websock.js | 36 +++-- tests/test.websock.js | 74 +++++++-- 4 files changed, 220 insertions(+), 252 deletions(-) diff --git a/core/ra2.js b/core/ra2.js index b2bfb50ab..d330b848d 100644 --- a/core/ra2.js +++ b/core/ra2.js @@ -182,7 +182,8 @@ 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); @@ -193,7 +194,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin { 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); @@ -234,7 +236,8 @@ 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"); @@ -295,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() { diff --git a/core/rfb.js b/core/rfb.js index 6857ed71f..da95a3866 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1381,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; } @@ -1391,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'; @@ -1443,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; } @@ -1505,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(); } @@ -1528,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; } @@ -1587,10 +1595,8 @@ 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; return true; @@ -1609,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; @@ -1641,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; } @@ -1659,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"; @@ -1724,10 +1723,12 @@ export default class RFB extends EventTargetMixin { return false; } - this._sock.send([0, 0, 0, this._rfbCredentials.username.length]); - this._sock.send([0, 0, 0, this._rfbCredentials.password.length]); - this._sock.sendString(this._rfbCredentials.username); - this._sock.sendString(this._rfbCredentials.password); + this._sock.sQpush32(this._rfbCredentials.username.length); + this._sock.sQpush32(this._rfbCredentials.password.length); + this._sock.sQpushString(this._rfbCredentials.username); + this._sock.sQpushString(this._rfbCredentials.password); + this._sock.flush(); + this._rfbInitState = "SecurityResult"; return true; } @@ -1765,7 +1766,8 @@ export default class RFB extends EventTargetMixin { "vendor or signature"); } Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); - this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + this._sock.sQpush32(0); // use NOTUNNEL + this._sock.flush(); return false; // wait until we receive the sub auth count to continue } else { return this._fail("Server wanted tunnels, but doesn't support " + @@ -1815,7 +1817,8 @@ export default class RFB extends EventTargetMixin { for (let authType in clientSupportedTypes) { if (serverSupportedTypes.indexOf(authType) != -1) { - this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + this._sock.sQpush32(clientSupportedTypes[authType]); + this._sock.flush(); Log.Debug("Selected authentication type: " + authType); switch (authType) { @@ -1911,9 +1914,10 @@ export default class RFB extends EventTargetMixin { passwordBytes[password.length] = 0; usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes); passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes); - this._sock.send(B); - this._sock.send(usernameBytes); - this._sock.send(passwordBytes); + this._sock.sQpushBytes(B); + this._sock.sQpushBytes(usernameBytes); + this._sock.sQpushBytes(passwordBytes); + this._sock.flush(); this._rfbInitState = "SecurityResult"; return true; } @@ -2141,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; @@ -2887,21 +2892,13 @@ export default class RFB extends EventTargetMixin { // 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(); }, @@ -2915,46 +2912,28 @@ RFB.messages = { return xtScanCode; } - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type + sock.sQpush8(255); // msg-type + sock.sQpush8(0); // sub msg-type - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; + sock.sQpush16(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; - - buff[offset] = 5; // msg-type - - buff[offset + 1] = mask; + sock.sQpush8(5); // msg-type - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; + sock.sQpush8(mask); - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; + sock.sQpush16(x); + sock.sQpush16(y); - sock._sQlen += 6; sock.flush(); }, @@ -3054,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) { @@ -3070,12 +3046,7 @@ 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; + sock.sQpush32(length); // We have to keep track of from where in the data we begin creating the // buffer for the flush in the next iteration. @@ -3085,11 +3056,8 @@ RFB.messages = { 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.sQpushBytes(data.subarray(dataOffset, dataOffset + flushSize)); sock.flush(); remaining -= flushSize; @@ -3099,92 +3067,57 @@ RFB.messages = { }, setDesktopSize(sock, width, height, id, flags) { - const buff = sock._sQ; - const offset = sock._sQlen; + sock.sQpush8(251); // msg-type + + sock.sQpush8(0); // padding - 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.sQpush16(width); + sock.sQpush16(height); - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding + 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._sQlen += 24; + sock.sQpush32(id); + sock.sQpush16(0); // x-position + sock.sQpush16(0); // y-position + sock.sQpush16(width); + sock.sQpush16(height); + sock.sQpush32(flags); + sock.flush(); }, clientFence(sock, flags, payload) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 248; // msg-type + sock.sQpush8(248); // 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] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; + sock.sQpush32(flags); - const n = payload.length; + sock.sQpush8(payload.length); + sock.sQpushString(payload); - 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; - - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag - - 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._sQlen += 10; + sock.sQpush8(150); // msg-type + + sock.sQpush8(enable); + + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(width); + sock.sQpush16(height); + sock.flush(); }, pixelFormat(sock, depth, trueColor) { - const buff = sock._sQ; - const offset = sock._sQlen; - let bpp; if (depth > 16) { @@ -3197,100 +3130,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; - - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; - - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; + sock.sQpush8(3); // msg-type - buff[offset + 6] = (w >> 8) & 0xFF; - buff[offset + 7] = w & 0xFF; + sock.sQpush8(incremental ? 1 : 0); - buff[offset + 8] = (h >> 8) & 0xFF; - buff[offset + 9] = h & 0xFF; + sock.sQpush16(x); + sock.sQpush16(y); + sock.sQpush16(w); + sock.sQpush16(h); - 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 75be044a3..e8e0390c6 100644 --- a/core/websock.js +++ b/core/websock.js @@ -175,6 +175,32 @@ export default class Websock { // Send Queue + sQpush8(num) { + this._sQ[this._sQlen++] = num; + } + + sQpush16(num) { + this._sQ[this._sQlen++] = (num >> 8) & 0xff; + this._sQ[this._sQlen++] = (num >> 0) & 0xff; + } + + sQpush32(num) { + 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) { + this._sQ.set(bytes, this._sQlen); + this._sQlen += bytes.length; + } + flush() { if (this._sQlen > 0 && this.readyState === 'open') { this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen)); @@ -182,16 +208,6 @@ export default class Websock { } } - 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))); - } - // Event Handlers off(evt) { this._eventHandlers[evt] = () => {}; diff --git a/tests/test.websock.js b/tests/test.websock.js index f3cc42247..b7fdd7d67 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -157,6 +157,66 @@ describe('Websock', function () { 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([])); + }); + }); + + 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([])); + }); + }); + + 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([])); + }); + }); + + 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([])); + }); + }); + + 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([])); + }); + }); + describe('flush', function () { it('should actually send on the websocket', function () { sock._sQ = new Uint8Array([1, 2, 3]); @@ -174,20 +234,6 @@ describe('Websock', function () { expect(sock).to.have.sent(new Uint8Array([])); }); }); - - describe('send', function () { - it('should send the given data immediately', function () { - sock.send([1, 2, 3]); - expect(sock).to.have.sent(new Uint8Array([1, 2, 3])); - }); - }); - - describe('sendString', function () { - it('should send after converting the string to an array', function () { - sock.sendString("\x01\x02\x03"); - expect(sock).to.have.sent(new Uint8Array([1, 2, 3])); - }); - }); }); describe('lifecycle methods', function () { From ccef89f55651e083fc2e818a251d09ff0bf9e75a Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 30 May 2023 20:32:31 +0200 Subject: [PATCH 64/65] Implicitly flush Websock if needed Callers shouldn't have to deal with the internal buffering limits of Websock, so implicitly flush the buffer if more room is needed. --- core/rfb.js | 19 +------ core/websock.js | 23 ++++++++- tests/test.websock.js | 116 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 19 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index da95a3866..fb9df0b9c 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -3047,23 +3047,8 @@ RFB.messages = { } sock.sQpush32(length); - - // 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)); - - sock.sQpushBytes(data.subarray(dataOffset, dataOffset + flushSize)); - sock.flush(); - - remaining -= flushSize; - dataOffset += flushSize; - } - + sock.sQpushBytes(data); + sock.flush(); }, setDesktopSize(sock, width, height, id, flags) { diff --git a/core/websock.js b/core/websock.js index e8e0390c6..21327c31a 100644 --- a/core/websock.js +++ b/core/websock.js @@ -176,15 +176,18 @@ 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; @@ -197,8 +200,18 @@ export default class Websock { } sQpushBytes(bytes) { - this._sQ.set(bytes, this._sQlen); - this._sQlen += bytes.length; + 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() { @@ -208,6 +221,12 @@ export default class Websock { } } + _sQensureSpace(bytes) { + if (this._sQbufferSize - this._sQlen < bytes) { + this.flush(); + } + } + // Event Handlers off(evt) { this._eventHandlers[evt] = () => {}; diff --git a/tests/test.websock.js b/tests/test.websock.js index b7fdd7d67..dc361b749 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -150,6 +150,8 @@ describe('Websock', function () { describe('Send queue methods', function () { let sock; + const bufferSize = 10 * 1024; + beforeEach(function () { let websock = new FakeWebSocket(); websock._open(); @@ -167,6 +169,18 @@ describe('Websock', 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 () { @@ -179,6 +193,19 @@ describe('Websock', 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 () { @@ -191,6 +218,21 @@ describe('Websock', 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 () { @@ -203,6 +245,41 @@ describe('Websock', 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 () { @@ -215,6 +292,45 @@ describe('Websock', 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 () { From a30f609de48227df08f633bc0443e16678b20d32 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Wed, 2 Nov 2022 10:23:36 +0100 Subject: [PATCH 65/65] Don't crash if we can't use localStorage Our settings are not a fatal requirement, we can fall back on the default values if they can't be accessed. A scenario where we've seen this happen is when cookies are disabled in the browser. It seems localStorage is disabled along with cookies in these settings. So, lets log a message about the failure and otherwise silently continue in this case. Fixes issue #1577. --- app/webutil.js | 74 ++++++++++++++++++++++++++++++++++++++++--- tests/test.webutil.js | 15 +++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/app/webutil.js b/app/webutil.js index b94f035d3..6011442cb 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); } } @@ -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 { + 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/tests/test.webutil.js b/tests/test.webutil.js index 6f460a3fc..df8227aef 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -92,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 () { @@ -137,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 @@ -145,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; + }); }); });