Merge remote-tracking branch 'origin/master' into feature/support-existing-rtcdatachannel-or-websocket

* origin/master:
  Fix focus problem after closing the toolbar
  Set a default value for the quality input
  Add UI for quality setting
  adds qualityLevel property to RFB class for updating JPEG quality level encoding on the fly
  Hide clipboard side bar button when view only mode
  Fix crash with too large clipboard data
  Style all input types for consistent UI
  Add extended clipboard Pseudo-Encoding
  Export constants in inflate.js for easier usage
  Fix bug where inflate would read too much data
  Split api of inflate
  Handle errors from zlib/pako
  Move error handling to Inflate class
  Add missing copyright header for Inflator.js
  Remove unused inflate argument
  Add util for unsigned and signed int. conversion
  Add deflator helper class for deflating data
  Make clipBoardPasteFrom() test more specific
  Correcting path to package.json for running at a path other than root.
This commit is contained in:
Ryan Castner 2020-04-06 11:54:59 -04:00
commit c5ca2be33e
17 changed files with 1289 additions and 112 deletions

View File

@ -83,8 +83,20 @@ html {
* ----------------------------------------
*/
input[type=input], input[type=password], input[type=number],
input:not([type]), textarea {
input:not([type]),
input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
/* Disable default rendering */
-webkit-appearance: none;
-moz-appearance: none;
@ -98,7 +110,11 @@ input:not([type]), textarea {
background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
}
input[type=button], input[type=submit], select {
input[type=button],
input[type=color],
input[type=reset],
input[type=submit],
select {
/* Disable default rendering */
-webkit-appearance: none;
-moz-appearance: none;
@ -116,7 +132,10 @@ input[type=button], input[type=submit], select {
vertical-align: middle;
}
input[type=button], input[type=submit] {
input[type=button],
input[type=color],
input[type=reset],
input[type=submit] {
padding-left: 20px;
padding-right: 20px;
}
@ -126,35 +145,72 @@ option {
background: white;
}
input[type=input]:focus, input[type=password]:focus,
input:not([type]):focus, input[type=button]:focus,
input:not([type]):focus,
input[type=button]:focus,
input[type=color]:focus,
input[type=date]:focus,
input[type=datetime-local]:focus,
input[type=email]:focus,
input[type=month]:focus,
input[type=number]:focus,
input[type=password]:focus,
input[type=reset]:focus,
input[type=search]:focus,
input[type=submit]:focus,
textarea:focus, select:focus {
input[type=tel]:focus,
input[type=text]:focus,
input[type=time]:focus,
input[type=url]:focus,
input[type=week]:focus,
select:focus,
textarea:focus {
box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5);
border-color: rgb(74, 144, 217);
outline: none;
}
input[type=button]::-moz-focus-inner,
input[type=color]::-moz-focus-inner,
input[type=reset]::-moz-focus-inner,
input[type=submit]::-moz-focus-inner {
border: none;
}
input[type=input]:disabled, input[type=password]:disabled,
input:not([type]):disabled, input[type=button]:disabled,
input[type=submit]:disabled, input[type=number]:disabled,
textarea:disabled, select:disabled {
input:not([type]):disabled,
input[type=button]:disabled,
input[type=color]:disabled,
input[type=date]:disabled,
input[type=datetime-local]:disabled,
input[type=email]:disabled,
input[type=month]:disabled,
input[type=number]:disabled,
input[type=password]:disabled,
input[type=reset]:disabled,
input[type=search]:disabled,
input[type=submit]:disabled,
input[type=tel]:disabled,
input[type=text]:disabled,
input[type=time]:disabled,
input[type=url]:disabled,
input[type=week]:disabled,
select:disabled,
textarea:disabled {
color: rgb(128, 128, 128);
background: rgb(240, 240, 240);
}
input[type=button]:active, input[type=submit]:active,
input[type=button]:active,
input[type=color]:active,
input[type=reset]:active,
input[type=submit]:active,
select:active {
border-bottom-width: 1px;
margin-top: 3px;
}
:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=color]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=reset]:hover:not(:disabled),
:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled),
:root:not(.noVNC_touch) select:hover:not(:disabled) {
background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));

View File

@ -61,7 +61,7 @@ const UI = {
// Translate the DOM
l10n.translateDOM();
WebUtil.fetchJSON('../package.json')
WebUtil.fetchJSON('./package.json')
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
@ -161,6 +161,7 @@ const UI = {
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('shared', true);
UI.initSetting('view_only', false);
UI.initSetting('show_dot', false);
@ -347,6 +348,8 @@ const UI = {
UI.addSettingChangeHandler('resize');
UI.addSettingChangeHandler('resize', UI.applyResizeMode);
UI.addSettingChangeHandler('resize', UI.updateViewClip);
UI.addSettingChangeHandler('quality');
UI.addSettingChangeHandler('quality', UI.updateQuality);
UI.addSettingChangeHandler('view_clip');
UI.addSettingChangeHandler('view_clip', UI.updateViewClip);
UI.addSettingChangeHandler('shared');
@ -532,6 +535,7 @@ const UI = {
UI.closeAllPanels();
document.getElementById('noVNC_control_bar')
.classList.remove("noVNC_open");
UI.rfb.focus();
},
toggleControlbar() {
@ -829,6 +833,7 @@ const UI = {
UI.updateSetting('encrypt');
UI.updateSetting('view_clip');
UI.updateSetting('resize');
UI.updateSetting('quality');
UI.updateSetting('shared');
UI.updateSetting('view_only');
UI.updateSetting('path');
@ -1030,6 +1035,7 @@ const UI = {
UI.rfb.clipViewport = UI.getSetting('view_clip');
UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale';
UI.rfb.resizeSession = UI.getSetting('resize') === 'remote';
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.updateViewOnly(); // requires UI.rfb
@ -1324,6 +1330,18 @@ const UI = {
/* ------^-------
* /VIEWDRAG
* ==============
* QUALITY
* ------v------*/
updateQuality() {
if (!UI.rfb) return;
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
},
/* ------^-------
* /QUALITY
* ==============
* KEYBOARD
* ------v------*/
@ -1621,6 +1639,8 @@ const UI = {
.classList.add('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
.classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
} else {
document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden');
@ -1628,6 +1648,8 @@ const UI = {
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_mouse_button' + UI.rfb.touchButton)
.classList.remove('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.remove('noVNC_hidden');
}
},

View File

@ -160,10 +160,9 @@ export default class TightDecoder {
return false;
}
data = this._zlibs[streamId].inflate(data, true, uncompressedSize);
if (data.length != uncompressedSize) {
throw new Error("Incomplete zlib block");
}
this._zlibs[streamId].setInput(data);
data = this._zlibs[streamId].inflate(uncompressedSize);
this._zlibs[streamId].setInput(null);
}
display.blitRgbImage(x, y, width, height, data, 0, false);
@ -208,10 +207,9 @@ export default class TightDecoder {
return false;
}
data = this._zlibs[streamId].inflate(data, true, uncompressedSize);
if (data.length != uncompressedSize) {
throw new Error("Incomplete zlib block");
}
this._zlibs[streamId].setInput(data);
data = this._zlibs[streamId].inflate(uncompressedSize);
this._zlibs[streamId].setInput(null);
}
// Convert indexed (palette based) image data to RGB

79
core/deflator.js Normal file
View File

@ -0,0 +1,79 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
export default class Deflator {
constructor() {
this.strm = new ZStream();
this.chunkSize = 1024 * 10 * 10;
this.outputBuffer = new Uint8Array(this.chunkSize);
this.windowBits = 5;
deflateInit(this.strm, this.windowBits);
}
deflate(inData) {
this.strm.input = inData;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
this.strm.output = this.outputBuffer;
this.strm.avail_out = this.chunkSize;
this.strm.next_out = 0;
let lastRet = deflate(this.strm, Z_FULL_FLUSH);
let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
if (this.strm.avail_in > 0) {
// Read chunks until done
let chunks = [outData];
let totalLen = outData.length;
do {
this.strm.output = new Uint8Array(this.chunkSize);
this.strm.next_out = 0;
this.strm.avail_out = this.chunkSize;
lastRet = deflate(this.strm, Z_FULL_FLUSH);
if (lastRet < 0) {
throw new Error("zlib deflate failed");
}
let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
totalLen += chunk.length;
chunks.push(chunk);
} while (this.strm.avail_in > 0);
// Combine chunks into a single data
let newData = new Uint8Array(totalLen);
let offset = 0;
for (let i = 0; i < chunks.length; i++) {
newData.set(chunks[i], offset);
offset += chunks[i].length;
}
outData = newData;
}
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
return outData;
}
}

View File

@ -27,7 +27,8 @@ export const encodings = {
pseudoEncodingContinuousUpdates: -313,
pseudoEncodingCompressLevel9: -247,
pseudoEncodingCompressLevel0: -256,
pseudoEncodingVMwareCursor: 0x574d5664
pseudoEncodingVMwareCursor: 0x574d5664,
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
};
export function encodingName(num) {

View File

@ -1,3 +1,11 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
@ -11,12 +19,20 @@ export default class Inflate {
inflateInit(this.strm, this.windowBits);
}
inflate(data, flush, expected) {
this.strm.input = data;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
this.strm.next_out = 0;
setInput(data) {
if (!data) {
//FIXME: flush remaining data.
this.strm.input = null;
this.strm.avail_in = 0;
this.strm.next_in = 0;
} else {
this.strm.input = data;
this.strm.avail_in = this.strm.input.length;
this.strm.next_in = 0;
}
}
inflate(expected) {
// resize our output buffer if it's too small
// (we could just use multiple chunks, but that would cause an extra
// allocation each time to flatten the chunks)
@ -25,9 +41,17 @@ export default class Inflate {
this.strm.output = new Uint8Array(this.chunkSize);
}
this.strm.avail_out = this.chunkSize;
this.strm.next_out = 0;
this.strm.avail_out = expected;
inflate(this.strm, flush);
let ret = inflate(this.strm, 0); // Flush argument not used.
if (ret < 0) {
throw new Error("zlib inflate failed");
}
if (this.strm.next_out != expected) {
throw new Error("Incomplete zlib block");
}
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
}

View File

@ -1,17 +1,20 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
import * as Log from './util/logging.js';
import { decodeUTF8 } from './util/strings.js';
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold } from './util/browser.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import Inflator from "./inflator.js";
import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js";
import Mouse from "./input/mouse.js";
import Cursor from "./util/cursor.js";
@ -33,6 +36,23 @@ import TightPNGDecoder from "./decoders/tightpng.js";
const DISCONNECT_TIMEOUT = 3;
const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
// Extended clipboard pseudo-encoding formats
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
const extendedClipboardFormatRtf = 1 << 1;
const extendedClipboardFormatHtml = 1 << 2;
const extendedClipboardFormatDib = 1 << 3;
const extendedClipboardFormatFiles = 1 << 4;
/*eslint-enable */
// Extended clipboard pseudo-encoding actions
const extendedClipboardActionCaps = 1 << 24;
const extendedClipboardActionRequest = 1 << 25;
const extendedClipboardActionPeek = 1 << 26;
const extendedClipboardActionNotify = 1 << 27;
const extendedClipboardActionProvide = 1 << 28;
export default class RFB extends EventTargetMixin {
constructor(target, urlOrChannel, options) {
if (!target) {
@ -84,6 +104,10 @@ export default class RFB extends EventTargetMixin {
this._qemuExtKeyEventSupported = false;
this._clipboardText = null;
this._clipboardServerCapabilitiesActions = {};
this._clipboardServerCapabilitiesFormats = {};
// Internal objects
this._sock = null; // WebSocket or RTCDataChannel object
this._display = null; // Display object
@ -251,6 +275,8 @@ export default class RFB extends EventTargetMixin {
Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
this._showDotCursor = options.showDotCursor;
}
this._qualityLevel = 6;
}
// ===== PROPERTIES =====
@ -313,6 +339,26 @@ export default class RFB extends EventTargetMixin {
get background() { return this._screen.style.background; }
set background(cssValue) { this._screen.style.background = cssValue; }
get qualityLevel() {
return this._qualityLevel;
}
set qualityLevel(qualityLevel) {
if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
Log.Error("qualityLevel must be an integer between 0 and 9");
return;
}
if (this._qualityLevel === qualityLevel) {
return;
}
this._qualityLevel = qualityLevel;
if (this._rfb_connection_state === 'connected') {
this._sendEncodings();
}
}
// ===== PUBLIC METHODS =====
disconnect() {
@ -390,7 +436,21 @@ export default class RFB extends EventTargetMixin {
clipboardPasteFrom(text) {
if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
RFB.messages.clientCutText(this._sock, text);
if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
this._clipboardText = text;
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
} else {
let data = new Uint8Array(text.length);
for (let i = 0; i < text.length; i++) {
// FIXME: text can have values outside of Latin1/Uint8
data[i] = text.charCodeAt(i);
}
RFB.messages.clientCutText(this._sock, data);
}
}
// ===== PRIVATE METHODS =====
@ -1267,7 +1327,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.encodingRaw);
// Psuedo-encoding settings
encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
encs.push(encodings.pseudoEncodingDesktopSize);
@ -1278,6 +1338,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingFence);
encs.push(encodings.pseudoEncodingContinuousUpdates);
encs.push(encodings.pseudoEncodingDesktopName);
encs.push(encodings.pseudoEncodingExtendedClipboard);
if (this._fb_depth == 24) {
encs.push(encodings.pseudoEncodingVMwareCursor);
@ -1336,18 +1397,167 @@ export default class RFB extends EventTargetMixin {
Log.Debug("ServerCutText");
if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
this._sock.rQskipBytes(3); // Padding
const length = this._sock.rQshift32();
if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
const text = this._sock.rQshiftStr(length);
let length = this._sock.rQshift32();
length = toSigned32bit(length);
if (this._viewOnly) { return true; }
if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: text } }));
if (length >= 0) {
//Standard msg
const text = this._sock.rQshiftStr(length);
if (this._viewOnly) {
return true;
}
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: text } }));
} else {
//Extended msg.
length = Math.abs(length);
const flags = this._sock.rQshift32();
let formats = flags & 0x0000FFFF;
let actions = flags & 0xFF000000;
let isCaps = (!!(actions & extendedClipboardActionCaps));
if (isCaps) {
this._clipboardServerCapabilitiesFormats = {};
this._clipboardServerCapabilitiesActions = {};
// Update our server capabilities for Formats
for (let i = 0; i <= 15; i++) {
let index = 1 << i;
// Check if format flag is set.
if ((formats & index)) {
this._clipboardServerCapabilitiesFormats[index] = true;
// We don't send unsolicited clipboard, so we
// ignore the size
this._sock.rQshift32();
}
}
// Update our server capabilities for Actions
for (let i = 24; i <= 31; i++) {
let index = 1 << i;
this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
}
/* Caps handling done, send caps with the clients
capabilities set as a response */
let clientActions = [
extendedClipboardActionCaps,
extendedClipboardActionRequest,
extendedClipboardActionPeek,
extendedClipboardActionNotify,
extendedClipboardActionProvide
];
RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
} else if (actions === extendedClipboardActionRequest) {
if (this._viewOnly) {
return true;
}
// Check if server has told us it can handle Provide and there is clipboard data to send.
if (this._clipboardText != null &&
this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
if (formats & extendedClipboardFormatText) {
RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
}
}
} else if (actions === extendedClipboardActionPeek) {
if (this._viewOnly) {
return true;
}
if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
if (this._clipboardText != null) {
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
} else {
RFB.messages.extendedClipboardNotify(this._sock, []);
}
}
} else if (actions === extendedClipboardActionNotify) {
if (this._viewOnly) {
return true;
}
if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
if (formats & extendedClipboardFormatText) {
RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
}
}
} else if (actions === extendedClipboardActionProvide) {
if (this._viewOnly) {
return true;
}
if (!(formats & extendedClipboardFormatText)) {
return true;
}
// Ignore what we had in our clipboard client side.
this._clipboardText = null;
// FIXME: Should probably verify that this data was actually requested
let zlibStream = this._sock.rQshiftBytes(length - 4);
let streamInflator = new Inflator();
let textData = null;
streamInflator.setInput(zlibStream);
for (let i = 0; i <= 15; i++) {
let format = 1 << i;
if (formats & format) {
let size = 0x00;
let sizeArray = streamInflator.inflate(4);
size |= (sizeArray[0] << 24);
size |= (sizeArray[1] << 16);
size |= (sizeArray[2] << 8);
size |= (sizeArray[3]);
let chunk = streamInflator.inflate(size);
if (format === extendedClipboardFormatText) {
textData = chunk;
}
}
}
streamInflator.setInput(null);
if (textData !== null) {
let tmpText = "";
for (let i = 0; i < textData.length; i++) {
tmpText += String.fromCharCode(textData[i]);
}
textData = tmpText;
textData = decodeUTF8(textData);
if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
textData = textData.slice(0, -1);
}
textData = textData.replace("\r\n", "\n");
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: textData } }));
}
} else {
return this._fail("Unexpected action in extended clipboard message: " + actions);
}
}
return true;
}
@ -1977,8 +2187,102 @@ RFB.messages = {
sock.flush();
},
// TODO(directxman12): make this unicode compatible?
clientCutText(sock, text) {
// Used to build Notify and Request data.
_buildExtendedClipboardFlags(actions, formats) {
let data = new Uint8Array(4);
let formatFlag = 0x00000000;
let actionFlag = 0x00000000;
for (let i = 0; i < actions.length; i++) {
actionFlag |= actions[i];
}
for (let i = 0; i < formats.length; i++) {
formatFlag |= formats[i];
}
data[0] = actionFlag >> 24; // Actions
data[1] = 0x00; // Reserved
data[2] = 0x00; // Reserved
data[3] = formatFlag; // Formats
return data;
},
extendedClipboardProvide(sock, formats, inData) {
// Deflate incomming data and their sizes
let deflator = new Deflator();
let dataToDeflate = [];
for (let i = 0; i < formats.length; i++) {
// We only support the format Text at this time
if (formats[i] != extendedClipboardFormatText) {
throw new Error("Unsupported extended clipboard format for Provide message.");
}
// Change lone \r or \n into \r\n as defined in rfbproto
inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
// Check if it already has \0
let text = encodeUTF8(inData[i] + "\0");
dataToDeflate.push( (text.length >> 24) & 0xFF,
(text.length >> 16) & 0xFF,
(text.length >> 8) & 0xFF,
(text.length & 0xFF));
for (let j = 0; j < text.length; j++) {
dataToDeflate.push(text.charCodeAt(j));
}
}
let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
// Build data to send
let data = new Uint8Array(4 + deflatedData.length);
data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
formats));
data.set(deflatedData, 4);
RFB.messages.clientCutText(sock, data, true);
},
extendedClipboardNotify(sock, formats) {
let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
formats);
RFB.messages.clientCutText(sock, flags, true);
},
extendedClipboardRequest(sock, formats) {
let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
formats);
RFB.messages.clientCutText(sock, flags, true);
},
extendedClipboardCaps(sock, actions, formats) {
let formatKeys = Object.keys(formats);
let data = new Uint8Array(4 + (4 * formatKeys.length));
formatKeys.map(x => parseInt(x));
formatKeys.sort((a, b) => a - b);
data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
let loopOffset = 4;
for (let i = 0; i < formatKeys.length; i++) {
data[loopOffset] = formats[formatKeys[i]] >> 24;
data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
loopOffset += 4;
data[3] |= (1 << formatKeys[i]); // Update our format flags
}
RFB.messages.clientCutText(sock, data, true);
},
clientCutText(sock, data, extended = false) {
const buff = sock._sQ;
const offset = sock._sQlen;
@ -1988,7 +2292,12 @@ RFB.messages = {
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
let length = text.length;
let length;
if (extended) {
length = toUnsigned32bit(-data.length);
} else {
length = data.length;
}
buff[offset + 4] = length >> 24;
buff[offset + 5] = length >> 16;
@ -1997,24 +2306,25 @@ RFB.messages = {
sock._sQlen += 8;
// We have to keep track of from where in the text we begin creating the
// We have to keep track of from where in the data we begin creating the
// buffer for the flush in the next iteration.
let textOffset = 0;
let dataOffset = 0;
let remaining = length;
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] = text.charCodeAt(textOffset + i);
buff[sock._sQlen + i] = data[dataOffset + i];
}
sock._sQlen += flushSize;
sock.flush();
remaining -= flushSize;
textOffset += flushSize;
dataOffset += flushSize;
}
},
setDesktopSize(sock, width, height, id, flags) {

15
core/util/int.js Normal file
View File

@ -0,0 +1,15 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
export function toUnsigned32bit(toConvert) {
return toConvert >>> 0;
}
export function toSigned32bit(toConvert) {
return toConvert | 0;
}

View File

@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
@ -52,3 +52,10 @@ if (typeof Object.assign != 'function') {
window.CustomEvent = CustomEvent;
}
})();
/* Number.isInteger() (taken from MDN) */
Number.isInteger = Number.isInteger || function isInteger(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value;
};

