Merge branch 'novnc:master' into patch-1

This commit is contained in:
tutacat 2023-08-17 01:27:08 +01:00 committed by GitHub
commit 4311930796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 3149 additions and 2212 deletions

View File

@ -1,21 +1,22 @@
{ {
"HTTPS is required for full functionality": "",
"Connecting...": "En cours de connexion...", "Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...", "Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...", "Reconnecting...": "Reconnexion en cours...",
"Internal error": "Erreur interne", "Internal error": "Erreur interne",
"Must set host": "Doit définir l'hôte", "Must set host": "Doit définir l'hôte",
"Connected (encrypted) to ": "Connecté (crypté) à ", "Connected (encrypted) to ": "Connecté (chiffré) à ",
"Connected (unencrypted) to ": "Connecté (non crypté) à ", "Connected (unencrypted) to ": "Connecté (non chiffré) à ",
"Something went wrong, connection is closed": "Quelque chose est arrivé, la connexion est fermée", "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", "Failed to connect to server": "Échec de connexion au serveur",
"Disconnected": "Déconnecté", "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", "New connection has been rejected": "Une nouvelle connexion a été rejetée",
"Credentials are required": "Les identifiants sont requis", "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", "Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
"Drag": "Faire glisser", "Drag": "Faire glisser",
"Move/Drag Viewport": "Déplacer/faire glisser Viewport", "Move/Drag Viewport": "Déplacer/faire glisser le Viewport",
"Keyboard": "Clavier", "Keyboard": "Clavier",
"Show Keyboard": "Afficher le clavier", "Show Keyboard": "Afficher le clavier",
"Extra keys": "Touches supplémentaires", "Extra keys": "Touches supplémentaires",
@ -39,8 +40,7 @@
"Reboot": "Redémarrer", "Reboot": "Redémarrer",
"Reset": "Réinitialiser", "Reset": "Réinitialiser",
"Clipboard": "Presse-papiers", "Clipboard": "Presse-papiers",
"Clear": "Effacer", "Edit clipboard content in the textarea below.": "",
"Fullscreen": "Plein écran",
"Settings": "Paramètres", "Settings": "Paramètres",
"Shared Mode": "Mode partagé", "Shared Mode": "Mode partagé",
"View Only": "Afficher uniquement", "View Only": "Afficher uniquement",
@ -54,7 +54,7 @@
"Compression level:": "Niveau de compression :", "Compression level:": "Niveau de compression :",
"Repeater ID:": "ID Répéteur :", "Repeater ID:": "ID Répéteur :",
"WebSocket": "WebSocket", "WebSocket": "WebSocket",
"Encrypt": "Crypter", "Encrypt": "Chiffrer",
"Host:": "Hôte :", "Host:": "Hôte :",
"Port:": "Port :", "Port:": "Port :",
"Path:": "Chemin :", "Path:": "Chemin :",
@ -65,6 +65,12 @@
"Version:": "Version :", "Version:": "Version :",
"Disconnect": "Déconnecter", "Disconnect": "Déconnecter",
"Connect": "Connecter", "Connect": "Connecter",
"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 :", "Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :", "Password:": "Mot de passe :",
"Send Credentials": "Envoyer les identifiants", "Send Credentials": "Envoyer les identifiants",

View File

@ -1,4 +1,5 @@
{ {
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Connecting...": "Ansluter...", "Connecting...": "Ansluter...",
"Disconnecting...": "Kopplar ner...", "Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...", "Reconnecting...": "Återansluter...",
@ -39,8 +40,8 @@
"Reboot": "Boota om", "Reboot": "Boota om",
"Reset": "Återställ", "Reset": "Återställ",
"Clipboard": "Urklipp", "Clipboard": "Urklipp",
"Clear": "Rensa", "Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
"Fullscreen": "Fullskärm", "Full Screen": "Fullskärm",
"Settings": "Inställningar", "Settings": "Inställningar",
"Shared Mode": "Delat Läge", "Shared Mode": "Delat Läge",
"View Only": "Endast Visning", "View Only": "Endast Visning",
@ -65,6 +66,13 @@
"Version:": "Version:", "Version:": "Version:",
"Disconnect": "Koppla från", "Disconnect": "Koppla från",
"Connect": "Anslut", "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:", "Username:": "Användarnamn:",
"Password:": "Lösenord:", "Password:": "Lösenord:",
"Send Credentials": "Skicka Användaruppgifter", "Send Credentials": "Skicka Användaruppgifter",

View File

@ -16,13 +16,19 @@ export class Localizer {
this.language = 'en'; this.language = 'en';
// Current dictionary of translations // Current dictionary of translations
this.dictionary = undefined; this._dictionary = undefined;
} }
// Configure suitable language based on user preferences // Configure suitable language based on user preferences
setup(supportedLanguages) { async setup(supportedLanguages, baseURL) {
this.language = 'en'; // Default: US English 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+) * Navigator.languages only available in Chrome (32+) and FireFox (32+)
* Fall back to navigator.language for other browsers * Fall back to navigator.language for other browsers
@ -40,12 +46,6 @@ export class Localizer {
.replace("_", "-") .replace("_", "-")
.split("-"); .split("-");
// Built-in default?
if ((userLang[0] === 'en') &&
((userLang[1] === undefined) || (userLang[1] === 'us'))) {
return;
}
// First pass: perfect match // First pass: perfect match
for (let j = 0; j < supportedLanguages.length; j++) { for (let j = 0; j < supportedLanguages.length; j++) {
const supLang = supportedLanguages[j] const supLang = supportedLanguages[j]
@ -64,7 +64,12 @@ export class Localizer {
return; 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++) { for (let j = 0;j < supportedLanguages.length;j++) {
const supLang = supportedLanguages[j] const supLang = supportedLanguages[j]
.toLowerCase() .toLowerCase()
@ -84,10 +89,32 @@ export class Localizer {
} }
} }
async _setupDictionary(baseURL) {
if (baseURL) {
if (!baseURL.endsWith("/")) {
baseURL = baseURL + "/";
}
} else {
baseURL = "";
}
if (this.language === "en") {
return;
}
let response = await fetch(baseURL + this.language + ".json");
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
this._dictionary = await response.json();
}
// Retrieve localised text // Retrieve localised text
get(id) { get(id) {
if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { if (typeof this._dictionary !== 'undefined' &&
return this.dictionary[id]; this._dictionary[id]) {
return this._dictionary[id];
} else { } else {
return id; return id;
} }

View File

@ -661,7 +661,7 @@ html {
justify-content: center; justify-content: center;
align-content: center; align-content: center;
line-height: 25px; line-height: 1.6;
word-wrap: break-word; word-wrap: break-word;
color: #fff; color: #fff;
@ -887,7 +887,7 @@ html {
.noVNC_logo { .noVNC_logo {
color:yellow; color:yellow;
font-family: 'Orbitron', 'OrbitronTTF', sans-serif; font-family: 'Orbitron', 'OrbitronTTF', sans-serif;
line-height:90%; line-height: 0.9;
text-shadow: 0.1em 0.1em 0 black; text-shadow: 0.1em 0.1em 0 black;
} }
.noVNC_logo span{ .noVNC_logo span{

View File

@ -86,13 +86,16 @@ option {
* Checkboxes * Checkboxes
*/ */
input[type=checkbox] { input[type=checkbox] {
position: relative; display: inline-flex;
justify-content: center;
align-items: center;
background-color: white; background-color: white;
background-image: unset; background-image: unset;
border: 1px solid dimgrey; border: 1px solid dimgrey;
border-radius: 3px; border-radius: 3px;
width: 13px; width: 13px;
height: 13px; height: 13px;
padding: 0;
margin-right: 6px; margin-right: 6px;
vertical-align: bottom; vertical-align: bottom;
transition: 0.2s background-color linear; transition: 0.2s background-color linear;
@ -103,14 +106,12 @@ input[type=checkbox]:checked {
} }
input[type=checkbox]:checked::after { input[type=checkbox]:checked::after {
content: ""; content: "";
position: absolute; display: block; /* width & height doesn't work on inline elements */
top: 0;
left: 3px;
width: 3px; width: 3px;
height: 7px; height: 7px;
border: 1px solid white; border: 1px solid white;
border-width: 0 2px 2px 0; border-width: 0 2px 2px 0;
transform: rotate(40deg); transform: rotate(40deg) translateY(-1px);
} }
/* /*

View File

@ -1763,20 +1763,8 @@ const UI = {
// Set up translations // 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", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS); l10n.setup(LINGUAS, "app/locale/")
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)) .catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime); .then(UI.prime);
}
export default UI; export default UI;

View File

@ -6,16 +6,16 @@
* See README.md for usage and integration instructions. * 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 // init log level reading the logging HTTP param
export function initLogging(level) { export function initLogging(level) {
"use strict"; "use strict";
if (typeof level !== "undefined") { if (typeof level !== "undefined") {
mainInitLogging(level); Log.initLogging(level);
} else { } else {
const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/);
mainInitLogging(param || undefined); Log.initLogging(param || undefined);
} }
} }
@ -25,14 +25,14 @@ export function initLogging(level) {
// //
// For privacy (Using a hastag #, the parameters will not be sent to the server) // For privacy (Using a hastag #, the parameters will not be sent to the server)
// the url can be requested in the following way: // 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: // 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) { export function getQueryVar(name, defVal) {
"use strict"; "use strict";
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), 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 (typeof defVal === 'undefined') { defVal = null; }
if (match) { if (match) {
@ -146,7 +146,7 @@ export function writeSetting(name, value) {
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.set(settings); window.chrome.storage.sync.set(settings);
} else { } 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)) { if ((name in settings) || (window.chrome && window.chrome.storage)) {
value = settings[name]; value = settings[name];
} else { } else {
value = localStorage.getItem(name); value = localStorageGet(name);
settings[name] = value; settings[name] = value;
} }
if (typeof value === "undefined") { if (typeof value === "undefined") {
@ -181,6 +181,70 @@ export function eraseSetting(name) {
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name); window.chrome.storage.sync.remove(name);
} else { } 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); localStorage.removeItem(name);
} catch (e) {
if (e instanceof DOMException) {
logOnce(cookiesMsg);
logOnce("'localStorage.removeItem(" + name + ")' failed: " + e,
"debug");
} else {
throw e;
}
} }
} }

178
core/crypto/aes.js Normal file
View File

@ -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;
}
}

34
core/crypto/bigint.js Normal file
View File

@ -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);
}

90
core/crypto/crypto.js Normal file
View File

@ -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;

View File

@ -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 */ /* eslint-enable comma-spacing */
export default class DES { class DES {
constructor(password) { constructor(password) {
this.keys = []; this.keys = [];
@ -258,9 +258,73 @@ export default class DES {
} }
return b; return b;
} }
}
// Encrypt 16 bytes of text using passwd as key export class DESECBCipher {
encrypt(t) { constructor() {
return this.enc8(t.slice(0, 8)).concat(this.enc8(t.slice(8, 16))); this._cipher = null;
}
get algorithm() {
return { name: "DES-ECB" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESECBCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key, _extractable, _keyUsages) {
this._cipher = new DES(key);
}
encrypt(_algorithm, plaintext) {
const x = new Uint8Array(plaintext);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
}
return x;
}
}
export class DESCBCCipher {
constructor() {
this._cipher = null;
}
get algorithm() {
return { name: "DES-CBC" };
}
static importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new DESCBCCipher;
cipher._importKey(key);
return cipher;
}
_importKey(key) {
this._cipher = new DES(key);
}
encrypt(algorithm, plaintext) {
const x = new Uint8Array(plaintext);
let y = new Uint8Array(algorithm.iv);
if (x.length % 8 !== 0 || this._cipher === null) {
return null;
}
const n = x.length / 8;
for (let i = 0; i < n; i++) {
for (let j = 0; j < 8; j++) {
y[j] ^= plaintext[i * 8 + j];
}
y = this._cipher.enc8(y);
x.set(y, i * 8);
}
return x;
} }
} }

55
core/crypto/dh.js Normal file
View File

@ -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);
}
}

View File

@ -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) { export async function MD5(d) {
let r = M(V(Y(X(d), 8 * d.length))); let s = "";
return r; 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) { function M(d) {

132
core/crypto/rsa.js Normal file
View File

@ -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 };
}
}

View File

@ -31,10 +31,7 @@ export default class HextileDecoder {
return false; return false;
} }
let rQ = sock.rQ; let subencoding = sock.rQpeek8();
let rQi = sock.rQi;
let subencoding = rQ[rQi]; // Peek
if (subencoding > 30) { // Raw if (subencoding > 30) { // Raw
throw new Error("Illegal hextile subencoding (subencoding: " + throw new Error("Illegal hextile subencoding (subencoding: " +
subencoding + ")"); subencoding + ")");
@ -65,7 +62,7 @@ export default class HextileDecoder {
return false; return false;
} }
let subrects = rQ[rQi + bytes - 1]; // Peek let subrects = sock.rQpeekBytes(bytes).at(-1);
if (subencoding & 0x10) { // SubrectsColoured if (subencoding & 0x10) { // SubrectsColoured
bytes += subrects * (4 + 2); bytes += subrects * (4 + 2);
} else { } else {
@ -79,7 +76,7 @@ export default class HextileDecoder {
} }
// We know the encoding and have a whole tile // We know the encoding and have a whole tile
rQi++; sock.rQshift8();
if (subencoding === 0) { if (subencoding === 0) {
if (this._lastsubencoding & 0x01) { if (this._lastsubencoding & 0x01) {
// Weird: ignore blanks are RAW // Weird: ignore blanks are RAW
@ -89,42 +86,36 @@ export default class HextileDecoder {
} }
} else if (subencoding & 0x01) { // Raw } else if (subencoding & 0x01) { // Raw
let pixels = tw * th; let pixels = tw * th;
let data = sock.rQshiftBytes(pixels * 4, false);
// Max sure the image is fully opaque // Max sure the image is fully opaque
for (let i = 0;i < pixels;i++) { 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); display.blitImage(tx, ty, tw, th, data, 0);
rQi += bytes - 1;
} else { } else {
if (subencoding & 0x02) { // Background if (subencoding & 0x02) { // Background
this._background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; this._background = new Uint8Array(sock.rQshiftBytes(4));
rQi += 4;
} }
if (subencoding & 0x04) { // Foreground if (subencoding & 0x04) { // Foreground
this._foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; this._foreground = new Uint8Array(sock.rQshiftBytes(4));
rQi += 4;
} }
this._startTile(tx, ty, tw, th, this._background); this._startTile(tx, ty, tw, th, this._background);
if (subencoding & 0x08) { // AnySubrects if (subencoding & 0x08) { // AnySubrects
let subrects = rQ[rQi]; let subrects = sock.rQshift8();
rQi++;
for (let s = 0; s < subrects; s++) { for (let s = 0; s < subrects; s++) {
let color; let color;
if (subencoding & 0x10) { // SubrectsColoured if (subencoding & 0x10) { // SubrectsColoured
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; color = sock.rQshiftBytes(4);
rQi += 4;
} else { } else {
color = this._foreground; color = this._foreground;
} }
const xy = rQ[rQi]; const xy = sock.rQshift8();
rQi++;
const sx = (xy >> 4); const sx = (xy >> 4);
const sy = (xy & 0x0f); const sy = (xy & 0x0f);
const wh = rQ[rQi]; const wh = sock.rQshift8();
rQi++;
const sw = (wh >> 4) + 1; const sw = (wh >> 4) + 1;
const sh = (wh & 0x0f) + 1; const sh = (wh & 0x0f) + 1;
@ -133,7 +124,6 @@ export default class HextileDecoder {
} }
this._finishTile(display); this._finishTile(display);
} }
sock.rQi = rQi;
this._lastsubencoding = subencoding; this._lastsubencoding = subencoding;
this._tiles--; this._tiles--;
} }

View File

@ -11,131 +11,136 @@ export default class JPEGDecoder {
constructor() { constructor() {
// RealVNC will reuse the quantization tables // RealVNC will reuse the quantization tables
// and Huffman tables, so we need to cache them. // and Huffman tables, so we need to cache them.
this._quantTables = [];
this._huffmanTables = [];
this._cachedQuantTables = []; this._cachedQuantTables = [];
this._cachedHuffmanTables = []; this._cachedHuffmanTables = [];
this._jpegLength = 0;
this._segments = []; this._segments = [];
} }
decodeRect(x, y, width, height, sock, display, depth) { decodeRect(x, y, width, height, sock, display, depth) {
// A rect of JPEG encodings is simply a JPEG file // A rect of JPEG encodings is simply a JPEG file
if (!this._parseJPEG(sock.rQslice(0))) { while (true) {
let segment = this._readSegment(sock);
if (segment === null) {
return false; return false;
} }
const data = sock.rQshiftBytes(this._jpegLength); this._segments.push(segment);
if (this._quantTables.length != 0 && this._huffmanTables.length != 0) { // End of image?
// If there are quantization tables and Huffman tables in the JPEG if (segment[1] === 0xD9) {
// image, we can directly render it. break;
display.imageRect(x, y, width, height, "image/jpeg", data); }
return true; }
} else {
// Otherwise we need to insert cached tables. let huffmanTables = [];
let quantTables = [];
for (let segment of this._segments) {
let type = segment[1];
if (type === 0xC4) {
// Huffman tables
huffmanTables.push(segment);
} else if (type === 0xDB) {
// Quantization tables
quantTables.push(segment);
}
}
const sofIndex = this._segments.findIndex( const sofIndex = this._segments.findIndex(
x => x[1] == 0xC0 || x[1] == 0xC2 x => x[1] == 0xC0 || x[1] == 0xC2
); );
if (sofIndex == -1) { if (sofIndex == -1) {
throw new Error("Illegal JPEG image without SOF"); throw new Error("Illegal JPEG image without SOF");
} }
let segments = this._segments.slice(0, sofIndex);
segments = segments.concat(this._quantTables.length ? if (quantTables.length === 0) {
this._quantTables : this._segments.splice(sofIndex+1, 0,
this._cachedQuantTables); ...this._cachedQuantTables);
segments.push(this._segments[sofIndex]);
segments = segments.concat(this._huffmanTables.length ?
this._huffmanTables :
this._cachedHuffmanTables,
this._segments.slice(sofIndex + 1));
let length = 0;
for (let i = 0; i < segments.length; i++) {
length += segments[i].length;
}
const data = new Uint8Array(length);
length = 0;
for (let i = 0; i < segments.length; i++) {
data.set(segments[i], length);
length += segments[i].length;
}
display.imageRect(x, y, width, height, "image/jpeg", data);
return true;
} }
if (huffmanTables.length === 0) {
this._segments.splice(sofIndex+1, 0,
...this._cachedHuffmanTables);
} }
_parseJPEG(buffer) { let length = 0;
if (this._quantTables.length != 0) { for (let segment of this._segments) {
this._cachedQuantTables = this._quantTables; length += segment.length;
} }
if (this._huffmanTables.length != 0) {
this._cachedHuffmanTables = this._huffmanTables; let data = new Uint8Array(length);
length = 0;
for (let segment of this._segments) {
data.set(segment, length);
length += segment.length;
} }
this._quantTables = [];
this._huffmanTables = []; display.imageRect(x, y, width, height, "image/jpeg", data);
if (huffmanTables.length !== 0) {
this._cachedHuffmanTables = huffmanTables;
}
if (quantTables.length !== 0) {
this._cachedQuantTables = quantTables;
}
this._segments = []; 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; 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;
} }
_readSegment(sock) {
if (sock.rQwait("JPEG", 2)) {
return null;
} }
if (!hasFoundEndOfScan) {
return false; let marker = sock.rQshift8();
if (marker != 0xFF) {
throw new Error("Illegal JPEG marker received (byte: " +
marker + ")");
} }
this._segments.push(buffer.slice(i, j)); let type = sock.rQshift8();
i = j; if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
continue;
} else if (type >= 0xD0 && type < 0xD9 || type == 0x01) {
// No length after marker // No length after marker
this._segments.push(buffer.slice(i, j)); return new Uint8Array([marker, type]);
i = j;
continue;
} }
if (j + 2 > bufferLength) {
return false; if (sock.rQwait("JPEG", 2, 2)) {
return null;
} }
const length = (buffer[j] << 8) + buffer[j+1] - 2;
if (length < 0) { let length = sock.rQshift16();
if (length < 2) {
throw new Error("Illegal JPEG length received (length: " + throw new Error("Illegal JPEG length received (length: " +
length + ")"); length + ")");
} }
j += 2;
if (j + length > bufferLength) { if (sock.rQwait("JPEG", length-2, 4)) {
return false; return null;
} }
j += length;
const segment = buffer.slice(i, j); let extra = 0;
if (type == 0xC4) { if (type === 0xDA) {
// Huffman tables // start of scan
this._huffmanTables.push(segment); extra += 2;
} else if (type == 0xDB) { while (true) {
// Quantization tables if (sock.rQwait("JPEG", length-2+extra, 4)) {
this._quantTables.push(segment); return null;
} }
this._segments.push(segment); let data = sock.rQpeekBytes(length-2+extra, false);
i = j; if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
extra -= 2;
break;
}
extra++;
} }
} }
let segment = new Uint8Array(2 + length + extra);
segment[0] = marker;
segment[1] = type;
segment[2] = length >> 8;
segment[3] = length;
segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
return segment;
}
} }