View File

@ -64,6 +64,11 @@ protocol stream.
to the element containing the remote session screen. The default value is `rgb(40, 40, 40)`
(solid gray color).
`qualityLevel`
- Is an `int` in range `[0-9]` controlling the desired JPEG quality.
Value `0` implies low quality and `9` implies high quality.
Default value is `6`.
`capabilities` *Read only*
- Is an `Object` indicating which optional extensions are available
on the server. Some methods may only be called if the corresponding

View File

@ -61,6 +61,8 @@ query string. Currently the following options are available:
* `resize` - How to resize the remote session if it is not the same size as
the browser window. Can be one of `off`, `scale` and `remote`.
* `quality` - The session JPEG quality level. Can be `0` to `9`.
* `show_dot` - If a dot cursor should be shown when the remote server provides
no local cursor, or provides a fully-transparent (invisible) cursor.

80
tests/test.deflator.js Normal file
View File

@ -0,0 +1,80 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { inflateInit, inflate } from "../vendor/pako/lib/zlib/inflate.js";
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
import Deflator from "../core/deflator.js";
function _inflator(compText, expected) {
let strm = new ZStream();
let chunkSize = 1024 * 10 * 10;
strm.output = new Uint8Array(chunkSize);
inflateInit(strm, 5);
if (expected > chunkSize) {
chunkSize = expected;
strm.output = new Uint8Array(chunkSize);
}
strm.input = compText;
strm.avail_in = strm.input.length;
strm.next_in = 0;
strm.next_out = 0;
strm.avail_out = expected.length;
let ret = inflate(strm, 0);
// Check that return code is not an error
expect(ret).to.be.greaterThan(-1);
return new Uint8Array(strm.output.buffer, 0, strm.next_out);
}
describe('Deflate data', function () {
it('should be able to deflate messages', function () {
let deflator = new Deflator();
let text = "123asdf";
let preText = new Uint8Array(text.length);
for (let i = 0; i < preText.length; i++) {
preText[i] = text.charCodeAt(i);
}
let compText = deflator.deflate(preText);
let inflatedText = _inflator(compText, text.length);
expect(inflatedText).to.array.equal(preText);
});
it('should be able to deflate large messages', function () {
let deflator = new Deflator();
/* Generate a big string with random characters. Used because
repetition of letters might be deflated more effectively than
random ones. */
let text = "";
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 300000; i++) {
text += characters.charAt(Math.floor(Math.random() * characters.length));
}
let preText = new Uint8Array(text.length);
for (let i = 0; i < preText.length; i++) {
preText[i] = text.charCodeAt(i);
}
let compText = deflator.deflate(preText);
//Check that the compressed size is expected size
expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2);
let inflatedText = _inflator(compText, text.length);
expect(inflatedText).to.array.equal(preText);
});
});

16
tests/test.int.js Normal file
View File

@ -0,0 +1,16 @@
/* eslint-disable no-console */
const expect = chai.expect;
import { toUnsigned32bit, toSigned32bit } from '../core/util/int.js';
describe('Integer casting', function () {
it('should cast unsigned to signed', function () {
let expected = 4294967286;
expect(toUnsigned32bit(-10)).to.equal(expected);
});
it('should cast signed to unsigned', function () {
let expected = -10;
expect(toSigned32bit(4294967286)).to.equal(expected);
});
});

View File

@ -2,7 +2,11 @@ const expect = chai.expect;
import RFB from '../core/rfb.js';
import WebChannel from '../core/websock.js';
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
import { encodings } from '../core/encodings.js';
import { toUnsigned32bit } from '../core/util/int.js';
import { encodeUTF8 } from '../core/util/strings.js';
import FakeWebSocket from './fake.websocket.js';
@ -48,6 +52,35 @@ function pushString(arr, string) {
}
}
function deflateWithSize(data) {
// Adds the size of the string in front before deflating
let unCompData = [];
unCompData.push((data.length >> 24) & 0xFF,
(data.length >> 16) & 0xFF,
(data.length >> 8) & 0xFF,
(data.length & 0xFF));
for (let i = 0; i < data.length; i++) {
unCompData.push(data.charCodeAt(i));
}
let strm = new ZStream();
let chunkSize = 1024 * 10 * 10;
strm.output = new Uint8Array(chunkSize);
deflateInit(strm, 5);
strm.input = unCompData;
strm.avail_in = strm.input.length;
strm.next_in = 0;
strm.next_out = 0;
strm.avail_out = chunkSize;
deflate(strm, 3);
return new Uint8Array(strm.output.buffer, 0, strm.next_out);
}
describe('Remote Frame Buffer Protocol Client', function () {
let clock;
let raf;
@ -291,12 +324,39 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
describe('#clipboardPasteFrom', function () {
it('should send the given text in a paste event', function () {
const expected = {_sQ: new Uint8Array(11), _sQlen: 0,
_sQbufferSize: 11, flush: () => {}};
RFB.messages.clientCutText(expected, 'abc');
client.clipboardPasteFrom('abc');
expect(client._sock).to.have.sent(expected._sQ);
describe('Clipboard update handling', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'clientCutText');
sinon.spy(RFB.messages, 'extendedClipboardNotify');
});
afterEach(function () {
RFB.messages.clientCutText.restore();
RFB.messages.extendedClipboardNotify.restore();
});
it('should send the given text in an clipboard update', function () {
client.clipboardPasteFrom('abc');
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(client._sock,
new Uint8Array([97, 98, 99]));
});
it('should send an notify if extended clipboard is supported by server', function () {
// Send our capabilities
let data = [3, 0, 0, 0];
const flags = [0x1F, 0x00, 0x00, 0x01];
let fileSizes = [0x00, 0x00, 0x00, 0x1E];
push32(data, toUnsigned32bit(-8));
data = data.concat(flags);
data = data.concat(fileSizes);
client._sock._websocket._receive_data(new Uint8Array(data));
client.clipboardPasteFrom('extended test');
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;
});
});
it('should flush multiple times for large clipboards', function () {
@ -2336,17 +2396,249 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
const expected_str = 'cheese!';
const data = [3, 0, 0, 0];
push32(data, expected_str.length);
for (let i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); }
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
describe('Normal Clipboard Handling Receive', function () {
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
const expected_str = 'cheese!';
const data = [3, 0, 0, 0];
push32(data, expected_str.length);
for (let i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); }
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
client._sock._websocket._receive_data(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expected_str);
});
});
describe('Extended clipboard Handling', function () {
describe('Extended clipboard initialization', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'extendedClipboardCaps');
});
afterEach(function () {
RFB.messages.extendedClipboardCaps.restore();
});
it('should update capabilities when receiving a Caps message', function () {
let data = [3, 0, 0, 0];
const flags = [0x1F, 0x00, 0x00, 0x03];
let fileSizes = [0x00, 0x00, 0x00, 0x1E,
0x00, 0x00, 0x00, 0x3C];
push32(data, toUnsigned32bit(-12));
data = data.concat(flags);
data = data.concat(fileSizes);
client._sock._websocket._receive_data(new Uint8Array(data));
// Check that we give an response caps when we receive one
expect(RFB.messages.extendedClipboardCaps).to.have.been.calledOnce;
// FIXME: Can we avoid checking internal variables?
expect(client._clipboardServerCapabilitiesFormats[0]).to.not.equal(true);
expect(client._clipboardServerCapabilitiesFormats[1]).to.equal(true);
expect(client._clipboardServerCapabilitiesFormats[2]).to.equal(true);
expect(client._clipboardServerCapabilitiesActions[(1 << 24)]).to.equal(true);
});
});
describe('Extended Clipboard Handling Receive', function () {
beforeEach(function () {
// Send our capabilities
let data = [3, 0, 0, 0];
const flags = [0x1F, 0x00, 0x00, 0x01];
let fileSizes = [0x00, 0x00, 0x00, 0x1E];
push32(data, toUnsigned32bit(-8));
data = data.concat(flags);
data = data.concat(fileSizes);
client._sock._websocket._receive_data(new Uint8Array(data));
});
describe('Handle Provide', function () {
it('should update clipboard with correct Unicode data from a Provide message', function () {
let expectedData = "Aå漢字!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
/* The size 10 (utf8 encoded string size) and the
string "Aå漢字!" utf8 encoded and deflated. */
let deflatedData = [120, 94, 99, 96, 96, 224, 114, 60,
188, 244, 217, 158, 69, 79, 215,
78, 87, 4, 0, 35, 207, 6, 66];
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedData.length)));
data = data.concat(flags);
data = data.concat(deflatedData);
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
client._sock._websocket._receive_data(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
it('should update clipboard with correct escape characters from a Provide message ', function () {
let expectedData = "Oh\nmy!";
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8("Oh\r\nmy!\0");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
let sendData = new Uint8Array(data.length + deflatedText.length);
sendData.set(data);
sendData.set(deflatedText, data.length);
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
client._sock._websocket._receive_data(sendData);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
it('should be able to handle large Provide messages', function () {
// repeat() is not supported in IE so a loop is needed instead
let expectedData = "hello";
for (let i = 1; i <= 100000; i++) {
expectedData += "hello";
}
let data = [3, 0, 0, 0];
const flags = [0x10, 0x00, 0x00, 0x01];
let text = encodeUTF8(expectedData + "\0");
let deflatedText = deflateWithSize(text);
// How much data we are sending.
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
data = data.concat(flags);
let sendData = new Uint8Array(data.length + deflatedText.length);
sendData.set(data);
sendData.set(deflatedText, data.length);
const spy = sinon.spy();
client.addEventListener("clipboard", spy);
client._sock._websocket._receive_data(sendData);
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expectedData);
client.removeEventListener("clipboard", spy);
});
});
describe('Handle Notify', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'extendedClipboardRequest');
});
afterEach(function () {
RFB.messages.extendedClipboardRequest.restore();
});
it('should make a request with supported formats when receiving a notify message', function () {
let data = [3, 0, 0, 0];
const flags = [0x08, 0x00, 0x00, 0x07];
push32(data, toUnsigned32bit(-4));
data = data.concat(flags);
let expectedData = [0x01];
client._sock._websocket._receive_data(new Uint8Array(data));
expect(RFB.messages.extendedClipboardRequest).to.have.been.calledOnce;
expect(RFB.messages.extendedClipboardRequest).to.have.been.calledWith(client._sock, expectedData);
});
});
describe('Handle Peek', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'extendedClipboardNotify');
});
afterEach(function () {
RFB.messages.extendedClipboardNotify.restore();
});
it('should send an empty Notify when receiving a Peek and no excisting clipboard data', function () {
let data = [3, 0, 0, 0];
const flags = [0x04, 0x00, 0x00, 0x00];
push32(data, toUnsigned32bit(-4));
data = data.concat(flags);
let expectedData = [];
client._sock._websocket._receive_data(new Uint8Array(data));
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledWith(client._sock, expectedData);
});
it('should send a Notify message with supported formats when receiving a Peek', function () {
let data = [3, 0, 0, 0];
const flags = [0x04, 0x00, 0x00, 0x00];
push32(data, toUnsigned32bit(-4));
data = data.concat(flags);
let expectedData = [0x01];
// Needed to have clipboard data to read.
// This will trigger a call to Notify, reset history
client.clipboardPasteFrom("HejHej");
RFB.messages.extendedClipboardNotify.resetHistory();
client._sock._websocket._receive_data(new Uint8Array(data));
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledWith(client._sock, expectedData);
});
});
describe('Handle Request', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'extendedClipboardProvide');
});
afterEach(function () {
RFB.messages.extendedClipboardProvide.restore();
});
it('should send a Provide message with supported formats when receiving a Request', function () {
let data = [3, 0, 0, 0];
const flags = [0x02, 0x00, 0x00, 0x01];
push32(data, toUnsigned32bit(-4));
data = data.concat(flags);
let expectedData = [0x01];
client.clipboardPasteFrom("HejHej");
expect(RFB.messages.extendedClipboardProvide).to.not.have.been.called;
client._sock._websocket._receive_data(new Uint8Array(data));
expect(RFB.messages.extendedClipboardProvide).to.have.been.calledOnce;
expect(RFB.messages.extendedClipboardProvide).to.have.been.calledWith(client._sock, expectedData, ["HejHej"]);
});
});
});
client._sock._webChannel._receive_data(new Uint8Array(data));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.text).to.equal(expected_str);
});
it('should fire the bell callback on Bell', function () {
@ -2573,4 +2865,269 @@ describe('Remote Frame Buffer Protocol Client', function () {
// error events do nothing
});
});
describe('Quality level setting', function () {
const defaultQuality = 6;
let client;
beforeEach(function () {
client = make_rfb();
sinon.spy(RFB.messages, "clientEncodings");
});
afterEach(function () {
RFB.messages.clientEncodings.restore();
});
it(`should equal ${defaultQuality} by default`, function () {
expect(client._qualityLevel).to.equal(defaultQuality);
});
it('should ignore non-integers when set', function () {
client.qualityLevel = '1';
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = 1.5;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = null;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = undefined;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = {};
expect(RFB.messages.clientEncodings).to.not.have.been.called;
});
it('should ignore integers out of range [0, 9]', function () {
client.qualityLevel = -1;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = 10;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
});
it('should send clientEncodings with new quality value', function () {
let newQuality;
newQuality = 8;
client.qualityLevel = newQuality;
expect(client.qualityLevel).to.equal(newQuality);
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);
});
it('should not send clientEncodings if quality is the same', function () {
let newQuality;
newQuality = 2;
client.qualityLevel = newQuality;
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);
RFB.messages.clientEncodings.resetHistory();
client.qualityLevel = newQuality;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
});
it('should not send clientEncodings if not in connected state', function () {
let newQuality;
client._rfb_connection_state = '';
newQuality = 2;
client.qualityLevel = newQuality;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client._rfb_connection_state = 'connnecting';
newQuality = 6;
client.qualityLevel = newQuality;
expect(RFB.messages.clientEncodings).to.not.have.been.called;
RFB.messages.clientEncodings.resetHistory();
client._rfb_connection_state = 'connected';
newQuality = 5;
client.qualityLevel = newQuality;
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingQualityLevel0 + newQuality);
});
});
});
describe('RFB messages', function () {
let sock;
before(function () {
FakeWebSocket.replace();
sock = new WebChannel();
sock.open();
});
after(function () {
FakeWebSocket.restore();
});
describe('Extended Clipboard Handling Send', function () {
beforeEach(function () {
sinon.spy(RFB.messages, 'clientCutText');
});
afterEach(function () {
RFB.messages.clientCutText.restore();
});
it('should call clientCutText with correct Caps data', function () {
let formats = {
0: 2,
2: 4121
};
let expectedData = new Uint8Array([0x1F, 0x00, 0x00, 0x05,
0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x10, 0x19]);
let actions = [
1 << 24, // Caps
1 << 25, // Request
1 << 26, // Peek
1 << 27, // Notify
1 << 28 // Provide
];
RFB.messages.extendedClipboardCaps(sock, actions, formats);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData);
});
it('should call clientCutText with correct Request data', function () {
let formats = new Uint8Array([0x01]);
let expectedData = new Uint8Array([0x02, 0x00, 0x00, 0x01]);
RFB.messages.extendedClipboardRequest(sock, formats);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData);
});
it('should call clientCutText with correct Notify data', function () {
let formats = new Uint8Array([0x01]);
let expectedData = new Uint8Array([0x08, 0x00, 0x00, 0x01]);
RFB.messages.extendedClipboardNotify(sock, formats);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData);
});
it('should call clientCutText with correct Provide data', function () {
let testText = "Test string";
let expectedText = encodeUTF8(testText + "\0");
let deflatedData = deflateWithSize(expectedText);
// Build Expected with flags and deflated data
let expectedData = new Uint8Array(4 + deflatedData.length);
expectedData[0] = 0x10; // The client capabilities
expectedData[1] = 0x00; // Reserved flags
expectedData[2] = 0x00; // Reserved flags
expectedData[3] = 0x01; // The formats client supports
expectedData.set(deflatedData, 4);
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true);
});
describe('End of line characters', function () {
it('Carriage return', function () {
let testText = "Hello\rworld\r\r!";
let expectedText = encodeUTF8("Hello\r\nworld\r\n\r\n!\0");
let deflatedData = deflateWithSize(expectedText);
// Build Expected with flags and deflated data
let expectedData = new Uint8Array(4 + deflatedData.length);
expectedData[0] = 0x10; // The client capabilities
expectedData[1] = 0x00; // Reserved flags
expectedData[2] = 0x00; // Reserved flags
expectedData[3] = 0x01; // The formats client supports
expectedData.set(deflatedData, 4);
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true);
});
it('Carriage return Line feed', function () {
let testText = "Hello\r\n\r\nworld\r\n!";
let expectedText = encodeUTF8(testText + "\0");
let deflatedData = deflateWithSize(expectedText);
// Build Expected with flags and deflated data
let expectedData = new Uint8Array(4 + deflatedData.length);
expectedData[0] = 0x10; // The client capabilities
expectedData[1] = 0x00; // Reserved flags
expectedData[2] = 0x00; // Reserved flags
expectedData[3] = 0x01; // The formats client supports
expectedData.set(deflatedData, 4);
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true);
});
it('Line feed', function () {
let testText = "Hello\n\n\nworld\n!";
let expectedText = encodeUTF8("Hello\r\n\r\n\r\nworld\r\n!\0");
let deflatedData = deflateWithSize(expectedText);
// Build Expected with flags and deflated data
let expectedData = new Uint8Array(4 + deflatedData.length);
expectedData[0] = 0x10; // The client capabilities
expectedData[1] = 0x00; // Reserved flags
expectedData[2] = 0x00; // Reserved flags
expectedData[3] = 0x01; // The formats client supports
expectedData.set(deflatedData, 4);
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true);
});
it('Carriage return and Line feed mixed', function () {
let testText = "\rHello\r\n\rworld\n\n!";
let expectedText = encodeUTF8("\r\nHello\r\n\r\nworld\r\n\r\n!\0");
let deflatedData = deflateWithSize(expectedText);
// Build Expected with flags and deflated data
let expectedData = new Uint8Array(4 + deflatedData.length);
expectedData[0] = 0x10; // The client capabilities
expectedData[1] = 0x00; // Reserved flags
expectedData[2] = 0x00; // Reserved flags
expectedData[3] = 0x01; // The formats client supports
expectedData.set(deflatedData, 4);
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true);
});
});
});
});