View File

@ -24,41 +24,34 @@ export default class RawDecoder {
const pixelSize = depth == 8 ? 1 : 4; const pixelSize = depth == 8 ? 1 : 4;
const bytesPerLine = width * pixelSize; const bytesPerLine = width * pixelSize;
while (this._lines > 0) {
if (sock.rQwait("RAW", bytesPerLine)) { if (sock.rQwait("RAW", bytesPerLine)) {
return false; return false;
} }
const curY = y + (height - this._lines); const curY = y + (height - this._lines);
const currHeight = Math.min(this._lines,
Math.floor(sock.rQlen / bytesPerLine));
const pixels = width * currHeight;
let data = sock.rQ; let data = sock.rQshiftBytes(bytesPerLine, false);
let index = sock.rQi;
// Convert data if needed // Convert data if needed
if (depth == 8) { if (depth == 8) {
const newdata = new Uint8Array(pixels * 4); const newdata = new Uint8Array(width * 4);
for (let i = 0; i < pixels; i++) { for (let i = 0; i < width; i++) {
newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
newdata[i * 4 + 3] = 255; newdata[i * 4 + 3] = 255;
} }
data = newdata; data = newdata;
index = 0;
} }
// Max sure the image is fully opaque // Max sure the image is fully opaque
for (let i = 0; i < pixels; i++) { for (let i = 0; i < width; i++) {
data[index + i * 4 + 3] = 255; data[i * 4 + 3] = 255;
} }
display.blitImage(x, curY, width, currHeight, data, index); display.blitImage(x, curY, width, 1, data, 0);
sock.rQskipBytes(currHeight * bytesPerLine); this._lines--;
this._lines -= currHeight;
if (this._lines > 0) {
return false;
} }
return true; return true;

View File

@ -76,12 +76,8 @@ export default class TightDecoder {
return false; return false;
} }
const rQi = sock.rQi; let pixel = sock.rQshiftBytes(3);
const rQ = sock.rQ; display.fillRect(x, y, width, height, pixel, false);
display.fillRect(x, y, width, height,
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
sock.rQskipBytes(3);
return true; return true;
} }
@ -316,7 +312,7 @@ export default class TightDecoder {
return null; return null;
} }
let data = sock.rQshiftBytes(this._len); let data = sock.rQshiftBytes(this._len, false);
this._len = 0; this._len = 0;
return data; return data;

View File

@ -32,7 +32,7 @@ export default class ZRLEDecoder {
return false; return false;
} }
const data = sock.rQshiftBytes(this._length); const data = sock.rQshiftBytes(this._length, false);
this._inflator.setInput(data); this._inflator.setInput(data);

View File

@ -15,7 +15,7 @@ export default class Display {
this._drawCtx = null; this._drawCtx = null;
this._renderQ = []; // queue drawing actions for in-oder rendering this._renderQ = []; // queue drawing actions for in-oder rendering
this._flushing = false; this._flushPromise = null;
// the full frame buffer (logical canvas) size // the full frame buffer (logical canvas) size
this._fbWidth = 0; this._fbWidth = 0;
@ -61,10 +61,6 @@ export default class Display {
this._scale = 1.0; this._scale = 1.0;
this._clipViewport = false; this._clipViewport = false;
// ===== EVENT HANDLERS =====
this.onflush = () => {}; // A flush request has finished
} }
// ===== PROPERTIES ===== // ===== PROPERTIES =====
@ -306,9 +302,14 @@ export default class Display {
flush() { flush() {
if (this._renderQ.length === 0) { if (this._renderQ.length === 0) {
this.onflush(); return Promise.resolve();
} else { } 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) { if (this._renderQ.length === 0 &&
this._flushing = false; this._flushPromise !== null) {
this.onflush(); this._flushResolve();
this._flushPromise = null;
this._flushResolve = null;
} }
} }
} }

View File