View File

@ -9,51 +9,51 @@ import msg from "./messages.js";
/* Allowed flush values; see deflate() and inflate() below for details */
var Z_NO_FLUSH = 0;
var Z_PARTIAL_FLUSH = 1;
//var Z_SYNC_FLUSH = 2;
var Z_FULL_FLUSH = 3;
var Z_FINISH = 4;
var Z_BLOCK = 5;
//var Z_TREES = 6;
export const Z_NO_FLUSH = 0;
export const Z_PARTIAL_FLUSH = 1;
//export const Z_SYNC_FLUSH = 2;
export const Z_FULL_FLUSH = 3;
export const Z_FINISH = 4;
export const Z_BLOCK = 5;
//export const Z_TREES = 6;
/* Return codes for the compression/decompression functions. Negative values
* are errors, positive values are used for special but normal events.
*/
var Z_OK = 0;
var Z_STREAM_END = 1;
//var Z_NEED_DICT = 2;
//var Z_ERRNO = -1;
var Z_STREAM_ERROR = -2;
var Z_DATA_ERROR = -3;
//var Z_MEM_ERROR = -4;
var Z_BUF_ERROR = -5;
//var Z_VERSION_ERROR = -6;
export const Z_OK = 0;
export const Z_STREAM_END = 1;
//export const Z_NEED_DICT = 2;
//export const Z_ERRNO = -1;
export const Z_STREAM_ERROR = -2;
export const Z_DATA_ERROR = -3;
//export const Z_MEM_ERROR = -4;
export const Z_BUF_ERROR = -5;
//export const Z_VERSION_ERROR = -6;
/* compression levels */
//var Z_NO_COMPRESSION = 0;
//var Z_BEST_SPEED = 1;
//var Z_BEST_COMPRESSION = 9;
var Z_DEFAULT_COMPRESSION = -1;
//export const Z_NO_COMPRESSION = 0;
//export const Z_BEST_SPEED = 1;
//export const Z_BEST_COMPRESSION = 9;
export const Z_DEFAULT_COMPRESSION = -1;
var Z_FILTERED = 1;
var Z_HUFFMAN_ONLY = 2;
var Z_RLE = 3;
var Z_FIXED = 4;
var Z_DEFAULT_STRATEGY = 0;
export const Z_FILTERED = 1;
export const Z_HUFFMAN_ONLY = 2;
export const Z_RLE = 3;
export const Z_FIXED = 4;
export const Z_DEFAULT_STRATEGY = 0;
/* Possible values of the data_type field (though see inflate()) */
//var Z_BINARY = 0;
//var Z_TEXT = 1;
//var Z_ASCII = 1; // = Z_TEXT
var Z_UNKNOWN = 2;
//export const Z_BINARY = 0;
//export const Z_TEXT = 1;
//export const Z_ASCII = 1; // = Z_TEXT
export const Z_UNKNOWN = 2;
/* The deflate compression method */
var Z_DEFLATED = 8;
export const Z_DEFLATED = 8;
/*============================================================================*/