@ -1,146 +1,25 @@
import Base64 from './base64.js';
import { encodeUTF8 } from './util/strings.js'; import { encodeUTF8 } from './util/strings.js';
import EventTargetMixin from './util/eventtarget.js'; import EventTargetMixin from './util/eventtarget.js';
import legacyCrypto from './crypto/crypto.js';
export class AESEAXCipher { class RA2Cipher {
constructor() { constructor() {
this._rawKey = null; this._cipher = 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._counter = new Uint8Array(16); this._counter = new Uint8Array(16);
} }
async setKey(key) { async setKey(key) {
await this._cipher.setKey(key); this._cipher = await legacyCrypto.importKey(
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
} }
async makeMessage(message) { async makeMessage(message) {
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]); 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++); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
const res = new Uint8Array(message.length + 2 + 16); const res = new Uint8Array(message.length + 2 + 16);
res.set(ad); res.set(ad);
@ -148,164 +27,18 @@ export class RA2Cipher {
return res; return res;
} }
async receiveMessage(length, encrypted, mac) { async receiveMessage(length, encrypted) {
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]); 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++); for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
return res; 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 { export default class RSAAESAuthenticationState extends EventTargetMixin {
constructor(sock, getCredentials) { constructor(sock, getCredentials) {
super(); super();
@ -406,7 +139,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
this._hasStarted = true; this._hasStarted = true;
// 1: Receive server public key // 1: Receive server public key
await this._waitSockAsync(4); await this._waitSockAsync(4);
const serverKeyLengthBuffer = this._sock.rQslice(0, 4); const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
const serverKeyLength = this._sock.rQshift32(); const serverKeyLength = this._sock.rQshift32();
if (serverKeyLength < 1024) { if (serverKeyLength < 1024) {
throw new Error("RA2: server public key is too short: " + serverKeyLength); throw new Error("RA2: server public key is too short: " + serverKeyLength);
@ -417,26 +150,31 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
await this._waitSockAsync(serverKeyBytes * 2); await this._waitSockAsync(serverKeyBytes * 2);
const serverN = this._sock.rQshiftBytes(serverKeyBytes); const serverN = this._sock.rQshiftBytes(serverKeyBytes);
const serverE = this._sock.rQshiftBytes(serverKeyBytes); const serverE = this._sock.rQshiftBytes(serverKeyBytes);
const serverRSACipher = new RSACipher(serverKeyLength); const serverRSACipher = await legacyCrypto.importKey(
serverRSACipher.setPublicKey(serverN, serverE); "raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2); const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
serverPublickey.set(serverKeyLengthBuffer); serverPublickey.set(serverKeyLengthBuffer);
serverPublickey.set(serverN, 4); serverPublickey.set(serverN, 4);
serverPublickey.set(serverE, 4 + serverKeyBytes); serverPublickey.set(serverE, 4 + serverKeyBytes);
// verify server public key // verify server public key
let approveKey = this._waitApproveKeyAsync();
this.dispatchEvent(new CustomEvent("serververification", { this.dispatchEvent(new CustomEvent("serververification", {
detail: { type: "RSA", publickey: serverPublickey } detail: { type: "RSA", publickey: serverPublickey }
})); }));
await this._waitApproveKeyAsync(); await approveKey;
// 2: Send client public key // 2: Send client public key
const clientKeyLength = 2048; const clientKeyLength = 2048;
const clientKeyBytes = Math.ceil(clientKeyLength / 8); const clientKeyBytes = Math.ceil(clientKeyLength / 8);
const clientRSACipher = new RSACipher(clientKeyLength); const clientRSACipher = (await legacyCrypto.generateKey({
await clientRSACipher.generateKey(); name: "RSA-PKCS1-v1_5",
const clientN = clientRSACipher.n; modulusLength: clientKeyLength,
const clientE = clientRSACipher.e; 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); const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24; clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16; clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
@ -444,17 +182,20 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientPublicKey[3] = clientKeyLength & 0xff; clientPublicKey[3] = clientKeyLength & 0xff;
clientPublicKey.set(clientN, 4); clientPublicKey.set(clientN, 4);
clientPublicKey.set(clientE, 4 + clientKeyBytes); clientPublicKey.set(clientE, 4 + clientKeyBytes);
this._sock.send(clientPublicKey); this._sock.sQpushBytes(clientPublicKey);
this._sock.flush();
// 3: Send client random // 3: Send client random
const clientRandom = new Uint8Array(16); const clientRandom = new Uint8Array(16);
window.crypto.getRandomValues(clientRandom); 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); const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8; clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
clientRandomMessage[1] = serverKeyBytes & 0xff; clientRandomMessage[1] = serverKeyBytes & 0xff;
clientRandomMessage.set(clientEncryptedRandom, 2); clientRandomMessage.set(clientEncryptedRandom, 2);
this._sock.send(clientRandomMessage); this._sock.sQpushBytes(clientRandomMessage);
this._sock.flush();
// 4: Receive server random // 4: Receive server random
await this._waitSockAsync(2); await this._waitSockAsync(2);
@ -462,7 +203,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong encrypted message length"); throw new Error("RA2: wrong encrypted message length");
} }
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes); 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) { if (serverRandom === null || serverRandom.length !== 16) {
throw new Error("RA2: corrupted server encrypted random"); throw new Error("RA2: corrupted server encrypted random");
} }
@ -494,13 +236,14 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash); clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
serverHash = new Uint8Array(serverHash); serverHash = new Uint8Array(serverHash);
clientHash = new Uint8Array(clientHash); 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); await this._waitSockAsync(2 + 20 + 16);
if (this._sock.rQshift16() !== 20) { if (this._sock.rQshift16() !== 20) {
throw new Error("RA2: wrong server hash"); throw new Error("RA2: wrong server hash");
} }
const serverHashReceived = await serverCipher.receiveMessage( const serverHashReceived = await serverCipher.receiveMessage(
20, this._sock.rQshiftBytes(20), this._sock.rQshiftBytes(16)); 20, this._sock.rQshiftBytes(20 + 16));
if (serverHashReceived === null) { if (serverHashReceived === null) {
throw new Error("RA2: failed to authenticate the message"); throw new Error("RA2: failed to authenticate the message");
} }
@ -516,11 +259,12 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
throw new Error("RA2: wrong subtype"); throw new Error("RA2: wrong subtype");
} }
let subtype = (await serverCipher.receiveMessage( let subtype = (await serverCipher.receiveMessage(
1, this._sock.rQshiftBytes(1), this._sock.rQshiftBytes(16))); 1, this._sock.rQshiftBytes(1 + 16)));
if (subtype === null) { if (subtype === null) {
throw new Error("RA2: failed to authenticate the message"); throw new Error("RA2: failed to authenticate the message");
} }
subtype = subtype[0]; subtype = subtype[0];
let waitCredentials = this._waitCredentialsAsync(subtype);
if (subtype === 1) { if (subtype === 1) {
if (this._getCredentials().username === undefined || if (this._getCredentials().username === undefined ||
this._getCredentials().password === undefined) { this._getCredentials().password === undefined) {
@ -537,7 +281,7 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
} else { } else {
throw new Error("RA2: wrong subtype"); throw new Error("RA2: wrong subtype");
} }
await this._waitCredentialsAsync(subtype); await waitCredentials;
let username; let username;
if (subtype === 1) { if (subtype === 1) {
username = encodeUTF8(this._getCredentials().username).slice(0, 255); username = encodeUTF8(this._getCredentials().username).slice(0, 255);
@ -554,7 +298,8 @@ export default class RSAAESAuthenticationState extends EventTargetMixin {
for (let i = 0; i < password.length; i++) { for (let i = 0; i < password.length; i++) {
credentials[username.length + 2 + i] = password.charCodeAt(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() { get hasStarted() {

View File

@ -21,12 +21,11 @@ import Keyboard from "./input/keyboard.js";
import GestureHandler from "./input/gesturehandler.js"; import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js"; import Cursor from "./util/cursor.js";
import Websock from "./websock.js"; import Websock from "./websock.js";
import DES from "./des.js";
import KeyTable from "./input/keysym.js"; import KeyTable from "./input/keysym.js";
import XtScancode from "./input/xtscancodes.js"; import XtScancode from "./input/xtscancodes.js";
import { encodings } from "./encodings.js"; import { encodings } from "./encodings.js";
import RSAAESAuthenticationState from "./ra2.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 RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js"; import CopyRectDecoder from "./decoders/copyrect.js";
@ -258,7 +257,6 @@ export default class RFB extends EventTargetMixin {
Log.Error("Display exception: " + exc); Log.Error("Display exception: " + exc);
throw exc; throw exc;
} }
this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas); this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
@ -960,7 +958,7 @@ export default class RFB extends EventTargetMixin {
} }
_handleMessage() { _handleMessage() {
if (this._sock.rQlen === 0) { if (this._sock.rQwait("message", 1)) {
Log.Warn("handleMessage called on an empty receive queue"); Log.Warn("handleMessage called on an empty receive queue");
return; return;
} }
@ -977,7 +975,7 @@ export default class RFB extends EventTargetMixin {
if (!this._normalMsg()) { if (!this._normalMsg()) {
break; break;
} }
if (this._sock.rQlen === 0) { if (this._sock.rQwait("message", 1)) {
break; break;
} }
} }
@ -1383,7 +1381,8 @@ export default class RFB extends EventTargetMixin {
while (repeaterID.length < 250) { while (repeaterID.length < 250) {
repeaterID += "\0"; repeaterID += "\0";
} }
this._sock.sendString(repeaterID); this._sock.sQpushString(repeaterID);
this._sock.flush();
return true; return true;
} }
@ -1393,7 +1392,8 @@ export default class RFB extends EventTargetMixin {
const cversion = "00" + parseInt(this._rfbVersion, 10) + const cversion = "00" + parseInt(this._rfbVersion, 10) +
".00" + ((this._rfbVersion * 10) % 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); Log.Debug('Sent ProtocolVersion: ' + cversion);
this._rfbInitState = 'Security'; this._rfbInitState = 'Security';
@ -1445,7 +1445,8 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + types + ")"); return this._fail("Unsupported security types (types: " + types + ")");
} }
this._sock.send([this._rfbAuthScheme]); this._sock.sQpush8(this._rfbAuthScheme);
this._sock.flush();
} else { } else {
// Server decides // Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; } if (this._sock.rQwait("security scheme", 4)) { return false; }
@ -1507,12 +1508,15 @@ export default class RFB extends EventTargetMixin {
return false; return false;
} }
const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) + this._sock.sQpush8(this._rfbCredentials.username.length);
String.fromCharCode(this._rfbCredentials.target.length) + this._sock.sQpush8(this._rfbCredentials.target.length);
this._rfbCredentials.username + this._sock.sQpushString(this._rfbCredentials.username);
this._rfbCredentials.target; this._sock.sQpushString(this._rfbCredentials.target);
this._sock.sendString(xvpAuthStr);
this._sock.flush();
this._rfbAuthScheme = securityTypeVNCAuth; this._rfbAuthScheme = securityTypeVNCAuth;
return this._negotiateAuthentication(); return this._negotiateAuthentication();
} }
@ -1530,7 +1534,9 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported VeNCrypt version " + major + "." + minor); 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; this._rfbVeNCryptState = 1;
} }
@ -1589,12 +1595,10 @@ export default class RFB extends EventTargetMixin {
return this._fail("Unsupported security types (types: " + subtypes + ")"); return this._fail("Unsupported security types (types: " + subtypes + ")");
} }
this._sock.send([this._rfbAuthScheme >> 24, this._sock.sQpush32(this._rfbAuthScheme);
this._rfbAuthScheme >> 16, this._sock.flush();
this._rfbAuthScheme >> 8,
this._rfbAuthScheme]);
this._rfbVeNCryptState == 4; this._rfbVeNCryptState = 4;
return true; return true;
} }
} }
@ -1611,20 +1615,11 @@ export default class RFB extends EventTargetMixin {
const user = encodeUTF8(this._rfbCredentials.username); const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password); const pass = encodeUTF8(this._rfbCredentials.password);
this._sock.send([ this._sock.sQpush32(user.length);
(user.length >> 24) & 0xFF, this._sock.sQpush32(pass.length);
(user.length >> 16) & 0xFF, this._sock.sQpushString(user);
(user.length >> 8) & 0xFF, this._sock.sQpushString(pass);
user.length & 0xFF this._sock.flush();
]);
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._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
@ -1643,7 +1638,8 @@ export default class RFB extends EventTargetMixin {
// TODO(directxman12): make genDES not require an Array // TODO(directxman12): make genDES not require an Array
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
const response = RFB.genDES(this._rfbCredentials.password, challenge); const response = RFB.genDES(this._rfbCredentials.password, challenge);
this._sock.send(response); this._sock.sQpushBytes(response);
this._sock.flush();
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -1661,8 +1657,9 @@ export default class RFB extends EventTargetMixin {
if (this._rfbCredentials.ardPublicKey != undefined && if (this._rfbCredentials.ardPublicKey != undefined &&
this._rfbCredentials.ardCredentials != undefined) { this._rfbCredentials.ardCredentials != undefined) {
// if the async web crypto is done return the results // if the async web crypto is done return the results
this._sock.send(this._rfbCredentials.ardCredentials); this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
this._sock.send(this._rfbCredentials.ardPublicKey); this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
this._sock.flush();
this._rfbCredentials.ardCredentials = null; this._rfbCredentials.ardCredentials = null;
this._rfbCredentials.ardPublicKey = null; this._rfbCredentials.ardPublicKey = null;
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
@ -1681,77 +1678,35 @@ export default class RFB extends EventTargetMixin {
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength)); let clientKey = legacyCrypto.generateKey(
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join(''); { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
return false; return false;
} }
_modPow(base, exponent, modulus) { async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
const sharedKey = legacyCrypto.deriveBits(
{ name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
let b = BigInt(baseHex); const credentials = window.crypto.getRandomValues(new Uint8Array(128));
let e = BigInt(exponentHex); for (let i = 0; i < username.length; i++) {
let m = BigInt(modulusHex); credentials[i] = username.charCodeAt(i);
let r = 1n;
b = b % m;
while (e > 0) {
if (e % 2n === 1n) {
r = (r * b) % m;
} }
e = e / 2n; credentials[username.length] = 0;
b = (b * b) % m; for (let i = 0; i < password.length; i++) {
credentials[64 + i] = password.charCodeAt(i);
} }
let hexResult = r.toString(16); credentials[64 + password.length] = 0;
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) { const key = await legacyCrypto.digest("MD5", sharedKey);
hexResult = "0"+hexResult; const cipher = await legacyCrypto.importKey(
} "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
let bytesResult = [];
for (let c = 0; c < hexResult.length; c += 2) {
bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
}
return bytesResult;
}
async _aesEcbEncrypt(string, key) {
// perform AES-ECB blocks
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
let data = new Uint8Array(string.length);
for (let i = 0; i < string.length; ++i) {
data[i] = string.charCodeAt(i);
}
let encrypted = new Uint8Array(data.length);
for (let i=0;i<data.length;i+=16) {
let block = data.slice(i, i+16);
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
);
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
}
return encrypted;
}
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
// calculate the DH keys
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
let paddedUsername = username + '\0' + padding.substring(0, 63);
let paddedPassword = password + '\0' + padding.substring(0, 63);
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
this._rfbCredentials.ardCredentials = encrypted; this._rfbCredentials.ardCredentials = encrypted;
this._rfbCredentials.ardPublicKey = clientPublicKey; this._rfbCredentials.ardPublicKey = clientPublicKey;
@ -1768,10 +1723,12 @@ export default class RFB extends EventTargetMixin {
return false; return false;
} }
this._sock.send([0, 0, 0, this._rfbCredentials.username.length]); this._sock.sQpush32(this._rfbCredentials.username.length);
this._sock.send([0, 0, 0, this._rfbCredentials.password.length]); this._sock.sQpush32(this._rfbCredentials.password.length);
this._sock.sendString(this._rfbCredentials.username); this._sock.sQpushString(this._rfbCredentials.username);
this._sock.sendString(this._rfbCredentials.password); this._sock.sQpushString(this._rfbCredentials.password);
this._sock.flush();
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -1809,7 +1766,8 @@ export default class RFB extends EventTargetMixin {
"vendor or signature"); "vendor or signature");
} }
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); 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 return false; // wait until we receive the sub auth count to continue
} else { } else {
return this._fail("Server wanted tunnels, but doesn't support " + return this._fail("Server wanted tunnels, but doesn't support " +
@ -1859,7 +1817,8 @@ export default class RFB extends EventTargetMixin {
for (let authType in clientSupportedTypes) { for (let authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) { 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); Log.Debug("Selected authentication type: " + authType);
switch (authType) { switch (authType) {
@ -1905,8 +1864,8 @@ export default class RFB extends EventTargetMixin {
if (e.message !== "disconnect normally") { if (e.message !== "disconnect normally") {
this._fail(e.message); this._fail(e.message);
} }
}).then(() => { })
this.dispatchEvent(new CustomEvent('securityresult')); .then(() => {
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
}).finally(() => { }).finally(() => {
@ -1934,15 +1893,15 @@ export default class RFB extends EventTargetMixin {
const g = this._sock.rQshiftBytes(8); const g = this._sock.rQshiftBytes(8);
const p = this._sock.rQshiftBytes(8); const p = this._sock.rQshiftBytes(8);
const A = this._sock.rQshiftBytes(8); const A = this._sock.rQshiftBytes(8);
const b = window.crypto.getRandomValues(new Uint8Array(8)); const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
const B = new Uint8Array(this._modPow(g, b, p)); const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
const secret = new Uint8Array(this._modPow(A, b, p)); 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 username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63); const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
const usernameBytes = new Uint8Array(256); let usernameBytes = new Uint8Array(256);
const passwordBytes = new Uint8Array(64); let passwordBytes = new Uint8Array(64);
window.crypto.getRandomValues(usernameBytes); window.crypto.getRandomValues(usernameBytes);
window.crypto.getRandomValues(passwordBytes); window.crypto.getRandomValues(passwordBytes);
for (let i = 0; i < username.length; i++) { for (let i = 0; i < username.length; i++) {
@ -1953,25 +1912,12 @@ export default class RFB extends EventTargetMixin {
passwordBytes[i] = password.charCodeAt(i); passwordBytes[i] = password.charCodeAt(i);
} }
passwordBytes[password.length] = 0; passwordBytes[password.length] = 0;
let x = new Uint8Array(secret); usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
for (let i = 0; i < 32; i++) { passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
for (let j = 0; j < 8; j++) { this._sock.sQpushBytes(B);
x[j] ^= usernameBytes[i * 8 + j]; this._sock.sQpushBytes(usernameBytes);
} this._sock.sQpushBytes(passwordBytes);
x = des.enc8(x); this._sock.flush();
usernameBytes.set(x, i * 8);
}
x = new Uint8Array(secret);
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
x[j] ^= passwordBytes[i * 8 + j];
}
x = des.enc8(x);
passwordBytes.set(x, i * 8);
}
this._sock.send(B);
this._sock.send(usernameBytes);
this._sock.send(passwordBytes);
this._rfbInitState = "SecurityResult"; this._rfbInitState = "SecurityResult";
return true; return true;
} }
@ -2199,7 +2145,8 @@ export default class RFB extends EventTargetMixin {
return this._handleSecurityReason(); return this._handleSecurityReason();
case 'ClientInitialisation': case 'ClientInitialisation':
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
this._sock.flush();
this._rfbInitState = 'ServerInitialisation'; this._rfbInitState = 'ServerInitialisation';
return true; return true;
@ -2512,19 +2459,11 @@ export default class RFB extends EventTargetMixin {
default: default:
this._fail("Unexpected server message (type " + msgType + ")"); 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; return true;
} }
} }
_onFlush() {
this._flushing = false;
// Resume processing
if (this._sock.rQlen > 0) {
this._handleMessage();
}
}
_framebufferUpdate() { _framebufferUpdate() {
if (this._FBU.rects === 0) { if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; } if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
@ -2535,7 +2474,14 @@ export default class RFB extends EventTargetMixin {
// to avoid building up an excessive queue // to avoid building up an excessive queue
if (this._display.pending()) { if (this._display.pending()) {
this._flushing = true; this._flushing = true;
this._display.flush(); this._display.flush()
.then(() => {
this._flushing = false;
// Resume processing
if (!this._sock.rQwait("message", 1)) {
this._handleMessage();
}
});
return false; return false;
} }
} }
@ -2545,13 +2491,13 @@ export default class RFB extends EventTargetMixin {
if (this._sock.rQwait("rect header", 12)) { return false; } if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */ /* New FramebufferUpdate */
const hdr = this._sock.rQshiftBytes(12); this._FBU.x = this._sock.rQshift16();
this._FBU.x = (hdr[0] << 8) + hdr[1]; this._FBU.y = this._sock.rQshift16();
this._FBU.y = (hdr[2] << 8) + hdr[3]; this._FBU.width = this._sock.rQshift16();
this._FBU.width = (hdr[4] << 8) + hdr[5]; this._FBU.height = this._sock.rQshift16();
this._FBU.height = (hdr[6] << 8) + hdr[7]; this._FBU.encoding = this._sock.rQshift32();
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + /* Encodings are signed */
(hdr[10] << 8) + hdr[11], 10); this._FBU.encoding >>= 0;
} }
if (!this._handleRect()) { if (!this._handleRect()) {
@ -2785,26 +2731,18 @@ export default class RFB extends EventTargetMixin {
const firstUpdate = !this._supportsSetDesktopSize; const firstUpdate = !this._supportsSetDesktopSize;
this._supportsSetDesktopSize = true; 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(1); // number-of-screens
this._sock.rQskipBytes(3); // padding this._sock.rQskipBytes(3); // padding
for (let i = 0; i < numberOfScreens; i += 1) { for (let i = 0; i < numberOfScreens; i += 1) {
// Save the id and flags of the first screen // Save the id and flags of the first screen
if (i === 0) { if (i === 0) {
this._screenID = this._sock.rQshiftBytes(4); // id this._screenID = this._sock.rQshift32(); // id
this._sock.rQskipBytes(2); // x-position this._sock.rQskipBytes(2); // x-position
this._sock.rQskipBytes(2); // y-position this._sock.rQskipBytes(2); // y-position
this._sock.rQskipBytes(2); // width this._sock.rQskipBytes(2); // width
this._sock.rQskipBytes(2); // height this._sock.rQskipBytes(2); // height
this._screenFlags = this._sock.rQshiftBytes(4); // flags this._screenFlags = this._sock.rQshift32(); // flags
} else { } else {
this._sock.rQskipBytes(16); this._sock.rQskipBytes(16);
} }
@ -2842,6 +2780,14 @@ export default class RFB extends EventTargetMixin {
this._resize(this._FBU.width, this._FBU.height); 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; return true;
} }
@ -2937,28 +2883,22 @@ export default class RFB extends EventTargetMixin {
static genDES(password, challenge) { static genDES(password, challenge) {
const passwordChars = password.split('').map(c => c.charCodeAt(0)); const passwordChars = password.split('').map(c => c.charCodeAt(0));
return (new DES(passwordChars)).encrypt(challenge); const key = legacyCrypto.importKey(
"raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
} }
} }
// Class Methods // Class Methods
RFB.messages = { RFB.messages = {
keyEvent(sock, keysym, down) { keyEvent(sock, keysym, down) {
const buff = sock._sQ; sock.sQpush8(4); // msg-type
const offset = sock._sQlen; sock.sQpush8(down);
buff[offset] = 4; // msg-type sock.sQpush16(0);
buff[offset + 1] = down;
buff[offset + 2] = 0; sock.sQpush32(keysym);
buff[offset + 3] = 0;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
sock._sQlen += 8;
sock.flush(); sock.flush();
}, },
@ -2972,46 +2912,28 @@ RFB.messages = {
return xtScanCode; return xtScanCode;
} }
const buff = sock._sQ; sock.sQpush8(255); // msg-type
const offset = sock._sQlen; sock.sQpush8(0); // sub msg-type
buff[offset] = 255; // msg-type sock.sQpush16(down);
buff[offset + 1] = 0; // sub msg-type
buff[offset + 2] = (down >> 8); sock.sQpush32(keysym);
buff[offset + 3] = down;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
const RFBkeycode = getRFBkeycode(keycode); const RFBkeycode = getRFBkeycode(keycode);
buff[offset + 8] = (RFBkeycode >> 24); sock.sQpush32(RFBkeycode);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;
sock._sQlen += 12;
sock.flush(); sock.flush();
}, },
pointerEvent(sock, x, y, mask) { pointerEvent(sock, x, y, mask) {
const buff = sock._sQ; sock.sQpush8(5); // msg-type
const offset = sock._sQlen;
buff[offset] = 5; // msg-type sock.sQpush8(mask);
buff[offset + 1] = mask; sock.sQpush16(x);
sock.sQpush16(y);
buff[offset + 2] = x >> 8;
buff[offset + 3] = x;
buff[offset + 4] = y >> 8;
buff[offset + 5] = y;
sock._sQlen += 6;
sock.flush(); sock.flush();
}, },
@ -3111,14 +3033,11 @@ RFB.messages = {
}, },
clientCutText(sock, data, extended = false) { clientCutText(sock, data, extended = false) {
const buff = sock._sQ; sock.sQpush8(6); // msg-type
const offset = sock._sQlen;
buff[offset] = 6; // msg-type sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
let length; let length;
if (extended) { if (extended) {
@ -3127,121 +3046,63 @@ RFB.messages = {
length = data.length; length = data.length;
} }
buff[offset + 4] = length >> 24; sock.sQpush32(length);
buff[offset + 5] = length >> 16; sock.sQpushBytes(data);
buff[offset + 6] = length >> 8;
buff[offset + 7] = length;
sock._sQlen += 8;
// We have to keep track of from where in the data we begin creating the
// buffer for the flush in the next iteration.
let dataOffset = 0;
let remaining = data.length;
while (remaining > 0) {
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
for (let i = 0; i < flushSize; i++) {
buff[sock._sQlen + i] = data[dataOffset + i];
}
sock._sQlen += flushSize;
sock.flush(); sock.flush();
remaining -= flushSize;
dataOffset += flushSize;
}
}, },
setDesktopSize(sock, width, height, id, flags) { setDesktopSize(sock, width, height, id, flags) {
const buff = sock._sQ; sock.sQpush8(251); // msg-type
const offset = sock._sQlen;
buff[offset] = 251; // msg-type sock.sQpush8(0); // padding
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;
buff[offset + 6] = 1; // number-of-screens sock.sQpush16(width);
buff[offset + 7] = 0; // padding sock.sQpush16(height);
sock.sQpush8(1); // number-of-screens
sock.sQpush8(0); // padding
// screen array // screen array
buff[offset + 8] = id >> 24; // id sock.sQpush32(id);
buff[offset + 9] = id >> 16; sock.sQpush16(0); // x-position
buff[offset + 10] = id >> 8; sock.sQpush16(0); // y-position
buff[offset + 11] = id; sock.sQpush16(width);
buff[offset + 12] = 0; // x-position sock.sQpush16(height);
buff[offset + 13] = 0; sock.sQpush32(flags);
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.flush(); sock.flush();
}, },
clientFence(sock, flags, payload) { clientFence(sock, flags, payload) {
const buff = sock._sQ; sock.sQpush8(248); // msg-type
const offset = sock._sQlen;
buff[offset] = 248; // msg-type sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding sock.sQpush32(flags);
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
buff[offset + 4] = flags >> 24; // flags sock.sQpush8(payload.length);
buff[offset + 5] = flags >> 16; sock.sQpushString(payload);
buff[offset + 6] = flags >> 8;
buff[offset + 7] = flags;
const n = payload.length;
buff[offset + 8] = n; // length
for (let i = 0; i < n; i++) {
buff[offset + 9 + i] = payload.charCodeAt(i);
}
sock._sQlen += 9 + n;
sock.flush(); sock.flush();
}, },
enableContinuousUpdates(sock, enable, x, y, width, height) { enableContinuousUpdates(sock, enable, x, y, width, height) {
const buff = sock._sQ; sock.sQpush8(150); // msg-type
const offset = sock._sQlen;
buff[offset] = 150; // msg-type sock.sQpush8(enable);
buff[offset + 1] = enable; // enable-flag
buff[offset + 2] = x >> 8; // x sock.sQpush16(x);
buff[offset + 3] = x; sock.sQpush16(y);
buff[offset + 4] = y >> 8; // y sock.sQpush16(width);
buff[offset + 5] = y; sock.sQpush16(height);
buff[offset + 6] = width >> 8; // width
buff[offset + 7] = width;
buff[offset + 8] = height >> 8; // height
buff[offset + 9] = height;
sock._sQlen += 10;
sock.flush(); sock.flush();
}, },
pixelFormat(sock, depth, trueColor) { pixelFormat(sock, depth, trueColor) {
const buff = sock._sQ;
const offset = sock._sQlen;
let bpp; let bpp;
if (depth > 16) { if (depth > 16) {
@ -3254,100 +3115,69 @@ RFB.messages = {
const bits = Math.floor(depth/3); const bits = Math.floor(depth/3);
buff[offset] = 0; // msg-type sock.sQpush8(0); // msg-type
buff[offset + 1] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 2] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 3] = 0; // padding sock.sQpush8(0); // padding
buff[offset + 4] = bpp; // bits-per-pixel sock.sQpush8(bpp);
buff[offset + 5] = depth; // depth sock.sQpush8(depth);
buff[offset + 6] = 0; // little-endian sock.sQpush8(0); // little-endian
buff[offset + 7] = trueColor ? 1 : 0; // true-color sock.sQpush8(trueColor ? 1 : 0);
buff[offset + 8] = 0; // red-max sock.sQpush16((1 << bits) - 1); // red-max
buff[offset + 9] = (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 sock.sQpush8(bits * 0); // red-shift
buff[offset + 11] = (1 << bits) - 1; // green-max sock.sQpush8(bits * 1); // green-shift
sock.sQpush8(bits * 2); // blue-shift
buff[offset + 12] = 0; // blue-max sock.sQpush8(0); // padding
buff[offset + 13] = (1 << bits) - 1; // blue-max 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(); sock.flush();
}, },
clientEncodings(sock, encodings) { clientEncodings(sock, encodings) {
const buff = sock._sQ; sock.sQpush8(2); // msg-type
const offset = sock._sQlen;
buff[offset] = 2; // msg-type sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = encodings.length >> 8; sock.sQpush16(encodings.length);
buff[offset + 3] = encodings.length;
let j = offset + 4;
for (let i = 0; i < encodings.length; i++) { for (let i = 0; i < encodings.length; i++) {
const enc = encodings[i]; sock.sQpush32(encodings[i]);
buff[j] = enc >> 24;
buff[j + 1] = enc >> 16;
buff[j + 2] = enc >> 8;
buff[j + 3] = enc;
j += 4;
} }
sock._sQlen += j - offset;
sock.flush(); sock.flush();
}, },
fbUpdateRequest(sock, incremental, x, y, w, h) { fbUpdateRequest(sock, incremental, x, y, w, h) {
const buff = sock._sQ;
const offset = sock._sQlen;
if (typeof(x) === "undefined") { x = 0; } if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; } if (typeof(y) === "undefined") { y = 0; }
buff[offset] = 3; // msg-type sock.sQpush8(3); // msg-type
buff[offset + 1] = incremental ? 1 : 0;
buff[offset + 2] = (x >> 8) & 0xFF; sock.sQpush8(incremental ? 1 : 0);
buff[offset + 3] = x & 0xFF;
buff[offset + 4] = (y >> 8) & 0xFF; sock.sQpush16(x);
buff[offset + 5] = y & 0xFF; sock.sQpush16(y);
sock.sQpush16(w);
sock.sQpush16(h);
buff[offset + 6] = (w >> 8) & 0xFF;
buff[offset + 7] = w & 0xFF;
buff[offset + 8] = (h >> 8) & 0xFF;
buff[offset + 9] = h & 0xFF;
sock._sQlen += 10;
sock.flush(); sock.flush();
}, },
xvpOp(sock, ver, op) { xvpOp(sock, ver, op) {
const buff = sock._sQ; sock.sQpush8(250); // msg-type
const offset = sock._sQlen;
buff[offset] = 250; // msg-type sock.sQpush8(0); // padding
buff[offset + 1] = 0; // padding
buff[offset + 2] = ver; sock.sQpush8(ver);
buff[offset + 3] = op; sock.sQpush8(op);
sock._sQlen += 4;
sock.flush(); sock.flush();
} }
}; };

View File

@ -94,27 +94,7 @@ export default class Websock {
return "unknown"; 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 // Receive Queue
get rQlen() {
return this._rQlen - this._rQi;
}
rQpeek8() { rQpeek8() {
return this._rQ[this._rQi]; return this._rQ[this._rQi];
} }
@ -141,42 +121,47 @@ export default class Websock {
for (let byte = bytes - 1; byte >= 0; byte--) { for (let byte = bytes - 1; byte >= 0; byte--) {
res += this._rQ[this._rQi++] << (byte * 8); res += this._rQ[this._rQi++] << (byte * 8);
} }
return res; return res >>> 0;
} }
rQshiftStr(len) { rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
let str = ""; let str = "";
// Handle large arrays in steps to avoid long strings on the stack // Handle large arrays in steps to avoid long strings on the stack
for (let i = 0; i < len; i += 4096) { 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); str += String.fromCharCode.apply(null, part);
} }
return str; return str;
} }
rQshiftBytes(len) { rQshiftBytes(len, copy=true) {
if (typeof(len) === 'undefined') { len = this.rQlen; }
this._rQi += len; 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) { rQshiftTo(target, len) {
if (len === undefined) { len = this.rQlen; }
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ // 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)); target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
this._rQi += len; this._rQi += len;
} }
rQslice(start, end = this.rQlen) { rQpeekBytes(len, copy=true) {
return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); 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) // Check to see if we must wait for 'num' bytes (default to FBU.bytes)
// to be available in the receive queue. Return true if we need to // to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false. // wait (and possibly print a debug message), otherwise false.
rQwait(msg, num, goback) { rQwait(msg, num, goback) {
if (this.rQlen < num) { if (this._rQlen - this._rQi < num) {
if (goback) { if (goback) {
if (this._rQi < goback) { if (this._rQi < goback) {
throw new Error("rQwait cannot backup " + goback + " bytes"); throw new Error("rQwait cannot backup " + goback + " bytes");
@ -190,21 +175,56 @@ export default class Websock {
// Send Queue // Send Queue
sQpush8(num) {
this._sQensureSpace(1);
this._sQ[this._sQlen++] = num;
}
sQpush16(num) {
this._sQensureSpace(2);
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpush32(num) {
this._sQensureSpace(4);
this._sQ[this._sQlen++] = (num >> 24) & 0xff;
this._sQ[this._sQlen++] = (num >> 16) & 0xff;
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
}
sQpushString(str) {
let bytes = str.split('').map(chr => chr.charCodeAt(0));
this.sQpushBytes(new Uint8Array(bytes));
}
sQpushBytes(bytes) {
for (let offset = 0;offset < bytes.length;) {
this._sQensureSpace(1);
let chunkSize = this._sQbufferSize - this._sQlen;
if (chunkSize > bytes.length - offset) {
chunkSize = bytes.length - offset;
}
this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen);
this._sQlen += chunkSize;
offset += chunkSize;
}
}
flush() { flush() {
if (this._sQlen > 0 && this.readyState === 'open') { 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; this._sQlen = 0;
} }
} }
send(arr) { _sQensureSpace(bytes) {
this._sQ.set(arr, this._sQlen); if (this._sQbufferSize - this._sQlen < bytes) {
this._sQlen += arr.length;
this.flush(); this.flush();
} }
sendString(str) {
this.send(str.split('').map(chr => chr.charCodeAt(0)));
} }
// Event Handlers // Event Handlers
@ -283,17 +303,12 @@ export default class Websock {
} }
// private methods // 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, // We want to move all the unread data to the start of the queue,
// e.g. compacting. // e.g. compacting.
// The function also expands the receive que if needed, and for // The function also expands the receive que if needed, and for
// performance reasons we combine these two actions to avoid // performance reasons we combine these two actions to avoid
// unneccessary copying. // unnecessary copying.
_expandCompactRQ(minFit) { _expandCompactRQ(minFit) {
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place // if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
// instead of resizing // instead of resizing
@ -309,7 +324,7 @@ export default class Websock {
// we don't want to grow unboundedly // we don't want to grow unboundedly
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
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"); throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
} }
} }
@ -327,25 +342,22 @@ export default class Websock {
} }
// push arraybuffer values onto the end of the receive que // push arraybuffer values onto the end of the receive que
_DecodeMessage(data) {
const u8 = new Uint8Array(data);
if (u8.length > this._rQbufferSize - this._rQlen) {
this._expandCompactRQ(u8.length);
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
}
_recvMessage(e) { _recvMessage(e) {
this._DecodeMessage(e.data);
if (this.rQlen > 0) {
this._eventHandlers.message();
if (this._rQlen == this._rQi) { if (this._rQlen == this._rQi) {
// All data has now been processed, this means we // All data has now been processed, this means we
// can reset the receive queue. // can reset the receive queue.
this._rQlen = 0; this._rQlen = 0;
this._rQi = 0; this._rQi = 0;
} }
const u8 = new Uint8Array(e.data);
if (u8.length > this._rQbufferSize - this._rQlen) {
this._expandCompactRQ(u8.length);
}
this._rQ.set(u8, this._rQlen);
this._rQlen += u8.length;
if (this._rQlen - this._rQi > 0) {
this._eventHandlers.message();
} else { } else {
Log.Debug("Ignoring empty message"); Log.Debug("Ignoring empty message");
} }

View File

@ -81,9 +81,3 @@ None
| blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display | 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 | drawImage | (img, x, y) | Draw image and track damage
| autoscale | (containerWidth, containerHeight) | Scale the display | 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

View File

@ -1,6 +1,6 @@
{ {
"name": "@novnc/novnc", "name": "@novnc/novnc",
"version": "1.4.0-beta", "version": "1.4.0",
"description": "An HTML5 VNC client", "description": "An HTML5 VNC client",
"browser": "lib/rfb", "browser": "lib/rfb",
"directories": { "directories": {

199
po/ja.po
View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: noVNC 1.1.0\n" "Project-Id-Version: noVNC 1.1.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2020-07-03 16:11+0200\n" "POT-Creation-Date: 2022-12-27 15:24+0100\n"
"PO-Revision-Date: 2021-01-15 12:37+0900\n" "PO-Revision-Date: 2023-03-21 12:42+0900\n"
"Last-Translator: nnn1590 <nnn1590@nnn1590.org>\n" "Last-Translator: nnn1590 <nnn1590@nnn1590.org>\n"
"Language-Team: Japanese\n" "Language-Team: Japanese\n"
"Language: ja\n" "Language: ja\n"
@ -19,286 +19,325 @@ msgstr ""
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 2.3\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..." msgid "Connecting..."
msgstr "接続しています..." msgstr "接続しています..."
#: ../app/ui.js:401 #: ../app/ui.js:417
msgid "Disconnecting..." msgid "Disconnecting..."
msgstr "切断しています..." msgstr "切断しています..."
#: ../app/ui.js:407 #: ../app/ui.js:423
msgid "Reconnecting..." msgid "Reconnecting..."
msgstr "再接続しています..." msgstr "再接続しています..."
#: ../app/ui.js:412 #: ../app/ui.js:428
msgid "Internal error" msgid "Internal error"
msgstr "内部エラー" msgstr "内部エラー"
#: ../app/ui.js:1008 #: ../app/ui.js:1026
msgid "Must set host" msgid "Must set host"
msgstr "ホストを設定する必要があります" msgstr "ホストを設定する必要があります"
#: ../app/ui.js:1090 #: ../app/ui.js:1110
msgid "Connected (encrypted) to " msgid "Connected (encrypted) to "
msgstr "接続しました (暗号化済み): " msgstr "接続しました (暗号化済み): "
#: ../app/ui.js:1092 #: ../app/ui.js:1112
msgid "Connected (unencrypted) to " msgid "Connected (unencrypted) to "
msgstr "接続しました (暗号化されていません): " msgstr "接続しました (暗号化されていません): "
#: ../app/ui.js:1115 #: ../app/ui.js:1135
msgid "Something went wrong, connection is closed" msgid "Something went wrong, connection is closed"
msgstr "何らかの問題で、接続が閉じられました" msgstr "何らかの問題で、接続が閉じられました"
#: ../app/ui.js:1118 #: ../app/ui.js:1138
msgid "Failed to connect to server" msgid "Failed to connect to server"
msgstr "サーバーへの接続に失敗しました" msgstr "サーバーへの接続に失敗しました"
#: ../app/ui.js:1128 #: ../app/ui.js:1150
msgid "Disconnected" msgid "Disconnected"
msgstr "切断しました" msgstr "切断しました"
#: ../app/ui.js:1143 #: ../app/ui.js:1165
msgid "New connection has been rejected with reason: " msgid "New connection has been rejected with reason: "
msgstr "新規接続は次の理由で拒否されました: " msgstr "新規接続は次の理由で拒否されました: "
#: ../app/ui.js:1146 #: ../app/ui.js:1168
msgid "New connection has been rejected" msgid "New connection has been rejected"
msgstr "新規接続は拒否されました" msgstr "新規接続は拒否されました"
#: ../app/ui.js:1181 #: ../app/ui.js:1234
msgid "Credentials are required" msgid "Credentials are required"
msgstr "資格情報が必要です" msgstr "資格情報が必要です"
#: ../vnc.html:74 #: ../vnc.html:57
msgid "noVNC encountered an error:" msgid "noVNC encountered an error:"
msgstr "noVNC でエラーが発生しました:" msgstr "noVNC でエラーが発生しました:"
#: ../vnc.html:84 #: ../vnc.html:67
msgid "Hide/Show the control bar" msgid "Hide/Show the control bar"
msgstr "コントロールバーを隠す/表示する" msgstr "コントロールバーを隠す/表示する"
#: ../vnc.html:91 #: ../vnc.html:76
msgid "Drag" msgid "Drag"
msgstr "ドラッグ" msgstr "ドラッグ"
#: ../vnc.html:91 #: ../vnc.html:76
msgid "Move/Drag Viewport" msgid "Move/Drag Viewport"
msgstr "ビューポートを移動/ドラッグ" msgstr "ビューポートを移動/ドラッグ"
#: ../vnc.html:97 #: ../vnc.html:82
msgid "Keyboard" msgid "Keyboard"
msgstr "キーボード" msgstr "キーボード"
#: ../vnc.html:97 #: ../vnc.html:82
msgid "Show Keyboard" msgid "Show Keyboard"
msgstr "キーボードを表示" msgstr "キーボードを表示"
#: ../vnc.html:102 #: ../vnc.html:87
msgid "Extra keys" msgid "Extra keys"
msgstr "追加キー" msgstr "追加キー"
#: ../vnc.html:102 #: ../vnc.html:87
msgid "Show Extra Keys" msgid "Show Extra Keys"
msgstr "追加キーを表示" msgstr "追加キーを表示"
#: ../vnc.html:107 #: ../vnc.html:92
msgid "Ctrl" msgid "Ctrl"
msgstr "Ctrl" msgstr "Ctrl"
#: ../vnc.html:107 #: ../vnc.html:92
msgid "Toggle Ctrl" msgid "Toggle Ctrl"
msgstr "Ctrl キーを切り替え" msgstr "Ctrl キーをトグル"
#: ../vnc.html:110 #: ../vnc.html:95
msgid "Alt" msgid "Alt"
msgstr "Alt" msgstr "Alt"
#: ../vnc.html:110 #: ../vnc.html:95
msgid "Toggle Alt" msgid "Toggle Alt"
msgstr "Alt キーを切り替え" msgstr "Alt キーをトグル"
#: ../vnc.html:113 #: ../vnc.html:98
msgid "Toggle Windows" msgid "Toggle Windows"
msgstr "Windows キーを切り替え" msgstr "Windows キーをトグル"
#: ../vnc.html:113 #: ../vnc.html:98
msgid "Windows" msgid "Windows"
msgstr "Windows" msgstr "Windows"
#: ../vnc.html:116 #: ../vnc.html:101
msgid "Send Tab" msgid "Send Tab"
msgstr "Tab キーを送信" msgstr "Tab キーを送信"
#: ../vnc.html:116 #: ../vnc.html:101
msgid "Tab" msgid "Tab"
msgstr "Tab" msgstr "Tab"
#: ../vnc.html:119 #: ../vnc.html:104
msgid "Esc" msgid "Esc"
msgstr "Esc" msgstr "Esc"
#: ../vnc.html:119 #: ../vnc.html:104
msgid "Send Escape" msgid "Send Escape"
msgstr "Escape キーを送信" msgstr "Escape キーを送信"
#: ../vnc.html:122 #: ../vnc.html:107
msgid "Ctrl+Alt+Del" msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del"
#: ../vnc.html:122 #: ../vnc.html:107
msgid "Send Ctrl-Alt-Del" msgid "Send Ctrl-Alt-Del"
msgstr "Ctrl-Alt-Del を送信" msgstr "Ctrl-Alt-Del を送信"
#: ../vnc.html:129 #: ../vnc.html:114
msgid "Shutdown/Reboot" msgid "Shutdown/Reboot"
msgstr "シャットダウン/再起動" msgstr "シャットダウン/再起動"
#: ../vnc.html:129 #: ../vnc.html:114
msgid "Shutdown/Reboot..." msgid "Shutdown/Reboot..."
msgstr "シャットダウン/再起動..." msgstr "シャットダウン/再起動..."
#: ../vnc.html:135 #: ../vnc.html:120
msgid "Power" msgid "Power"
msgstr "電源" msgstr "電源"
#: ../vnc.html:137 #: ../vnc.html:122
msgid "Shutdown" msgid "Shutdown"
msgstr "シャットダウン" msgstr "シャットダウン"
#: ../vnc.html:138 #: ../vnc.html:123
msgid "Reboot" msgid "Reboot"
msgstr "再起動" msgstr "再起動"
#: ../vnc.html:139 #: ../vnc.html:124
msgid "Reset" msgid "Reset"
msgstr "リセット" msgstr "リセット"
#: ../vnc.html:144 ../vnc.html:150 #: ../vnc.html:129 ../vnc.html:135
msgid "Clipboard" msgid "Clipboard"
msgstr "クリップボード" msgstr "クリップボード"
#: ../vnc.html:154 #: ../vnc.html:137
msgid "Clear" msgid "Edit clipboard content in the textarea below."
msgstr "クリア" msgstr "以下の入力欄からクリップボードの内容を編集できます。"
#: ../vnc.html:160 #: ../vnc.html:145
msgid "Fullscreen" msgid "Full Screen"
msgstr "全画面表示" msgstr "全画面表示"
#: ../vnc.html:165 ../vnc.html:172 #: ../vnc.html:150 ../vnc.html:156
msgid "Settings" msgid "Settings"
msgstr "設定" msgstr "設定"
#: ../vnc.html:175 #: ../vnc.html:160
msgid "Shared Mode" msgid "Shared Mode"
msgstr "共有モード" msgstr "共有モード"
#: ../vnc.html:178 #: ../vnc.html:163
msgid "View Only" msgid "View Only"
msgstr "表示のみ" msgstr "表示専用"
#: ../vnc.html:182 #: ../vnc.html:167
msgid "Clip to Window" msgid "Clip to Window"
msgstr "ウィンドウにクリップ" msgstr "ウィンドウにクリップ"
#: ../vnc.html:185 #: ../vnc.html:170
msgid "Scaling Mode:" msgid "Scaling Mode:"
msgstr "スケーリングモード:" msgstr "スケーリングモード:"
#: ../vnc.html:187 #: ../vnc.html:172
msgid "None" msgid "None"
msgstr "なし" msgstr "なし"
#: ../vnc.html:188 #: ../vnc.html:173
msgid "Local Scaling" msgid "Local Scaling"
msgstr "ローカルスケーリング" msgstr "ローカルスケーリング"
#: ../vnc.html:189 #: ../vnc.html:174
msgid "Remote Resizing" msgid "Remote Resizing"
msgstr "リモートでリサイズ" msgstr "リモートでリサイズ"
#: ../vnc.html:194 #: ../vnc.html:179
msgid "Advanced" msgid "Advanced"
msgstr "高度" msgstr "高度"
#: ../vnc.html:197 #: ../vnc.html:182
msgid "Quality:" msgid "Quality:"
msgstr "品質:" msgstr "品質:"
#: ../vnc.html:201 #: ../vnc.html:186
msgid "Compression level:" msgid "Compression level:"
msgstr "圧縮レベル:" msgstr "圧縮レベル:"
#: ../vnc.html:206 #: ../vnc.html:191
msgid "Repeater ID:" msgid "Repeater ID:"
msgstr "リピーター ID:" msgstr "リピーター ID:"
#: ../vnc.html:210 #: ../vnc.html:195
msgid "WebSocket" msgid "WebSocket"
msgstr "WebSocket" msgstr "WebSocket"
#: ../vnc.html:213 #: ../vnc.html:198
msgid "Encrypt" msgid "Encrypt"
msgstr "暗号化" msgstr "暗号化"
#: ../vnc.html:216 #: ../vnc.html:201
msgid "Host:" msgid "Host:"
msgstr "ホスト:" msgstr "ホスト:"
#: ../vnc.html:220 #: ../vnc.html:205
msgid "Port:" msgid "Port:"
msgstr "ポート:" msgstr "ポート:"
#: ../vnc.html:224 #: ../vnc.html:209
msgid "Path:" msgid "Path:"
msgstr "パス:" msgstr "パス:"
#: ../vnc.html:231 #: ../vnc.html:216
msgid "Automatic Reconnect" msgid "Automatic Reconnect"
msgstr "自動再接続" msgstr "自動再接続"
#: ../vnc.html:234 #: ../vnc.html:219
msgid "Reconnect Delay (ms):" msgid "Reconnect Delay (ms):"
msgstr "再接続する遅延 (ミリ秒):" msgstr "再接続する遅延 (ミリ秒):"
#: ../vnc.html:239 #: ../vnc.html:224
msgid "Show Dot when No Cursor" msgid "Show Dot when No Cursor"
msgstr "カーソルがないときにドットを表示" msgstr "カーソルがないときにドットを表示する"
#: ../vnc.html:244 #: ../vnc.html:229
msgid "Logging:" msgid "Logging:"
msgstr "ロギング:" msgstr "ロギング:"
#: ../vnc.html:253 #: ../vnc.html:238
msgid "Version:" msgid "Version:"
msgstr "バージョン:" msgstr "バージョン:"
#: ../vnc.html:261 #: ../vnc.html:246
msgid "Disconnect" msgid "Disconnect"
msgstr "切断" msgstr "切断"
#: ../vnc.html:280 #: ../vnc.html:269
msgid "Connect" msgid "Connect"
msgstr "接続" 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:" msgid "Username:"
msgstr "ユーザー名:" msgstr "ユーザー名:"
#: ../vnc.html:294 #: ../vnc.html:310
msgid "Password:" msgid "Password:"
msgstr "パスワード:" msgstr "パスワード:"
#: ../vnc.html:298 #: ../vnc.html:314
msgid "Send Credentials" msgid "Send Credentials"
msgstr "資格情報を送信" msgstr "資格情報を送信"
#: ../vnc.html:308 #: ../vnc.html:323
msgid "Cancel" msgid "Cancel"
msgstr "キャンセル" msgstr "キャンセル"
#~ msgid "Clear"
#~ msgstr "クリア"
#~ msgid "Password is required" #~ msgid "Password is required"
#~ msgstr "パスワードが必要です" #~ msgstr "パスワードが必要です"

189
po/sv.po
View File

@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: noVNC 1.3.0\n" "Project-Id-Version: noVNC 1.3.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n" "Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2021-08-27 16:03+0200\n" "POT-Creation-Date: 2023-01-20 12:54+0100\n"
"PO-Revision-Date: 2021-08-27 16:18+0200\n" "PO-Revision-Date: 2023-01-20 12:58+0100\n"
"Last-Translator: Samuel Mannehed <samuel@cendio.se>\n" "Last-Translator: Samuel Mannehed <samuel@cendio.se>\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: sv\n" "Language: sv\n"
@ -17,265 +17,269 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\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..." msgid "Connecting..."
msgstr "Ansluter..." msgstr "Ansluter..."
#: ../app/ui.js:407 #: ../app/ui.js:417
msgid "Disconnecting..." msgid "Disconnecting..."
msgstr "Kopplar ner..." msgstr "Kopplar ner..."
#: ../app/ui.js:413 #: ../app/ui.js:423
msgid "Reconnecting..." msgid "Reconnecting..."
msgstr "Återansluter..." msgstr "Återansluter..."
#: ../app/ui.js:418 #: ../app/ui.js:428
msgid "Internal error" msgid "Internal error"
msgstr "Internt fel" msgstr "Internt fel"
#: ../app/ui.js:1009 #: ../app/ui.js:1026
msgid "Must set host" msgid "Must set host"
msgstr "Du måste specifiera en värd" msgstr "Du måste specifiera en värd"
#: ../app/ui.js:1091 #: ../app/ui.js:1110
msgid "Connected (encrypted) to " msgid "Connected (encrypted) to "
msgstr "Ansluten (krypterat) till " msgstr "Ansluten (krypterat) till "
#: ../app/ui.js:1093 #: ../app/ui.js:1112
msgid "Connected (unencrypted) to " msgid "Connected (unencrypted) to "
msgstr "Ansluten (okrypterat) till " msgstr "Ansluten (okrypterat) till "
#: ../app/ui.js:1116 #: ../app/ui.js:1135
msgid "Something went wrong, connection is closed" msgid "Something went wrong, connection is closed"
msgstr "Något gick fel, anslutningen avslutades" msgstr "Något gick fel, anslutningen avslutades"
#: ../app/ui.js:1119 #: ../app/ui.js:1138
msgid "Failed to connect to server" msgid "Failed to connect to server"
msgstr "Misslyckades att ansluta till servern" msgstr "Misslyckades att ansluta till servern"
#: ../app/ui.js:1129 #: ../app/ui.js:1150
msgid "Disconnected" msgid "Disconnected"
msgstr "Frånkopplad" msgstr "Frånkopplad"
#: ../app/ui.js:1144 #: ../app/ui.js:1165
msgid "New connection has been rejected with reason: " msgid "New connection has been rejected with reason: "
msgstr "Ny anslutning har blivit nekad med följande skäl: " 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" msgid "New connection has been rejected"
msgstr "Ny anslutning har blivit nekad" msgstr "Ny anslutning har blivit nekad"
#: ../app/ui.js:1182 #: ../app/ui.js:1234
msgid "Credentials are required" msgid "Credentials are required"
msgstr "Användaruppgifter krävs" msgstr "Användaruppgifter krävs"
#: ../vnc.html:61 #: ../vnc.html:55
msgid "noVNC encountered an error:" msgid "noVNC encountered an error:"
msgstr "noVNC stötte på ett problem:" msgstr "noVNC stötte på ett problem:"
#: ../vnc.html:71 #: ../vnc.html:65
msgid "Hide/Show the control bar" msgid "Hide/Show the control bar"
msgstr "Göm/Visa kontrollbaren" msgstr "Göm/Visa kontrollbaren"
#: ../vnc.html:78 #: ../vnc.html:74
msgid "Drag" msgid "Drag"
msgstr "Dra" msgstr "Dra"
#: ../vnc.html:78 #: ../vnc.html:74
msgid "Move/Drag Viewport" msgid "Move/Drag Viewport"
msgstr "Flytta/Dra Vyn" msgstr "Flytta/Dra Vyn"
#: ../vnc.html:84 #: ../vnc.html:80
msgid "Keyboard" msgid "Keyboard"
msgstr "Tangentbord" msgstr "Tangentbord"
#: ../vnc.html:84 #: ../vnc.html:80
msgid "Show Keyboard" msgid "Show Keyboard"
msgstr "Visa Tangentbord" msgstr "Visa Tangentbord"
#: ../vnc.html:89 #: ../vnc.html:85
msgid "Extra keys" msgid "Extra keys"
msgstr "Extraknappar" msgstr "Extraknappar"
#: ../vnc.html:89 #: ../vnc.html:85
msgid "Show Extra Keys" msgid "Show Extra Keys"
msgstr "Visa Extraknappar" msgstr "Visa Extraknappar"
#: ../vnc.html:94 #: ../vnc.html:90
msgid "Ctrl" msgid "Ctrl"
msgstr "Ctrl" msgstr "Ctrl"
#: ../vnc.html:94 #: ../vnc.html:90
msgid "Toggle Ctrl" msgid "Toggle Ctrl"
msgstr "Växla Ctrl" msgstr "Växla Ctrl"
#: ../vnc.html:97 #: ../vnc.html:93
msgid "Alt" msgid "Alt"
msgstr "Alt" msgstr "Alt"
#: ../vnc.html:97 #: ../vnc.html:93
msgid "Toggle Alt" msgid "Toggle Alt"
msgstr "Växla Alt" msgstr "Växla Alt"
#: ../vnc.html:100 #: ../vnc.html:96
msgid "Toggle Windows" msgid "Toggle Windows"
msgstr "Växla Windows" msgstr "Växla Windows"
#: ../vnc.html:100 #: ../vnc.html:96
msgid "Windows" msgid "Windows"
msgstr "Windows" msgstr "Windows"
#: ../vnc.html:103 #: ../vnc.html:99
msgid "Send Tab" msgid "Send Tab"
msgstr "Skicka Tab" msgstr "Skicka Tab"
#: ../vnc.html:103 #: ../vnc.html:99
msgid "Tab" msgid "Tab"
msgstr "Tab" msgstr "Tab"
#: ../vnc.html:106 #: ../vnc.html:102
msgid "Esc" msgid "Esc"
msgstr "Esc" msgstr "Esc"
#: ../vnc.html:106 #: ../vnc.html:102
msgid "Send Escape" msgid "Send Escape"
msgstr "Skicka Escape" msgstr "Skicka Escape"
#: ../vnc.html:109 #: ../vnc.html:105
msgid "Ctrl+Alt+Del" msgid "Ctrl+Alt+Del"
msgstr "Ctrl+Alt+Del" msgstr "Ctrl+Alt+Del"
#: ../vnc.html:109 #: ../vnc.html:105
msgid "Send Ctrl-Alt-Del" msgid "Send Ctrl-Alt-Del"
msgstr "Skicka Ctrl-Alt-Del" msgstr "Skicka Ctrl-Alt-Del"
#: ../vnc.html:116 #: ../vnc.html:112
msgid "Shutdown/Reboot" msgid "Shutdown/Reboot"
msgstr "Stäng av/Boota om" msgstr "Stäng av/Boota om"
#: ../vnc.html:116 #: ../vnc.html:112
msgid "Shutdown/Reboot..." msgid "Shutdown/Reboot..."
msgstr "Stäng av/Boota om..." msgstr "Stäng av/Boota om..."
#: ../vnc.html:122 #: ../vnc.html:118
msgid "Power" msgid "Power"
msgstr "Ström" msgstr "Ström"
#: ../vnc.html:124 #: ../vnc.html:120
msgid "Shutdown" msgid "Shutdown"
msgstr "Stäng av" msgstr "Stäng av"
#: ../vnc.html:125 #: ../vnc.html:121
msgid "Reboot" msgid "Reboot"
msgstr "Boota om" msgstr "Boota om"
#: ../vnc.html:126 #: ../vnc.html:122
msgid "Reset" msgid "Reset"
msgstr "Återställ" msgstr "Återställ"
#: ../vnc.html:131 ../vnc.html:137 #: ../vnc.html:127 ../vnc.html:133
msgid "Clipboard" msgid "Clipboard"
msgstr "Urklipp" msgstr "Urklipp"
#: ../vnc.html:141 #: ../vnc.html:135
msgid "Clear" msgid "Edit clipboard content in the textarea below."
msgstr "Rensa" msgstr "Redigera urklippets innehåll i fältet nedan."
#: ../vnc.html:147 #: ../vnc.html:143
msgid "Fullscreen" msgid "Full Screen"
msgstr "Fullskärm" msgstr "Fullskärm"
#: ../vnc.html:152 ../vnc.html:159 #: ../vnc.html:148 ../vnc.html:154
msgid "Settings" msgid "Settings"
msgstr "Inställningar" msgstr "Inställningar"
#: ../vnc.html:162 #: ../vnc.html:158
msgid "Shared Mode" msgid "Shared Mode"
msgstr "Delat Läge" msgstr "Delat Läge"
#: ../vnc.html:165 #: ../vnc.html:161
msgid "View Only" msgid "View Only"
msgstr "Endast Visning" msgstr "Endast Visning"
#: ../vnc.html:169 #: ../vnc.html:165
msgid "Clip to Window" msgid "Clip to Window"
msgstr "Begränsa till Fönster" msgstr "Begränsa till Fönster"
#: ../vnc.html:172 #: ../vnc.html:168
msgid "Scaling Mode:" msgid "Scaling Mode:"
msgstr "Skalningsläge:" msgstr "Skalningsläge:"
#: ../vnc.html:174 #: ../vnc.html:170
msgid "None" msgid "None"
msgstr "Ingen" msgstr "Ingen"
#: ../vnc.html:175 #: ../vnc.html:171
msgid "Local Scaling" msgid "Local Scaling"
msgstr "Lokal Skalning" msgstr "Lokal Skalning"
#: ../vnc.html:176 #: ../vnc.html:172
msgid "Remote Resizing" msgid "Remote Resizing"
msgstr "Ändra Storlek" msgstr "Ändra Storlek"
#: ../vnc.html:181 #: ../vnc.html:177
msgid "Advanced" msgid "Advanced"
msgstr "Avancerat" msgstr "Avancerat"
#: ../vnc.html:184 #: ../vnc.html:180
msgid "Quality:" msgid "Quality:"
msgstr "Kvalitet:" msgstr "Kvalitet:"
#: ../vnc.html:188 #: ../vnc.html:184
msgid "Compression level:" msgid "Compression level:"
msgstr "Kompressionsnivå:" msgstr "Kompressionsnivå:"
#: ../vnc.html:193 #: ../vnc.html:189
msgid "Repeater ID:" msgid "Repeater ID:"
msgstr "Repeater-ID:" msgstr "Repeater-ID:"
#: ../vnc.html:197 #: ../vnc.html:193
msgid "WebSocket" msgid "WebSocket"
msgstr "WebSocket" msgstr "WebSocket"
#: ../vnc.html:200 #: ../vnc.html:196
msgid "Encrypt" msgid "Encrypt"
msgstr "Kryptera" msgstr "Kryptera"
#: ../vnc.html:203 #: ../vnc.html:199
msgid "Host:" msgid "Host:"
msgstr "Värd:" msgstr "Värd:"
#: ../vnc.html:207 #: ../vnc.html:203
msgid "Port:" msgid "Port:"
msgstr "Port:" msgstr "Port:"
#: ../vnc.html:211 #: ../vnc.html:207
msgid "Path:" msgid "Path:"
msgstr "Sökväg:" msgstr "Sökväg:"
#: ../vnc.html:218 #: ../vnc.html:214
msgid "Automatic Reconnect" msgid "Automatic Reconnect"
msgstr "Automatisk Återanslutning" msgstr "Automatisk Återanslutning"
#: ../vnc.html:221 #: ../vnc.html:217
msgid "Reconnect Delay (ms):" msgid "Reconnect Delay (ms):"
msgstr "Fördröjning (ms):" msgstr "Fördröjning (ms):"
#: ../vnc.html:226 #: ../vnc.html:222
msgid "Show Dot when No Cursor" msgid "Show Dot when No Cursor"
msgstr "Visa prick när ingen muspekare finns" msgstr "Visa prick när ingen muspekare finns"
#: ../vnc.html:231 #: ../vnc.html:227
msgid "Logging:" msgid "Logging:"
msgstr "Loggning:" msgstr "Loggning:"
#: ../vnc.html:240 #: ../vnc.html:236
msgid "Version:" msgid "Version:"
msgstr "Version:" msgstr "Version:"
#: ../vnc.html:248 #: ../vnc.html:244
msgid "Disconnect" msgid "Disconnect"
msgstr "Koppla från" msgstr "Koppla från"
@ -283,18 +287,53 @@ msgstr "Koppla från"
msgid "Connect" msgid "Connect"
msgstr "Anslut" 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:" msgid "Username:"
msgstr "Användarnamn:" msgstr "Användarnamn:"
#: ../vnc.html:281 #: ../vnc.html:308
msgid "Password:" msgid "Password:"
msgstr "Lösenord:" msgstr "Lösenord:"
#: ../vnc.html:285 #: ../vnc.html:312
msgid "Send Credentials" msgid "Send Credentials"
msgstr "Skicka Användaruppgifter" msgstr "Skicka Användaruppgifter"
#: ../vnc.html:295 #: ../vnc.html:321
msgid "Cancel" msgid "Cancel"
msgstr "Avbryt" msgstr "Avbryt"
#~ msgid "Clear"
#~ msgstr "Rensa"

View File

@ -42,7 +42,7 @@ parts:
- jq - jq
websockify: 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 plugin: python
stage-packages: stage-packages:
- python3-numpy - python3-numpy

View File

@ -42,7 +42,7 @@ export default class FakeWebSocket {
} }
_getSentData() { _getSentData() {
const res = new Uint8Array(this._sendQueue.buffer, 0, this.bufferedAmount); const res = this._sendQueue.slice(0, this.bufferedAmount);
this.bufferedAmount = 0; this.bufferedAmount = 0;
return res; return res;
} }
@ -55,11 +55,15 @@ export default class FakeWebSocket {
} }
_receiveData(data) { _receiveData(data) {
if (data.length < 4096) {
// Break apart the data to expose bugs where we assume data is // Break apart the data to expose bugs where we assume data is
// neatly packaged // neatly packaged
for (let i = 0;i < data.length;i++) { for (let i = 0;i < data.length;i++) {
let buf = data.subarray(i, i+1); let buf = data.slice(i, i+1);
this.onmessage(new MessageEvent("message", { 'data': buf })); this.onmessage(new MessageEvent("message", { 'data': buf.buffer }));
}
} else {
this.onmessage(new MessageEvent("message", { 'data': data.buffer }));
} }
} }
} }

View File

@ -24,7 +24,7 @@ function loadFile() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
script.onload = resolve; script.onload = resolve;
script.onerror = reject; script.onerror = () => { reject("Failed to load " + fname); };
document.body.appendChild(script); document.body.appendChild(script);
script.src = "../recordings/" + fname; script.src = "../recordings/" + fname;
}); });
@ -200,6 +200,9 @@ function start() {
player.onrfbdisconnected = (evt) => { player.onrfbdisconnected = (evt) => {
if (!evt.detail.clean) { if (!evt.detail.clean) {
message(`noVNC sent disconnected during iteration ${evt.detail.iteration} frame ${evt.detail.frame}`); 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) => { player.onfinish = (evt) => {

View File

@ -131,12 +131,10 @@ export default class RecordingPlayer {
_doPacket() { _doPacket() {
// Avoid having excessive queue buildup in non-realtime mode // Avoid having excessive queue buildup in non-realtime mode
if (this._trafficManagement && this._rfb._flushing) { if (this._trafficManagement && this._rfb._flushing) {
const orig = this._rfb._display.onflush; this._rfb.flush()
this._rfb._display.onflush = () => { .then(() => {
this._rfb._display.onflush = orig;
this._rfb._onFlush();
this._doPacket(); this._doPacket();
}; });
return; return;
} }
@ -150,13 +148,8 @@ export default class RecordingPlayer {
_finish() { _finish() {
if (this._rfb._display.pending()) { if (this._rfb._display.pending()) {
this._rfb._display.onflush = () => { this._rfb._display.flush()
if (this._rfb._flushing) { .then(() => { this._finish(); });
this._rfb._onFlush();
}
this._finish();
};
this._rfb._display.flush();
} else { } else {
this._running = false; this._running = false;
this._ws.onclose({code: 1000, reason: ""}); this._ws.onclose({code: 1000, reason: ""});

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('CopyRect Decoder', function () { describe('CopyRect Decoder', function () {
@ -47,12 +50,15 @@ describe('CopyRect Decoder', function () {
display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]); display.fillRect(0, 0, 2, 2, [ 0x00, 0x00, 0xff ]);
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 0, 2, 2, 2, let done;
done = testDecodeRect(decoder, 0, 2, 2, 2,
[0x00, 0x02, 0x00, 0x00], [0x00, 0x02, 0x00, 0x00],
display, 24); display, 24);
testDecodeRect(decoder, 2, 2, 2, 2, expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 2, 2, 2,
[0x00, 0x00, 0x00, 0x00], [0x00, 0x00, 0x00, 0x00],
display, 24); display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
}); });

View File

@ -298,14 +298,11 @@ describe('Display/Canvas Helper', function () {
expect(display).to.have.displayed(checkedData); 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.imageRect(0, 0, 4, 4, "image/png", makeImagePng(checkedData, 4, 4));
display.flip(); display.flip();
display.onflush = () => { await display.flush();
expect(display).to.have.displayed(checkedData); expect(display).to.have.displayed(checkedData);
done();
};
display.flush();
}); });
it('should support blit images with true color via #blitImage', function () { 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; expect(img.addEventListener).to.have.been.calledOnce;
}); });
it('should call callback when queue is flushed', function () { it('should resolve promise when queue is flushed', async function () {
display.onflush = sinon.spy();
display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); display.fillRect(0, 0, 4, 4, [0, 0xff, 0]);
expect(display.onflush).to.not.have.been.called; let promise = display.flush();
display.flush(); expect(promise).to.be.an.instanceOf(Promise);
expect(display.onflush).to.have.been.calledOnce; await promise;
}); });
it('should draw a blit image on type "blit"', function () { it('should draw a blit image on type "blit"', function () {

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
function push32(arr, num) { function push32(arr, num) {
@ -62,7 +65,7 @@ describe('Hextile Decoder', function () {
data.push(2 | (2 << 4)); // x: 2, y: 2 data.push(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -92,8 +96,9 @@ describe('Hextile Decoder', function () {
data.push(0); 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); expect(display).to.have.displayed(targetData);
}); });
@ -102,13 +107,14 @@ describe('Hextile Decoder', function () {
data.push(0x02); data.push(0x02);
push32(data, 0x00ff0000); // becomes 00ff0000 --> #00FF00 bg color 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 = []; let expected = [];
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
push32(expected, 0x00ff00ff); push32(expected, 0x00ff00ff);
} }
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected)); expect(display).to.have.displayed(new Uint8Array(expected));
}); });
@ -125,7 +131,7 @@ describe('Hextile Decoder', function () {
// send an empty frame // send an empty frame
data.push(0x00); 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 = []; let expected = [];
for (let i = 0; i < 16; i++) { for (let i = 0; i < 16; i++) {
@ -135,6 +141,7 @@ describe('Hextile Decoder', function () {
push32(expected, 0x00ff00ff); // rect 2: same bkground color push32(expected, 0x00ff00ff); // rect 2: same bkground color
} }
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected)); 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(2 | (2 << 4)); // x: 2, y: 2
data.push(1 | (1 << 4)); // width: 2, height: 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -190,7 +198,7 @@ describe('Hextile Decoder', function () {
data.push(0); // x: 0, y: 0 data.push(0); // x: 0, y: 0
data.push(1 | (1 << 4)); // width: 2, height: 2 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 = [ let targetData = [
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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)); expected = expected.concat(targetData.slice(0, 16));
expect(done).to.be.true;
expect(display).to.have.displayed(new Uint8Array(expected)); 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
}); });

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('JPEG Decoder', function () { describe('JPEG Decoder', function () {
@ -41,7 +44,7 @@ describe('JPEG Decoder', function () {
display.resize(4, 4); display.resize(4, 4);
}); });
it('should handle JPEG rects', function (done) { it('should handle JPEG rects', async function () {
let data = [ let data = [
// JPEG data // JPEG data
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
@ -131,7 +134,8 @@ describe('JPEG Decoder', function () {
0xff, 0xd9, 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([ let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
@ -147,14 +151,11 @@ describe('JPEG Decoder', function () {
return diff < 5; return diff < 5;
} }
display.onflush = () => { await display.flush();
expect(display).to.have.displayed(targetData, almost); expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
}); });
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 = [ let data1 = [
// JPEG data // JPEG data
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46,
@ -244,7 +245,12 @@ describe('JPEG Decoder', function () {
0xff, 0xd9, 0xff, 0xd9,
]; ];
testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24); let decodeDone;
decodeDone = testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24);
expect(decodeDone).to.be.true;
display.fillRect(0, 0, 4, 4, [128, 128, 128, 255]);
let data2 = [ let data2 = [
// JPEG data // JPEG data
@ -263,7 +269,8 @@ describe('JPEG Decoder', function () {
0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9, 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([ let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255,
@ -279,10 +286,7 @@ describe('JPEG Decoder', function () {
return diff < 5; return diff < 5;
} }
display.onflush = () => { await display.flush();
expect(display).to.have.displayed(targetData, almost); expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
}); });
}); });

View File

@ -1,11 +1,12 @@
const expect = chai.expect; const expect = chai.expect;
import { l10n } from '../app/localization.js'; import _, { Localizer, l10n } from '../app/localization.js';
describe('Localization', function () { describe('Localization', function () {
"use strict"; "use strict";
describe('language selection', function () {
let origNavigator; let origNavigator;
let fetch;
beforeEach(function () { beforeEach(function () {
// window.navigator is a protected read-only property in many // window.navigator is a protected read-only property in many
// environments, so we need to redefine it whilst running these // environments, so we need to redefine it whilst running these
@ -14,48 +15,132 @@ describe('Localization', function () {
Object.defineProperty(window, "navigator", {value: {}}); Object.defineProperty(window, "navigator", {value: {}});
window.navigator.languages = []; window.navigator.languages = [];
fetch = sinon.stub(window, "fetch");
fetch.resolves(new Response("{}"));
}); });
afterEach(function () { afterEach(function () {
fetch.restore();
Object.defineProperty(window, "navigator", origNavigator); Object.defineProperty(window, "navigator", origNavigator);
}); });
describe('Singleton', function () {
it('should export a singleton object', function () {
expect(l10n).to.be.instanceOf(Localizer);
});
it('should export a singleton translation function', async function () {
// FIXME: Can we use some spy instead?
window.navigator.languages = ["de"];
fetch.resolves(new Response(JSON.stringify({ "Foobar": "gazonk" })));
await l10n.setup(["de"]);
expect(_("Foobar")).to.equal("gazonk");
});
});
describe('language selection', function () {
it('should use English by default', 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 () { it('should use English if no user language matches', async function () {
window.navigator.languages = ["nl", "de"]; window.navigator.languages = ["nl", "de"];
l10n.setup(["es", "fr"]); let lclz = new Localizer();
expect(l10n.language).to.equal('en'); await lclz.setup(["es", "fr"]);
expect(lclz.language).to.equal('en');
}); });
it('should use the most preferred user language', function () { it('should fall back to generic English for other English', async function () {
window.navigator.languages = ["en-AU", "de"];
let lclz = new Localizer();
await lclz.setup(["de", "fr", "en-GB"]);
expect(lclz.language).to.equal('en');
});
it('should prefer specific English over generic', async function () {
window.navigator.languages = ["en-GB", "de"];
let lclz = new Localizer();
await lclz.setup(["de", "en-AU", "en-GB"]);
expect(lclz.language).to.equal('en-GB');
});
it('should use the most preferred user language', async function () {
window.navigator.languages = ["nl", "de", "fr"]; window.navigator.languages = ["nl", "de", "fr"];
l10n.setup(["es", "fr", "de"]); let lclz = new Localizer();
expect(l10n.language).to.equal('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"]; window.navigator.languages = ["pt-BR"];
l10n.setup(["pt", "pt-BR"]); let lclz = new Localizer();
expect(l10n.language).to.equal('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"]; window.navigator.languages = ["pt-BR"];
l10n.setup(["fr", "pt", "de"]); let lclz = new Localizer();
expect(l10n.language).to.equal('pt'); 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"]; window.navigator.languages = ["pt", "de"];
l10n.setup(["fr", "pt-BR", "de"]); let lclz = new Localizer();
expect(l10n.language).to.equal('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"]; window.navigator.languages = ["pt-BR"];
l10n.setup(["pt_BR"]); let lclz = new Localizer();
expect(l10n.language).to.equal('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"]; window.navigator.languages = ["pt-br"];
l10n.setup(["pt-BR"]); let lclz = new Localizer();
expect(l10n.language).to.equal('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;
}); });
}); });
}); });

View File

@ -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" });
});
});

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('Raw Decoder', function () { describe('Raw Decoder', function () {
@ -42,22 +45,36 @@ describe('Raw Decoder', function () {
}); });
it('should handle the Raw encoding', function () { it('should handle the Raw encoding', function () {
testDecodeRect(decoder, 0, 0, 2, 2, let done;
[0xff, 0x00, 0x00, 0, 0x00, 0xff, 0x00, 0,
0x00, 0xff, 0x00, 0, 0xff, 0x00, 0x00, 0], 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); display, 24);
testDecodeRect(decoder, 2, 0, 2, 2, expect(done).to.be.true;
[0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0, done = testDecodeRect(decoder, 2, 0, 2, 2,
0x00, 0x00, 0xff, 0, 0x00, 0x00, 0xff, 0], [0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0,
0x00, 0x00, 0xff, 0],
display, 24); display, 24);
testDecodeRect(decoder, 0, 2, 4, 1, expect(done).to.be.true;
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0, done = testDecodeRect(decoder, 0, 2, 4, 1,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0], [0xee, 0x00, 0xff, 0,
0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0,
0xab, 0xee, 0xff, 0],
display, 24); display, 24);
testDecodeRect(decoder, 0, 3, 4, 1, expect(done).to.be.true;
[0xee, 0x00, 0xff, 0, 0x00, 0xee, 0xff, 0, done = testDecodeRect(decoder, 0, 3, 4, 1,
0xaa, 0xee, 0xff, 0, 0xab, 0xee, 0xff, 0], [0xee, 0x00, 0xff, 0,
0x00, 0xee, 0xff, 0,
0xaa, 0xee, 0xff, 0,
0xab, 0xee, 0xff, 0],
display, 24); display, 24);
expect(done).to.be.true;
let targetData = new Uint8Array([ let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 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 () { it('should handle the Raw encoding in low colour mode', function () {
testDecodeRect(decoder, 0, 0, 2, 2, let done;
done = testDecodeRect(decoder, 0, 0, 2, 2,
[0x30, 0x30, 0x30, 0x30], [0x30, 0x30, 0x30, 0x30],
display, 8); display, 8);
testDecodeRect(decoder, 2, 0, 2, 2, expect(done).to.be.true;
done = testDecodeRect(decoder, 2, 0, 2, 2,
[0x0c, 0x0c, 0x0c, 0x0c], [0x0c, 0x0c, 0x0c, 0x0c],
display, 8); display, 8);
testDecodeRect(decoder, 0, 2, 4, 1, expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 2, 4, 1,
[0x0c, 0x0c, 0x30, 0x30], [0x0c, 0x0c, 0x30, 0x30],
display, 8); display, 8);
testDecodeRect(decoder, 0, 3, 4, 1, expect(done).to.be.true;
done = testDecodeRect(decoder, 0, 3, 4, 1,
[0x0c, 0x0c, 0x30, 0x30], [0x0c, 0x0c, 0x30, 0x30],
display, 8); display, 8);
expect(done).to.be.true;
let targetData = new Uint8Array([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
}); });

File diff suppressed because it is too large Load Diff

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
function push16(arr, num) { function push16(arr, num) {
@ -76,7 +79,7 @@ describe('RRE Decoder', function () {
push16(data, 2); // width: 2 push16(data, 2); // width: 2
push16(data, 2); // height: 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -93,7 +97,10 @@ describe('RRE Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, [ 0x00, 0xff, 0xff, 0xff, 0xff ], display, 24); let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x00, 0x00, 0x00, 0x00,
0xff, 0xff, 0xff, 0xff ],
display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@ -102,6 +109,7 @@ describe('RRE Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 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); expect(display).to.have.displayed(targetData);
}); });
}); });

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('Tight Decoder', function () { describe('Tight Decoder', function () {
@ -42,7 +45,7 @@ describe('Tight Decoder', function () {
}); });
it('should handle fill rects', function () { it('should handle fill rects', function () {
testDecodeRect(decoder, 0, 0, 4, 4, let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x80, 0xff, 0x88, 0x44], [0x80, 0xff, 0x88, 0x44],
display, 24); display, 24);
@ -53,21 +56,31 @@ describe('Tight Decoder', function () {
0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 0xff, 0x88, 0x44, 255, 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle uncompressed copy rects', function () { it('should handle uncompressed copy rects', function () {
let done;
let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ]; let blueData = [ 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0xff ];
let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ]; let greenData = [ 0x00, 0x00, 0xff, 0x00, 0x00, 0xff, 0x00 ];
testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24); done = testDecodeRect(decoder, 0, 0, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24); expect(done).to.be.true;
testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24); done = testDecodeRect(decoder, 0, 1, 2, 1, blueData, display, 24);
testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24); expect(done).to.be.true;
testDecodeRect(decoder, 0, 2, 2, 1, greenData, display, 24); done = testDecodeRect(decoder, 2, 0, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 0, 3, 2, 1, greenData, display, 24); expect(done).to.be.true;
testDecodeRect(decoder, 2, 2, 2, 1, blueData, display, 24); done = testDecodeRect(decoder, 2, 1, 2, 1, greenData, display, 24);
testDecodeRect(decoder, 2, 3, 2, 1, blueData, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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, 0x60, 0x82, 0x01, 0x99, 0x8d, 0x29, 0x02, 0xa6,
0x00, 0x7e, 0xbf, 0x0f, 0xf1 ]; 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -110,7 +124,7 @@ describe('Tight Decoder', function () {
// Pixels // Pixels
0x30, 0x30, 0xc0, 0xc0 ]; 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -135,7 +150,7 @@ describe('Tight Decoder', function () {
0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00, 0x78, 0x9c, 0x33, 0x30, 0x38, 0x70, 0xc0, 0x00,
0x8a, 0x01, 0x21, 0x3c, 0x05, 0xa1 ]; 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle uncompressed palette rects', function () { it('should handle uncompressed palette rects', function () {
let done;
let data1 = [ let data1 = [
// Control bytes // Control bytes
0x40, 0x01, 0x40, 0x01,
@ -171,8 +188,10 @@ describe('Tight Decoder', function () {
// Pixels // Pixels
0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ]; 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00 ];
testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24); done = testDecodeRect(decoder, 0, 0, 4, 2, data1, display, 24);
testDecodeRect(decoder, 0, 2, 4, 2, data2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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, 0x62, 0x08, 0xc9, 0xc0, 0x00, 0x00, 0x00, 0x54,
0x00, 0x09 ]; 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 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([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 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 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); expect(display).to.have.displayed(targetData);
}); });
@ -238,7 +259,7 @@ describe('Tight Decoder', function () {
display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x40, 0x01, 0x01, [ 0x40, 0x01, 0x01,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff ], display, 24); 0xff, 0xff, 0xff ], display, 24);
@ -250,6 +271,7 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 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); 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(2, 0, 2, 2, [ 0x00, 0xff, 0x00 ]);
display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]); display.fillRect(0, 2, 2, 2, [ 0x00, 0xff, 0x00 ]);
testDecodeRect(decoder, 1, 2, 0, 0, let done = testDecodeRect(decoder, 1, 2, 0, 0,
[ 0x80, 0xff, 0xff, 0xff ], display, 24); [ 0x80, 0xff, 0xff, 0xff ],
display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@ -268,10 +291,11 @@ describe('Tight Decoder', function () {
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle JPEG rects', function (done) { it('should handle JPEG rects', async function () {
let data = [ let data = [
// Control bytes // Control bytes
0x90, 0xd6, 0x05, 0x90, 0xd6, 0x05,
@ -369,7 +393,8 @@ describe('Tight Decoder', function () {
0x3f, 0xeb, 0xff, 0x00, 0xff, 0xd9, 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([ let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@ -385,10 +410,7 @@ describe('Tight Decoder', function () {
return diff < 5; return diff < 5;
} }
display.onflush = () => { await display.flush();
expect(display).to.have.displayed(targetData, almost); expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
}); });
}); });

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('TightPng Decoder', function () { describe('TightPng Decoder', function () {
@ -41,7 +44,7 @@ describe('TightPng Decoder', function () {
display.resize(4, 4); display.resize(4, 4);
}); });
it('should handle the TightPng encoding', function (done) { it('should handle the TightPng encoding', async function () {
let data = [ let data = [
// Control bytes // Control bytes
0xa0, 0xb4, 0x04, 0xa0, 0xb4, 0x04,
@ -119,7 +122,8 @@ describe('TightPng Decoder', function () {
0xae, 0x42, 0x60, 0x82, 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([ let targetData = new Uint8Array([
0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
@ -135,10 +139,7 @@ describe('TightPng Decoder', function () {
return diff < 30; return diff < 30;
} }
display.onflush = () => { await display.flush();
expect(display).to.have.displayed(targetData, almost); expect(display).to.have.displayed(targetData, almost);
done();
};
display.flush();
}); });
}); });

View File

@ -6,92 +6,64 @@ import FakeWebSocket from './fake.websocket.js';
describe('Websock', function () { describe('Websock', function () {
"use strict"; "use strict";
describe('Queue methods', function () { describe('Receive queue methods', function () {
let sock; let sock, websock;
const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
beforeEach(function () { beforeEach(function () {
sock = new Websock(); sock = new Websock();
// skip init websock = new FakeWebSocket();
sock._allocateBuffers(); websock._open();
sock._rQ.set(RQ_TEMPLATE); sock.attach(websock);
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 () { describe('rQpeek8', function () {
it('should peek at the next byte without poping it off the queue', function () { it('should peek at the next byte without poping it off the queue', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd]));
const peek = sock.rQpeek8(); expect(sock.rQpeek8()).to.equal(0xab);
expect(sock.rQpeek8()).to.equal(peek); expect(sock.rQpeek8()).to.equal(0xab);
expect(sock.rQlen).to.equal(befLen);
}); });
}); });
describe('rQshift8()', function () { describe('rQshift8()', function () {
it('should pop a single byte from the receive queue', function () { it('should pop a single byte from the receive queue', function () {
const peek = sock.rQpeek8(); websock._receiveData(new Uint8Array([0xab, 0xcd]));
const befLen = sock.rQlen; expect(sock.rQshift8()).to.equal(0xab);
expect(sock.rQshift8()).to.equal(peek); expect(sock.rQshift8()).to.equal(0xcd);
expect(sock.rQlen).to.equal(befLen - 1);
}); });
}); });
describe('rQshift16()', function () { describe('rQshift16()', function () {
it('should pop two bytes from the receive queue and return a single number', function () { it('should pop two bytes from the receive queue and return a single number', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; expect(sock.rQshift16()).to.equal(0xabcd);
expect(sock.rQshift16()).to.equal(expected); expect(sock.rQshift16()).to.equal(0x1234);
expect(sock.rQlen).to.equal(befLen - 2);
}); });
}); });
describe('rQshift32()', function () { describe('rQshift32()', function () {
it('should pop four bytes from the receive queue and return a single number', function () { it('should pop four bytes from the receive queue and return a single number', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
const expected = (RQ_TEMPLATE[0] << 24) + 0x88, 0xee, 0x11, 0x33]));
(RQ_TEMPLATE[1] << 16) + expect(sock.rQshift32()).to.equal(0xabcd1234);
(RQ_TEMPLATE[2] << 8) + expect(sock.rQshift32()).to.equal(0x88ee1133);
RQ_TEMPLATE[3];
expect(sock.rQshift32()).to.equal(expected);
expect(sock.rQlen).to.equal(befLen - 4);
}); });
}); });
describe('rQshiftStr', function () { describe('rQshiftStr', function () {
it('should shift the given number of bytes off of the receive queue and return a string', function () { it('should shift the given number of bytes off of the receive queue and return a string', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
const befRQi = sock.rQi; 0x88, 0xee, 0x11, 0x33]));
const shifted = sock.rQshiftStr(3); expect(sock.rQshiftStr(4)).to.equal('\xab\xcd\x12\x34');
expect(shifted).to.be.a('string'); expect(sock.rQshiftStr(4)).to.equal('\x88\xee\x11\x33');
expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3))));
expect(sock.rQlen).to.equal(befLen - 3);
});
it('should shift the entire rest of the queue off if no length is given', function () {
sock.rQshiftStr();
expect(sock.rQlen).to.equal(0);
}); });
it('should be able to handle very large strings', function () { it('should be able to handle very large strings', function () {
const BIG_LEN = 500000; const BIG_LEN = 500000;
const RQ_BIG = new Uint8Array(BIG_LEN); const incoming = new Uint8Array(BIG_LEN);
let expected = ""; let expected = "";
let letterCode = 'a'.charCodeAt(0); let letterCode = 'a'.charCodeAt(0);
for (let i = 0; i < BIG_LEN; i++) { for (let i = 0; i < BIG_LEN; i++) {
RQ_BIG[i] = letterCode; incoming[i] = letterCode;
expected += String.fromCharCode(letterCode); expected += String.fromCharCode(letterCode);
if (letterCode < 'z'.charCodeAt(0)) { if (letterCode < 'z'.charCodeAt(0)) {
@ -100,146 +72,282 @@ describe('Websock', function () {
letterCode = 'a'.charCodeAt(0); letterCode = 'a'.charCodeAt(0);
} }
} }
sock._rQ.set(RQ_BIG); websock._receiveData(incoming);
sock._rQlen = RQ_BIG.length;
const shifted = sock.rQshiftStr(); const shifted = sock.rQshiftStr(BIG_LEN);
expect(shifted).to.be.equal(expected); expect(shifted).to.be.equal(expected);
expect(sock.rQlen).to.equal(0);
}); });
}); });
describe('rQshiftBytes', function () { describe('rQshiftBytes', function () {
it('should shift the given number of bytes of the receive queue and return an array', function () { it('should shift the given number of bytes of the receive queue and return an array', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
const befRQi = sock.rQi; 0x88, 0xee, 0x11, 0x33]));
const shifted = sock.rQshiftBytes(3); expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(shifted).to.be.an.instanceof(Uint8Array); expect(sock.rQshiftBytes(4)).to.array.equal(new Uint8Array([0x88, 0xee, 0x11, 0x33]));
expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, befRQi, 3));
expect(sock.rQlen).to.equal(befLen - 3);
}); });
it('should shift the entire rest of the queue off if no length is given', function () { it('should return a shared array if requested', function () {
sock.rQshiftBytes(); websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
expect(sock.rQlen).to.equal(0); 0x88, 0xee, 0x11, 0x33]));
const bytes = sock.rQshiftBytes(4, false);
expect(bytes).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(bytes.buffer.byteLength).to.not.equal(bytes.length);
}); });
}); });
describe('rQslice', function () { describe('rQpeekBytes', function () {
beforeEach(function () {
sock.rQi = 0;
});
it('should not modify the receive queue', function () { it('should not modify the receive queue', function () {
const befLen = sock.rQlen; websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
sock.rQslice(0, 2); 0x88, 0xee, 0x11, 0x33]));
expect(sock.rQlen).to.equal(befLen); expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
expect(sock.rQpeekBytes(4)).to.array.equal(new Uint8Array([0xab, 0xcd, 0x12, 0x34]));
}); });
it('should return an array containing the given slice of the receive queue', function () { it('should return a shared array if requested', function () {
const sl = sock.rQslice(0, 2); websock._receiveData(new Uint8Array([0xab, 0xcd, 0x12, 0x34,
expect(sl).to.be.an.instanceof(Uint8Array); 0x88, 0xee, 0x11, 0x33]));
expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); 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);
it('should use the rest of the receive queue if no end is given', function () {
const sl = sock.rQslice(1);
expect(sl).to.have.length(RQ_TEMPLATE.length - 1);
expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1));
});
it('should take the current rQi in to account', function () {
sock.rQi = 1;
expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2));
}); });
}); });
describe('rQwait', function () { describe('rQwait', function () {
beforeEach(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 () { 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 () { 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 () { it('should return true and reduce rQi by "goback" if there are not enough bytes', function () {
sock.rQi = 5; expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; expect(sock.rQwait('hi', 8, 2)).to.be.true;
expect(sock.rQi).to.equal(1); expect(sock.rQshift32()).to.equal(0x123488ee);
}); });
it('should raise an error if we try to go back more than possible', function () { it('should raise an error if we try to go back more than possible', function () {
sock.rQi = 5; expect(sock.rQshift32()).to.equal(0xabcd1234);
expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); expect(() => sock.rQwait('hi', 8, 6)).to.throw(Error);
}); });
it('should not reduce rQi if there are enough bytes', function () { it('should not reduce rQi if there are enough bytes', function () {
sock.rQi = 5; expect(sock.rQshift32()).to.equal(0xabcd1234);
sock.rQwait('hi', 1, 6); expect(sock.rQwait('hi', 4, 2)).to.be.false;
expect(sock.rQi).to.equal(5); expect(sock.rQshift32()).to.equal(0x88ee1133);
});
});
});
describe('Send queue methods', function () {
let sock;
const bufferSize = 10 * 1024;
beforeEach(function () {
let websock = new FakeWebSocket();
websock._open();
sock = new Websock();
sock.attach(websock);
});
describe('sQpush8()', function () {
it('should send a single byte', function () {
sock.sQpush8(42);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([42]));
});
it('should not send any data until flushing', function () {
sock.sQpush8(42);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize;i++) {
sock.sQpush8(42);
}
let expected = [];
for (let i = 0;i < bufferSize;i++) {
expected.push(42);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpush16()', function () {
it('should send a number as two bytes', function () {
sock.sQpush16(420);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([1, 164]));
});
it('should not send any data until flushing', function () {
sock.sQpush16(420);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/2;i++) {
sock.sQpush16(420);
}
let expected = [];
for (let i = 0;i < bufferSize/2;i++) {
expected.push(1);
expected.push(164);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpush32()', function () {
it('should send a number as two bytes', function () {
sock.sQpush32(420420);
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0, 6, 106, 68]));
});
it('should not send any data until flushing', function () {
sock.sQpush32(420420);
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/4;i++) {
sock.sQpush32(420420);
}
let expected = [];
for (let i = 0;i < bufferSize/4;i++) {
expected.push(0);
expected.push(6);
expected.push(106);
expected.push(68);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpushString()', function () {
it('should send a string buffer', function () {
sock.sQpushString('\x12\x34\x56\x78\x90');
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
});
it('should not send any data until flushing', function () {
sock.sQpushString('\x12\x34\x56\x78\x90');
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/5;i++) {
sock.sQpushString('\x12\x34\x56\x78\x90');
}
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
it('should implicitly split a large buffer', function () {
let str = '';
for (let i = 0;i <= bufferSize/5;i++) {
str += '\x12\x34\x56\x78\x90';
}
sock.sQpushString(str);
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
});
describe('sQpushBytes()', function () {
it('should send a byte buffer', function () {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
sock.flush();
expect(sock).to.have.sent(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
});
it('should not send any data until flushing', function () {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
expect(sock).to.have.sent(new Uint8Array([]));
});
it('should implicitly flush if the queue is full', function () {
for (let i = 0;i <= bufferSize/5;i++) {
sock.sQpushBytes(new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90]));
}
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
it('should implicitly split a large buffer', function () {
let buffer = [];
for (let i = 0;i <= bufferSize/5;i++) {
buffer.push(0x12);
buffer.push(0x34);
buffer.push(0x56);
buffer.push(0x78);
buffer.push(0x90);
}
sock.sQpushBytes(new Uint8Array(buffer));
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
}); });
}); });
describe('flush', function () { describe('flush', function () {
beforeEach(function () {
sock._websocket = {
send: sinon.spy()
};
});
it('should actually send on the websocket', 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._sQ = new Uint8Array([1, 2, 3]);
sock._sQlen = 3; sock._sQlen = 3;
const encoded = sock._encodeMessage();
sock.flush(); sock.flush();
expect(sock._websocket.send).to.have.been.calledOnce; expect(sock).to.have.sent(new Uint8Array([1, 2, 3]));
expect(sock._websocket.send).to.have.been.calledWith(encoded);
}); });
it('should not call send if we do not have anything queued up', function () { it('should not call send if we do not have anything queued up', function () {
sock._sQlen = 0; sock._sQlen = 0;
sock._websocket.bufferedAmount = 8;
sock.flush(); 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 () {
sock.send([1, 2, 3]);
expect(sock.flush).to.have.been.calledOnce;
});
});
describe('sendString', function () {
beforeEach(function () {
sock.send = sinon.spy();
});
it('should call send after converting the string to an array', function () {
sock.sendString("\x01\x02\x03");
expect(sock.send).to.have.been.calledWith([1, 2, 3]);
}); });
}); });
}); });
@ -450,9 +558,8 @@ describe('Websock', function () {
sock._allocateBuffers(); 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]) }; const msg = { data: new Uint8Array([1, 2, 3]) };
sock._mode = 'binary';
sock._recvMessage(msg); sock._recvMessage(msg);
expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03');
}); });
@ -473,80 +580,49 @@ describe('Websock', function () {
expect(sock._eventHandlers.message).not.to.have.been.called; expect(sock._eventHandlers.message).not.to.have.been.called;
}); });
it('should compact the receive queue when a message handler empties it', function () { it('should compact the receive queue when fully read', function () {
sock._eventHandlers.message = () => { sock.rQi = sock._rQlen; };
sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]);
sock._rQlen = 6; sock._rQlen = 6;
sock.rQi = 6; sock._rQi = 6;
const msg = { data: new Uint8Array([1, 2, 3]).buffer }; const msg = { data: new Uint8Array([1, 2, 3]).buffer };
sock._mode = 'binary';
sock._recvMessage(msg); sock._recvMessage(msg);
expect(sock._rQlen).to.equal(0); expect(sock._rQlen).to.equal(3);
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 () { it('should compact the receive queue when we reach the end of the buffer', function () {
sock._rQ = new Uint8Array(20); sock._rQ = new Uint8Array(20);
sock._rQbufferSize = 20; sock._rQbufferSize = 20;
sock._rQlen = 20; sock._rQlen = 20;
sock.rQi = 10; sock._rQi = 10;
const msg = { data: new Uint8Array([1, 2]).buffer }; const msg = { data: new Uint8Array([1, 2]).buffer };
sock._mode = 'binary';
sock._recvMessage(msg); sock._recvMessage(msg);
expect(sock._rQlen).to.equal(12); 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 () { it('should automatically resize the receive queue if the incoming message is larger than the buffer', function () {
sock._rQ = new Uint8Array(20); sock._rQ = new Uint8Array(20);
sock._rQlen = 0; sock._rQlen = 0;
sock.rQi = 0; sock._rQi = 0;
sock._rQbufferSize = 20; sock._rQbufferSize = 20;
const msg = { data: new Uint8Array(30).buffer }; const msg = { data: new Uint8Array(30).buffer };
sock._mode = 'binary';
sock._recvMessage(msg); sock._recvMessage(msg);
expect(sock._rQlen).to.equal(30); 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 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 () { 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._rQ = new Uint8Array(20);
sock._rQlen = 16; sock._rQlen = 16;
sock.rQi = 16; sock._rQi = 15;
sock._rQbufferSize = 20; sock._rQbufferSize = 20;
const msg = { data: new Uint8Array(6).buffer }; const msg = { data: new Uint8Array(6).buffer };
sock._mode = 'binary';
sock._recvMessage(msg); sock._recvMessage(msg);
expect(sock._rQlen).to.equal(6); expect(sock._rQlen).to.equal(7);
expect(sock.rQi).to.equal(0); expect(sock._rQi).to.equal(0);
expect(sock._rQ.length).to.equal(48); expect(sock._rQ.length).to.equal(56);
});
});
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]));
});
}); });
}); });
}); });

View File

@ -8,39 +8,48 @@ describe('WebUtil', function () {
"use strict"; "use strict";
describe('config variables', function () { 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 () { it('should parse query string variables', function () {
// history.pushState() will not cause the browser to attempt loading // history.pushState() will not cause the browser to attempt loading
// the URL, this is exactly what we want here for the tests. // 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"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
}); });
it('should return default value when no query match', function () { 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"); expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
}); });
it('should handle no query match and no default value', function () { 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); expect(WebUtil.getConfigVar("other")).to.be.equal(null);
}); });
it('should parse fragment variables', function () { it('should parse fragment variables', function () {
history.pushState({}, '', "test#myvar=myval"); history.replaceState({}, '', "test#myvar=myval");
expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval");
}); });
it('should return default value when no fragment match', function () { 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"); expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def");
}); });
it('should handle no fragment match and no default value', function () { 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); expect(WebUtil.getConfigVar("other")).to.be.equal(null);
}); });
it('should handle both query and fragment', function () { 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("myquery")).to.be.equal("1");
expect(WebUtil.getConfigVar("myhash")).to.be.equal("2"); expect(WebUtil.getConfigVar("myhash")).to.be.equal("2");
}); });
it('should prioritize fragment if both provide same var', function () { 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"); expect(WebUtil.getConfigVar("myvar")).to.be.equal("2");
}); });
}); });
@ -83,6 +92,11 @@ describe('WebUtil', function () {
expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value');
expect(WebUtil.readSetting('test')).to.equal('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 () { describe('setSetting', function () {
@ -128,6 +142,11 @@ describe('WebUtil', function () {
WebUtil.writeSetting('test', 'something else'); WebUtil.writeSetting('test', 'something else');
expect(WebUtil.readSetting('test')).to.equal('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 // this doesn't appear to be used anywhere
@ -136,6 +155,11 @@ describe('WebUtil', function () {
WebUtil.eraseSetting('test'); WebUtil.eraseSetting('test');
expect(window.localStorage.removeItem).to.have.been.calledWithExactly('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;
});
}); });
}); });