View File

@ -13,30 +13,30 @@ var DISTS = 2;
/* Allowed flush values; see deflate() and inflate() below for details */
//var Z_NO_FLUSH = 0;
//var Z_PARTIAL_FLUSH = 1;
//var Z_SYNC_FLUSH = 2;
//var Z_FULL_FLUSH = 3;
var Z_FINISH = 4;
var Z_BLOCK = 5;
var Z_TREES = 6;
//export const Z_NO_FLUSH = 0;
//export const Z_PARTIAL_FLUSH = 1;
//export const Z_SYNC_FLUSH = 2;
//export const Z_FULL_FLUSH = 3;
export const Z_FINISH = 4;
export const Z_BLOCK = 5;
export const Z_TREES = 6;
/* Return codes for the compression/decompression functions. Negative values
* are errors, positive values are used for special but normal events.
*/
var Z_OK = 0;
var Z_STREAM_END = 1;
var Z_NEED_DICT = 2;
//var Z_ERRNO = -1;
var Z_STREAM_ERROR = -2;
var Z_DATA_ERROR = -3;
var Z_MEM_ERROR = -4;
var Z_BUF_ERROR = -5;
//var Z_VERSION_ERROR = -6;
export const Z_OK = 0;
export const Z_STREAM_END = 1;
export const Z_NEED_DICT = 2;
//export const Z_ERRNO = -1;
export const Z_STREAM_ERROR = -2;
export const Z_DATA_ERROR = -3;
export const Z_MEM_ERROR = -4;
export const Z_BUF_ERROR = -5;
//export const Z_VERSION_ERROR = -6;
/* The deflate compression method */
var Z_DEFLATED = 8;
export const Z_DEFLATED = 8;
/* STATES ====================================================================*/

View File

@ -207,6 +207,11 @@
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">