View File

@ -9,23 +9,26 @@ import FakeWebSocket from './fake.websocket.js';
function testDecodeRect(decoder, x, y, width, height, data, display, depth) { function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock; let sock;
let done = false;
sock = new Websock; sock = new Websock;
sock.open("ws://example.com"); sock.open("ws://example.com");
sock.on('message', () => { 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 // Empty messages are filtered at multiple layers, so we need to
// do a direct call // do a direct call
if (data.length === 0) { if (data.length === 0) {
decoder.decodeRect(x, y, width, height, sock, display, depth); done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else { } else {
sock._websocket._receiveData(new Uint8Array(data)); sock._websocket._receiveData(new Uint8Array(data));
} }
display.flip(); display.flip();
return done;
} }
describe('ZRLE Decoder', function () { describe('ZRLE Decoder', function () {
@ -42,8 +45,10 @@ describe('ZRLE Decoder', function () {
}); });
it('should handle the Raw subencoding', function () { it('should handle the Raw subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4, 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], [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e,
0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12,
0x02, 0x00, 0x00, 0x00, 0xff, 0xff],
display, 24); display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
@ -53,12 +58,15 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle the Solid subencoding', function () { it('should handle the Solid subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4, let done = testDecodeRect(decoder, 0, 0, 4, 4,
[0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff], [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e,
0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00,
0x00, 0x00, 0xff, 0xff],
display, 24); display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
@ -68,13 +76,17 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle the Palette Tile subencoding', function () { it('should handle the Palette Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4, 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], [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E,
0x62, 0x62, 0x60, 248, 0xff, 0x9F,
0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff],
display, 24); display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
@ -84,12 +96,15 @@ describe('ZRLE Decoder', function () {
0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle the RLE Tile subencoding', function () { it('should handle the RLE Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4, 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], [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e,
0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00,
0x00, 0x00, 0x00, 0xff, 0xff],
display, 24); display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
@ -99,12 +114,16 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff 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); expect(display).to.have.displayed(targetData);
}); });
it('should handle the RLE Palette Tile subencoding', function () { it('should handle the RLE Palette Tile subencoding', function () {
testDecodeRect(decoder, 0, 0, 4, 4, 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], [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e,
0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f,
0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00,
0x00, 0xff, 0xff],
display, 24); display, 24);
let targetData = new Uint8Array([ let targetData = new Uint8Array([
@ -114,6 +133,7 @@ describe('ZRLE Decoder', function () {
0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff 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); expect(display).to.have.displayed(targetData);
}); });

View File

@ -107,20 +107,13 @@
// query string. If the variable isn't defined in the URL // query string. If the variable isn't defined in the URL
// it returns the default value instead. // it returns the default value instead.
function readQueryVariable(name, defaultValue) { 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 // 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 // Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search // because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = ''.concat(document.location.href, window.location.hash).match(re); match = document.location.href.match(re);
if (match) { if (match) {
// We have to decode the URL since want the cleartext value // We have to decode the URL since want the cleartext value