5216 lines
240 KiB
JavaScript
5216 lines
240 KiB
JavaScript
import RFB from '../core/rfb.js';
|
|
import Websock from '../core/websock.js';
|
|
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
|
|
import { deflateInit, deflate, Z_DEFAULT_COMPRESSION } 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 KeyTable from '../core/input/keysym.js';
|
|
import legacyCrypto from '../core/crypto/crypto.js';
|
|
|
|
import FakeWebSocket from './fake.websocket.js';
|
|
|
|
function push8(arr, num) {
|
|
"use strict";
|
|
arr.push(num & 0xFF);
|
|
}
|
|
|
|
function push16(arr, num) {
|
|
"use strict";
|
|
arr.push((num >> 8) & 0xFF,
|
|
num & 0xFF);
|
|
}
|
|
|
|
function push32(arr, num) {
|
|
"use strict";
|
|
arr.push((num >> 24) & 0xFF,
|
|
(num >> 16) & 0xFF,
|
|
(num >> 8) & 0xFF,
|
|
num & 0xFF);
|
|
}
|
|
|
|
function pushString(arr, string) {
|
|
let utf8 = unescape(encodeURIComponent(string));
|
|
for (let i = 0; i < utf8.length; i++) {
|
|
arr.push(utf8.charCodeAt(i));
|
|
}
|
|
}
|
|
|
|
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, Z_DEFAULT_COMPRESSION);
|
|
|
|
/* eslint-disable camelcase */
|
|
strm.input = unCompData;
|
|
strm.avail_in = strm.input.length;
|
|
strm.next_in = 0;
|
|
strm.next_out = 0;
|
|
strm.avail_out = chunkSize;
|
|
/* eslint-enable camelcase */
|
|
|
|
deflate(strm, 3);
|
|
|
|
return new Uint8Array(strm.output.buffer, 0, strm.next_out);
|
|
}
|
|
|
|
describe('Remote Frame Buffer protocol client', function () {
|
|
let clock;
|
|
let raf;
|
|
let fakeResizeObserver = null;
|
|
const realObserver = window.ResizeObserver;
|
|
|
|
// Since we are using fake timers we don't actually want
|
|
// to wait for the browser to observe the size change,
|
|
// that's why we use a fake ResizeObserver
|
|
class FakeResizeObserver {
|
|
constructor(handler) {
|
|
this.fire = handler;
|
|
fakeResizeObserver = this;
|
|
}
|
|
disconnect() {}
|
|
observe(target, options) {}
|
|
unobserve(target) {}
|
|
}
|
|
|
|
before(FakeWebSocket.replace);
|
|
after(FakeWebSocket.restore);
|
|
|
|
before(function () {
|
|
this.clock = clock = sinon.useFakeTimers(Date.now());
|
|
// sinon doesn't support this yet
|
|
raf = window.requestAnimationFrame;
|
|
window.requestAnimationFrame = setTimeout;
|
|
// We must do this in a 'before' since it needs to be set before
|
|
// the RFB constructor, which runs in beforeEach further down
|
|
window.ResizeObserver = FakeResizeObserver;
|
|
// Use a single set of buffers instead of reallocating to
|
|
// speed up tests
|
|
const sock = new Websock();
|
|
const _sQ = new Uint8Array(sock._sQbufferSize);
|
|
const rQ = new Uint8Array(sock._rQbufferSize);
|
|
|
|
Websock.prototype._oldAllocateBuffers = Websock.prototype._allocateBuffers;
|
|
Websock.prototype._allocateBuffers = function () {
|
|
this._sQ = _sQ;
|
|
this._rQ = rQ;
|
|
};
|
|
|
|
// Avoiding printing the entire Websock buffer on errors
|
|
Websock.prototype.inspect = function () { return "[object Websock]"; };
|
|
});
|
|
|
|
after(function () {
|
|
Websock.prototype._allocateBuffers = Websock.prototype._oldAllocateBuffers;
|
|
delete Websock.prototype.inspect;
|
|
this.clock.restore();
|
|
window.requestAnimationFrame = raf;
|
|
window.ResizeObserver = realObserver;
|
|
});
|
|
|
|
let container;
|
|
let rfbs;
|
|
|
|
beforeEach(function () {
|
|
// Create a container element for all RFB objects to attach to
|
|
container = document.createElement('div');
|
|
container.style.width = "100%";
|
|
container.style.height = "100%";
|
|
document.body.appendChild(container);
|
|
|
|
// And track all created RFB objects
|
|
rfbs = [];
|
|
});
|
|
afterEach(function () {
|
|
// Make sure every created RFB object is properly cleaned up
|
|
// or they might affect subsequent tests
|
|
rfbs.forEach(function (rfb) {
|
|
rfb.disconnect();
|
|
expect(rfb._disconnect).to.have.been.called;
|
|
});
|
|
rfbs = [];
|
|
|
|
document.body.removeChild(container);
|
|
container = null;
|
|
});
|
|
|
|
function makeRFB(url, options) {
|
|
url = url || 'wss://host:8675';
|
|
const rfb = new RFB(container, url, options);
|
|
clock.tick();
|
|
rfb._sock._websocket._open();
|
|
rfb._rfbConnectionState = 'connected';
|
|
sinon.spy(rfb, "_disconnect");
|
|
rfbs.push(rfb);
|
|
return rfb;
|
|
}
|
|
|
|
describe('Connecting/Disconnecting', function () {
|
|
describe('#RFB (constructor)', function () {
|
|
let open, attach;
|
|
beforeEach(function () {
|
|
open = sinon.spy(Websock.prototype, 'open');
|
|
attach = sinon.spy(Websock.prototype, 'attach');
|
|
});
|
|
afterEach(function () {
|
|
open.restore();
|
|
attach.restore();
|
|
});
|
|
|
|
it('should actually connect to the websocket', function () {
|
|
new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
|
|
expect(open).to.have.been.calledOnceWithExactly('ws://HOST:8675/PATH', []);
|
|
});
|
|
|
|
it('should pass on connection problems', function () {
|
|
open.restore();
|
|
open = sinon.stub(Websock.prototype, 'open');
|
|
open.throws(new Error('Failure'));
|
|
expect(() => new RFB(document.createElement('div'), 'ws://HOST:8675/PATH')).to.throw('Failure');
|
|
});
|
|
|
|
it('should handle WebSocket/RTCDataChannel objects', function () {
|
|
let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
|
|
new RFB(document.createElement('div'), sock);
|
|
expect(open).to.not.have.been.called;
|
|
expect(attach).to.have.been.calledOnceWithExactly(sock);
|
|
});
|
|
|
|
it('should handle already open WebSocket/RTCDataChannel objects', function () {
|
|
let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
|
|
sock._open();
|
|
const client = new RFB(document.createElement('div'), sock);
|
|
let callback = sinon.spy();
|
|
client.addEventListener('disconnect', callback);
|
|
expect(open).to.not.have.been.called;
|
|
expect(attach).to.have.been.calledOnceWithExactly(sock);
|
|
// Check if it is ready for some data
|
|
sock._receiveData(new Uint8Array(['R', 'F', 'B', '0', '0', '3', '0', '0', '8']));
|
|
expect(callback).to.not.have.been.called;
|
|
});
|
|
|
|
it('should refuse closed WebSocket/RTCDataChannel objects', function () {
|
|
let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
|
|
sock.readyState = WebSocket.CLOSED;
|
|
expect(() => new RFB(document.createElement('div'), sock)).to.throw();
|
|
});
|
|
|
|
it('should pass on attach problems', function () {
|
|
attach.restore();
|
|
attach = sinon.stub(Websock.prototype, 'attach');
|
|
attach.throws(new Error('Failure'));
|
|
let sock = new FakeWebSocket('ws://HOST:8675/PATH', []);
|
|
expect(() => new RFB(document.createElement('div'), sock)).to.throw('Failure');
|
|
});
|
|
});
|
|
|
|
describe('#disconnect', function () {
|
|
let client;
|
|
let close;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
close = sinon.stub(Websock.prototype, "close");
|
|
});
|
|
afterEach(function () {
|
|
close.restore();
|
|
});
|
|
|
|
it('should start closing WebSocket', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener('disconnect', callback);
|
|
client.disconnect();
|
|
expect(close).to.have.been.calledOnceWithExactly();
|
|
expect(callback).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send disconnect event', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener('disconnect', callback);
|
|
client.disconnect();
|
|
close.thisValues[0]._eventHandlers.close(new CloseEvent("close", { 'code': 1000, 'reason': "", 'wasClean': true }));
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.true;
|
|
});
|
|
|
|
it('should force disconnect if disconnecting takes too long', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener('disconnect', callback);
|
|
client.disconnect();
|
|
this.clock.tick(3 * 1000);
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.true;
|
|
});
|
|
|
|
it('should not fail if disconnect completes before timeout', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener('disconnect', callback);
|
|
client.disconnect();
|
|
client._updateConnectionState('disconnecting');
|
|
this.clock.tick(3 * 1000 / 2);
|
|
close.thisValues[0]._eventHandlers.close(new CloseEvent("close", { 'code': 1000, 'reason': "", 'wasClean': true }));
|
|
this.clock.tick(3 * 1000 / 2 + 1);
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.true;
|
|
});
|
|
|
|
it('should unregister error event handler', function () {
|
|
sinon.spy(client._sock, 'off');
|
|
client.disconnect();
|
|
expect(client._sock.off).to.have.been.calledWith('error');
|
|
});
|
|
|
|
it('should unregister message event handler', function () {
|
|
sinon.spy(client._sock, 'off');
|
|
client.disconnect();
|
|
expect(client._sock.off).to.have.been.calledWith('message');
|
|
});
|
|
|
|
it('should unregister open event handler', function () {
|
|
sinon.spy(client._sock, 'off');
|
|
client.disconnect();
|
|
expect(client._sock.off).to.have.been.calledWith('open');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Public API basic behavior', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
});
|
|
|
|
describe('#sendCtrlAlDel', function () {
|
|
it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.keyEvent(esock, 0xFFE3, 1);
|
|
RFB.messages.keyEvent(esock, 0xFFE9, 1);
|
|
RFB.messages.keyEvent(esock, 0xFFFF, 1);
|
|
RFB.messages.keyEvent(esock, 0xFFFF, 0);
|
|
RFB.messages.keyEvent(esock, 0xFFE9, 0);
|
|
RFB.messages.keyEvent(esock, 0xFFE3, 0);
|
|
let expected = ews._getSentData();
|
|
|
|
client.sendCtrlAltDel();
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send the keys if we are not in a normal state', function () {
|
|
client._rfbConnectionState = "connecting";
|
|
client.sendCtrlAltDel();
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
|
|
it('should not send the keys if we are set as view_only', function () {
|
|
client._viewOnly = true;
|
|
client.sendCtrlAltDel();
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
});
|
|
|
|
describe('#sendKey', function () {
|
|
it('should send a single key with the given code and state (down = true)', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.keyEvent(esock, 123, 1);
|
|
let expected = ews._getSentData();
|
|
|
|
client.sendKey(123, 'Key123', true);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should send both a down and up event if the state is not specified', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.keyEvent(esock, 123, 1);
|
|
RFB.messages.keyEvent(esock, 123, 0);
|
|
let expected = ews._getSentData();
|
|
|
|
client.sendKey(123, 'Key123');
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send the key if we are not in a normal state', function () {
|
|
client._rfbConnectionState = "connecting";
|
|
client.sendKey(123, 'Key123');
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
|
|
it('should not send the key if we are set as view_only', function () {
|
|
client._viewOnly = true;
|
|
client.sendKey(123, 'Key123');
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
|
|
it('should send QEMU extended events if supported', function () {
|
|
client._qemuExtKeyEventSupported = true;
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.QEMUExtendedKeyEvent(esock, 0x20, true, 0x0039);
|
|
let expected = ews._getSentData();
|
|
|
|
client.sendKey(0x20, 'Space', true);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send QEMU extended events if unknown key code', function () {
|
|
client._qemuExtKeyEventSupported = true;
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.keyEvent(esock, 123, 1);
|
|
let expected = ews._getSentData();
|
|
|
|
client.sendKey(123, 'FooBar', true);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
});
|
|
|
|
describe('#focus', function () {
|
|
it('should move focus to canvas object', function () {
|
|
sinon.spy(client._canvas, "focus");
|
|
client.focus();
|
|
expect(client._canvas.focus).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should include focus options', function () {
|
|
sinon.spy(client._canvas, "focus");
|
|
client.focus({ foobar: 12, gazonk: true });
|
|
expect(client._canvas.focus).to.have.been.calledOnce;
|
|
expect(client._canvas.focus).to.have.been.calledWith({ foobar: 12, gazonk: true});
|
|
});
|
|
});
|
|
|
|
describe('#blur', function () {
|
|
it('should remove focus from canvas object', function () {
|
|
sinon.spy(client._canvas, "blur");
|
|
client.blur();
|
|
expect(client._canvas.blur).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('#clipboardPasteFrom', function () {
|
|
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 mask unsupported characters', 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, 63]));
|
|
});
|
|
|
|
it('should mask characters, not UTF-16 code points', function () {
|
|
client.clipboardPasteFrom('😂');
|
|
|
|
expect(RFB.messages.clientCutText).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientCutText).to.have.been.calledWith(client._sock,
|
|
new Uint8Array([63]));
|
|
});
|
|
|
|
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._receiveData(new Uint8Array(data));
|
|
|
|
client.clipboardPasteFrom('extended test');
|
|
expect(RFB.messages.extendedClipboardNotify).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
it('should flush multiple times for large clipboards', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
let longText = "";
|
|
for (let i = 0; i < client._sock._sQbufferSize + 100; i++) {
|
|
longText += 'a';
|
|
}
|
|
client.clipboardPasteFrom(longText);
|
|
expect(client._sock.flush).to.have.been.calledTwice;
|
|
});
|
|
|
|
it('should not send the text if we are not in a normal state', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._rfbConnectionState = "connecting";
|
|
client.clipboardPasteFrom('abc');
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe("XVP operations", function () {
|
|
beforeEach(function () {
|
|
client._rfbXvpVer = 1;
|
|
});
|
|
|
|
it('should send the shutdown signal on #machineShutdown', function () {
|
|
client.machineShutdown();
|
|
expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02]));
|
|
});
|
|
|
|
it('should send the reboot signal on #machineReboot', function () {
|
|
client.machineReboot();
|
|
expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03]));
|
|
});
|
|
|
|
it('should send the reset signal on #machineReset', function () {
|
|
client.machineReset();
|
|
expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04]));
|
|
});
|
|
|
|
it('should not send XVP operations with higher versions than we support', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._xvpOp(2, 7);
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Clipping', function () {
|
|
let client;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
container.style.width = '70px';
|
|
container.style.height = '80px';
|
|
client.clipViewport = true;
|
|
});
|
|
|
|
it('should update display clip state when changing the property', function () {
|
|
const spy = sinon.spy(client._display, "clipViewport", ["set"]);
|
|
|
|
client.clipViewport = false;
|
|
expect(spy.set).to.have.been.calledOnce;
|
|
expect(spy.set).to.have.been.calledWith(false);
|
|
spy.set.resetHistory();
|
|
|
|
client.clipViewport = true;
|
|
expect(spy.set).to.have.been.calledOnce;
|
|
expect(spy.set).to.have.been.calledWith(true);
|
|
});
|
|
|
|
it('should update the viewport when the container size changes', function () {
|
|
sinon.spy(client._display, "viewportChangeSize");
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.viewportChangeSize).to.have.been.calledOnce;
|
|
expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50);
|
|
});
|
|
|
|
it('should update the viewport when the remote session resizes', function () {
|
|
// Simple ExtendedDesktopSize FBU message
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff,
|
|
0x00, 0x00, 0x00, 0x00 ];
|
|
|
|
sinon.spy(client._display, "viewportChangeSize");
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
// The resize will cause scrollbars on the container, this causes a
|
|
// resize observation in the browsers
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
// FIXME: Display implicitly calls viewportChangeSize() when
|
|
// resizing the framebuffer, hence calledTwice.
|
|
expect(client._display.viewportChangeSize).to.have.been.calledTwice;
|
|
expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80);
|
|
});
|
|
|
|
it('should not update the viewport if not clipping', function () {
|
|
client.clipViewport = false;
|
|
sinon.spy(client._display, "viewportChangeSize");
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.viewportChangeSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not update the viewport if scaling', function () {
|
|
client.scaleViewport = true;
|
|
sinon.spy(client._display, "viewportChangeSize");
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.viewportChangeSize).to.not.have.been.called;
|
|
});
|
|
|
|
describe('Clipping and remote resize', function () {
|
|
beforeEach(function () {
|
|
// Given a remote (100, 100) larger than the container (70x80),
|
|
client._resize(100, 100);
|
|
client._supportsSetDesktopSize = true;
|
|
client.resizeSession = true;
|
|
sinon.spy(RFB.messages, "setDesktopSize");
|
|
});
|
|
afterEach(function () {
|
|
RFB.messages.setDesktopSize.restore();
|
|
});
|
|
it('should not change remote size when changing clipping', function () {
|
|
// When changing clipping the scrollbars of the container
|
|
// will appear and disappear and thus trigger resize observations
|
|
client.clipViewport = false;
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
client.clipViewport = true;
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
// Then no resize requests should be sent
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('Dragging', function () {
|
|
beforeEach(function () {
|
|
client.dragViewport = true;
|
|
sinon.spy(RFB.messages, "pointerEvent");
|
|
});
|
|
|
|
afterEach(function () {
|
|
RFB.messages.pointerEvent.restore();
|
|
});
|
|
|
|
it('should not send button messages when initiating viewport dragging', function () {
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send button messages when release without movement', function () {
|
|
// Just up and down
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseButton(13, 9, 0x000);
|
|
expect(RFB.messages.pointerEvent).to.have.been.calledTwice;
|
|
|
|
RFB.messages.pointerEvent.resetHistory();
|
|
|
|
// Small movement
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseMove(15, 14);
|
|
client._handleMouseButton(15, 14, 0x000);
|
|
expect(RFB.messages.pointerEvent).to.have.been.calledTwice;
|
|
});
|
|
|
|
it('should not send button messages when in view only', function () {
|
|
client._viewOnly = true;
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseButton(13, 9, 0x000);
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send button message directly when drag is disabled', function () {
|
|
client.dragViewport = false;
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
expect(RFB.messages.pointerEvent).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should be initiate viewport dragging on sufficient movement', function () {
|
|
sinon.spy(client._display, "viewportChangePos");
|
|
|
|
// Too small movement
|
|
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseMove(18, 9);
|
|
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
expect(client._display.viewportChangePos).to.not.have.been.called;
|
|
|
|
// Sufficient movement
|
|
|
|
client._handleMouseMove(43, 9);
|
|
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
expect(client._display.viewportChangePos).to.have.been.calledOnce;
|
|
expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0);
|
|
|
|
client._display.viewportChangePos.resetHistory();
|
|
|
|
// Now a small movement should move right away
|
|
|
|
client._handleMouseMove(43, 14);
|
|
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
expect(client._display.viewportChangePos).to.have.been.calledOnce;
|
|
expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5);
|
|
});
|
|
|
|
it('should not send button messages when dragging ends', function () {
|
|
// First the movement
|
|
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseMove(43, 9);
|
|
client._handleMouseButton(43, 9, 0x000);
|
|
|
|
expect(RFB.messages.pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should terminate viewport dragging on a button up event', function () {
|
|
// First the dragging movement
|
|
|
|
client._handleMouseButton(13, 9, 0x001);
|
|
client._handleMouseMove(43, 9);
|
|
client._handleMouseButton(43, 9, 0x000);
|
|
|
|
// Another movement now should not move the viewport
|
|
|
|
sinon.spy(client._display, "viewportChangePos");
|
|
|
|
client._handleMouseMove(43, 59);
|
|
|
|
expect(client._display.viewportChangePos).to.not.have.been.called;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Scaling', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
container.style.width = '70px';
|
|
container.style.height = '80px';
|
|
client.scaleViewport = true;
|
|
});
|
|
|
|
it('should update display scale factor when changing the property', function () {
|
|
const spy = sinon.spy(client._display, "scale", ["set"]);
|
|
sinon.spy(client._display, "autoscale");
|
|
|
|
client.scaleViewport = false;
|
|
expect(spy.set).to.have.been.calledOnce;
|
|
expect(spy.set).to.have.been.calledWith(1.0);
|
|
expect(client._display.autoscale).to.not.have.been.called;
|
|
|
|
client.scaleViewport = true;
|
|
expect(client._display.autoscale).to.have.been.calledOnce;
|
|
expect(client._display.autoscale).to.have.been.calledWith(70, 80);
|
|
});
|
|
|
|
it('should update the clipping setting when changing the property', function () {
|
|
client.clipViewport = true;
|
|
|
|
const spy = sinon.spy(client._display, "clipViewport", ["set"]);
|
|
|
|
client.scaleViewport = false;
|
|
expect(spy.set).to.have.been.calledOnce;
|
|
expect(spy.set).to.have.been.calledWith(true);
|
|
|
|
spy.set.resetHistory();
|
|
|
|
client.scaleViewport = true;
|
|
expect(spy.set).to.have.been.calledOnce;
|
|
expect(spy.set).to.have.been.calledWith(false);
|
|
});
|
|
|
|
it('should update the scaling when the container size changes', function () {
|
|
sinon.spy(client._display, "autoscale");
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.autoscale).to.have.been.calledOnce;
|
|
expect(client._display.autoscale).to.have.been.calledWith(40, 50);
|
|
});
|
|
|
|
it('should update the scaling when the remote session resizes', function () {
|
|
// Simple ExtendedDesktopSize FBU message
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff,
|
|
0x00, 0x00, 0x00, 0x00 ];
|
|
|
|
sinon.spy(client._display, "autoscale");
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
// The resize will cause scrollbars on the container, this causes a
|
|
// resize observation in the browsers
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.autoscale).to.have.been.calledOnce;
|
|
expect(client._display.autoscale).to.have.been.calledWith(70, 80);
|
|
});
|
|
|
|
it('should not update the display scale factor if not scaling', function () {
|
|
client.scaleViewport = false;
|
|
|
|
sinon.spy(client._display, "autoscale");
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(client._display.autoscale).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('Remote resize', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
client.resizeSession = true;
|
|
container.style.width = '70px';
|
|
container.style.height = '80px';
|
|
|
|
const incoming = [ 0x00, // msg-type=FBU
|
|
0x00, // padding
|
|
0x00, 0x01, // number of rects = 1
|
|
0x00, 0x00, // reason = server initialized
|
|
0x00, 0x00, // status = no error
|
|
0x00, 0x04, // new width = 4
|
|
0x00, 0x04, // new height = 4
|
|
0xff, 0xff,
|
|
0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize
|
|
0x01, // number of screens = 1
|
|
0x00, 0x00,
|
|
0x00, // padding
|
|
0x78, 0x90,
|
|
0xab, 0xcd, // screen id = 0
|
|
0x00, 0x00, // screen x = 0
|
|
0x00, 0x00, // screen y = 0
|
|
0x00, 0x04, // screen width = 4
|
|
0x00, 0x04, // screen height = 4
|
|
0x12, 0x34,
|
|
0x56, 0x78]; // screen flags
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
|
|
sinon.spy(RFB.messages, "setDesktopSize");
|
|
});
|
|
|
|
afterEach(function () {
|
|
RFB.messages.setDesktopSize.restore();
|
|
});
|
|
|
|
it('should only request a resize when turned on', function () {
|
|
client.resizeSession = false;
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
client.resizeSession = true;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should request a resize when initially connecting', function () {
|
|
// Create a new object that hasn't yet seen a
|
|
// ExtendedDesktopSize rect
|
|
client = makeRFB();
|
|
client.resizeSession = true;
|
|
container.style.width = '70px';
|
|
container.style.height = '80px';
|
|
|
|
// Simple ExtendedDesktopSize FBU message
|
|
const incoming = [ 0x00, // msg-type=FBU
|
|
0x00, // padding
|
|
0x00, 0x01, // number of rects = 1
|
|
0x00, 0x00, // reason = server initialized
|
|
0x00, 0x00, // status = no error
|
|
0x00, 0x04, // new width = 4
|
|
0x00, 0x04, // new height = 4
|
|
0xff, 0xff,
|
|
0xfe, 0xcc, // enc = (-308) ExtendedDesktopSize
|
|
0x01, // number of screens = 1
|
|
0x00, 0x00,
|
|
0x00, // padding
|
|
0x78, 0x90,
|
|
0xab, 0xcd, // screen id = 0
|
|
0x00, 0x00, // screen x = 0
|
|
0x00, 0x00, // screen y = 0
|
|
0x00, 0x04, // screen width = 4
|
|
0x00, 0x04, // screen height = 4
|
|
0x12, 0x34,
|
|
0x56, 0x78]; // screen flags
|
|
|
|
// First message should trigger a resize
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
|
|
// It should match the current size of the container,
|
|
// not the reported size from the server
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
|
|
sinon.match.object, 70, 80, 0x7890abcd, 0x12345678);
|
|
|
|
RFB.messages.setDesktopSize.resetHistory();
|
|
|
|
// Second message should not trigger a resize
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should request a resize when the container resizes', function () {
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
|
|
sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);
|
|
});
|
|
|
|
it('should not request the same size twice', function () {
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
|
|
sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);
|
|
|
|
// Server responds with the requested size 40x50
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x28, 0x00, 0x32, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x32,
|
|
0x00, 0x00, 0x00, 0x00];
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
clock.tick(1000);
|
|
|
|
RFB.messages.setDesktopSize.resetHistory();
|
|
|
|
// size is still 40x50
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not resize until the container size is stable', function () {
|
|
container.style.width = '20px';
|
|
container.style.height = '30px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(400);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(400);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
|
|
clock.tick(200);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
|
|
sinon.match.object, 40, 50, 0x7890abcd, 0x12345678);
|
|
});
|
|
|
|
it('should not resize when resize is disabled', function () {
|
|
client._resizeSession = false;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not resize when resize is not supported', function () {
|
|
client._supportsSetDesktopSize = false;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not resize when in view only mode', function () {
|
|
client._viewOnly = true;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not try to override a server resize', function () {
|
|
// Simple ExtendedDesktopSize FBU message, new size: 100x100
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x64, 0x00, 0x64, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0xab, 0xab, 0xab, 0xab,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04,
|
|
0x11, 0x22, 0x33, 0x44 ];
|
|
|
|
// Note that this will cause the browser to display scrollbars
|
|
// since the framebuffer is 100x100 and the container is 70x80.
|
|
// The usable space (clientWidth/clientHeight) will be even smaller
|
|
// due to the scrollbars taking up space.
|
|
client._sock._websocket._receiveData(new Uint8Array(incoming));
|
|
// The scrollbars cause the ResizeObserver to fire
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
|
|
// An actual size change must not be ignored afterwards
|
|
container.style.width = '120px';
|
|
container.style.height = '130px';
|
|
fakeResizeObserver.fire();
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(
|
|
sinon.match.object, 120, 130, 0xabababab, 0x11223344);
|
|
});
|
|
});
|
|
|
|
describe('Misc internals', function () {
|
|
describe('#_fail', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
});
|
|
|
|
it('should close the WebSocket connection', function () {
|
|
sinon.spy(client._sock, 'close');
|
|
client._fail();
|
|
expect(client._sock.close).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should transition to disconnected', function () {
|
|
sinon.spy(client, '_updateConnectionState');
|
|
client._fail();
|
|
this.clock.tick(2000);
|
|
expect(client._updateConnectionState).to.have.been.called;
|
|
expect(client._rfbConnectionState).to.equal('disconnected');
|
|
});
|
|
|
|
it('should set clean_disconnect variable', function () {
|
|
client._rfbCleanDisconnect = true;
|
|
client._rfbConnectionState = 'connected';
|
|
client._fail();
|
|
expect(client._rfbCleanDisconnect).to.be.false;
|
|
});
|
|
|
|
it('should result in disconnect event with clean set to false', function () {
|
|
client._rfbConnectionState = 'connected';
|
|
const spy = sinon.spy();
|
|
client.addEventListener("disconnect", spy);
|
|
client._fail();
|
|
this.clock.tick(2000);
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
});
|
|
});
|
|
|
|
describe('Protocol initialization states', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
client._rfbConnectionState = 'connecting';
|
|
});
|
|
|
|
function sendVer(ver, client) {
|
|
const arr = new Uint8Array(12);
|
|
for (let i = 0; i < ver.length; i++) {
|
|
arr[i+4] = ver.charCodeAt(i);
|
|
}
|
|
arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
|
|
arr[11] = '\n';
|
|
client._sock._websocket._receiveData(arr);
|
|
}
|
|
|
|
function sendSecurity(type, cl) {
|
|
cl._sock._websocket._receiveData(new Uint8Array([1, type]));
|
|
}
|
|
|
|
describe('ProtocolVersion', function () {
|
|
describe('version parsing', function () {
|
|
it('should interpret version 003.003 as version 3.3', function () {
|
|
sendVer('003.003', client);
|
|
expect(client._rfbVersion).to.equal(3.3);
|
|
});
|
|
|
|
it('should interpret version 003.006 as version 3.3', function () {
|
|
sendVer('003.006', client);
|
|
expect(client._rfbVersion).to.equal(3.3);
|
|
});
|
|
|
|
it('should interpret version 003.889 as version 3.8', function () {
|
|
sendVer('003.889', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 003.007 as version 3.7', function () {
|
|
sendVer('003.007', client);
|
|
expect(client._rfbVersion).to.equal(3.7);
|
|
});
|
|
|
|
it('should interpret version 003.008 as version 3.8', function () {
|
|
sendVer('003.008', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 004.000 as version 3.8', function () {
|
|
sendVer('004.000', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 004.001 as version 3.8', function () {
|
|
sendVer('004.001', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 005.000 as version 3.8', function () {
|
|
sendVer('005.000', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
|
|
it('should fail on an invalid version', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
sendVer('002.000', client);
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
});
|
|
|
|
it('should send back the interpreted version', function () {
|
|
sendVer('004.000', client);
|
|
|
|
const expectedStr = 'RFB 003.008\n';
|
|
const expected = [];
|
|
for (let i = 0; i < expectedStr.length; i++) {
|
|
expected[i] = expectedStr.charCodeAt(i);
|
|
}
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should transition to the Security state on successful negotiation', function () {
|
|
sendVer('003.008', client);
|
|
expect(client._rfbInitState).to.equal('Security');
|
|
});
|
|
|
|
describe('Repeater', function () {
|
|
beforeEach(function () {
|
|
client = makeRFB('wss://host:8675', { repeaterID: "12345" });
|
|
client._rfbConnectionState = 'connecting';
|
|
});
|
|
|
|
it('should interpret version 000.000 as a repeater', function () {
|
|
sendVer('000.000', client);
|
|
expect(client._rfbVersion).to.equal(0);
|
|
|
|
const sentData = client._sock._websocket._getSentData();
|
|
expect(new Uint8Array(sentData.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0]));
|
|
expect(sentData).to.have.length(250);
|
|
});
|
|
|
|
it('should handle two step repeater negotiation', function () {
|
|
sendVer('000.000', client);
|
|
sendVer('003.008', client);
|
|
expect(client._rfbVersion).to.equal(3.8);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Security', function () {
|
|
beforeEach(function () {
|
|
sendVer('003.008\n', client);
|
|
client._sock._websocket._getSentData();
|
|
});
|
|
|
|
it('should respect server preference order', function () {
|
|
const authSchemes = [ 6, 79, 30, 188, 16, 6, 1 ];
|
|
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
|
|
expect(client._sock).to.have.sent(new Uint8Array([30]));
|
|
});
|
|
|
|
it('should fail if there are no supported schemes', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
const authSchemes = [1, 32];
|
|
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
it('should fail with the appropriate message if no types are sent', function () {
|
|
const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
|
|
let callback = sinon.spy();
|
|
client.addEventListener("securityfailure", callback);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(failureData));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.status).to.equal(1);
|
|
expect(callback.args[0][0].detail.reason).to.equal("whoops");
|
|
});
|
|
|
|
it('should transition to the Authentication state and continue on successful negotiation', function () {
|
|
const authSchemes = [1, 2];
|
|
sinon.spy(client, "_negotiateAuthentication");
|
|
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
|
|
expect(client._rfbInitState).to.equal('Authentication');
|
|
expect(client._negotiateAuthentication).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('Legacy authentication', function () {
|
|
it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
|
|
const errMsg = "Whoopsies";
|
|
const data = [0, 0, 0, 0];
|
|
const errLen = errMsg.length;
|
|
push32(data, errLen);
|
|
for (let i = 0; i < errLen; i++) {
|
|
data.push(errMsg.charCodeAt(i));
|
|
}
|
|
|
|
sendVer('003.006\n', client);
|
|
client._sock._websocket._getSentData();
|
|
let callback = sinon.spy();
|
|
client.addEventListener("securityfailure", callback);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.status).to.equal(1);
|
|
expect(callback.args[0][0].detail.reason).to.equal("Whoopsies");
|
|
});
|
|
|
|
it('should transition straight to ServerInitialisation on "no auth" for versions < 3.7', function () {
|
|
sendVer('003.006\n', client);
|
|
client._sock._websocket._getSentData();
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._rfbInitState).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () {
|
|
sendVer('003.007\n', client);
|
|
client._sock._websocket._getSentData();
|
|
|
|
sendSecurity(1, client);
|
|
expect(client._rfbInitState).to.equal('ServerInitialisation');
|
|
});
|
|
});
|
|
|
|
describe('Authentication', function () {
|
|
beforeEach(function () {
|
|
sendVer('003.008\n', client);
|
|
client._sock._websocket._getSentData();
|
|
});
|
|
|
|
it('should transition straight to SecurityResult on "no auth" (1)', function () {
|
|
sendSecurity(1, client);
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
});
|
|
|
|
it('should fail on an unknown auth scheme', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
sendSecurity(57, client);
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
describe('VNC authentication (type 2) handler', function () {
|
|
it('should fire the credentialsrequired event if missing a password', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
sendSecurity(2, client);
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receiveData(new Uint8Array(challenge));
|
|
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["password"]);
|
|
});
|
|
|
|
it('should encrypt the password with DES and then send it back', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ password: 'passwd' });
|
|
});
|
|
sendSecurity(2, client);
|
|
client._sock._websocket._getSentData(); // skip the choice of auth reply
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receiveData(new Uint8Array(challenge));
|
|
clock.tick();
|
|
|
|
const desPass = RFB.genDES('passwd', challenge);
|
|
expect(client._sock).to.have.sent(new Uint8Array(desPass));
|
|
});
|
|
|
|
it('should transition to SecurityResult immediately after sending the password', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ password: 'passwd' });
|
|
});
|
|
sendSecurity(2, client);
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receiveData(new Uint8Array(challenge));
|
|
clock.tick();
|
|
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
});
|
|
});
|
|
|
|
describe('RSA-AES authentication (type 6) handler', function () {
|
|
function fakeGetRandomValues(arr) {
|
|
if (arr.length === 16) {
|
|
arr.set(new Uint8Array([
|
|
0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9,
|
|
0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5,
|
|
]));
|
|
} else {
|
|
arr.set(new Uint8Array([
|
|
0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95,
|
|
0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf,
|
|
0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf,
|
|
0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87,
|
|
0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e,
|
|
0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a,
|
|
0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3,
|
|
0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62,
|
|
0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69,
|
|
0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0,
|
|
0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c,
|
|
0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77,
|
|
0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7,
|
|
0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f,
|
|
0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15,
|
|
0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70,
|
|
0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64,
|
|
0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c,
|
|
0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14,
|
|
0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b,
|
|
0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a,
|
|
0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f,
|
|
0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f,
|
|
0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58,
|
|
0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc,
|
|
0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2,
|
|
0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f,
|
|
0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a,
|
|
0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81,
|
|
0x31, 0xb0, 0x69, 0xd4, 0x4e,
|
|
]));
|
|
}
|
|
}
|
|
|
|
async function fakeGeneratekey() {
|
|
let key = { "alg": "RSA-OAEP-256",
|
|
"d": "B7QR2yI8sXjo8vQhJpX9odqqR6wIuPr" +
|
|
"TM1B1JJEKVeSrr7OYcc1FRJ52Vap9LI" +
|
|
"AU-ezigs9QDvWMxknB8motLnG69Wck3" +
|
|
"7nt9_z4s8lFQp0nROA-oaR92HW34KNL" +
|
|
"1b2fEVWGI0N86h730MvTJC5O2cmKeMe" +
|
|
"zIG-oNqbbfFyP8AW-WLdDlgZm11-Fjz" +
|
|
"hbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx" +
|
|
"4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5" +
|
|
"WeoyX3yBtQydpauW6rrgyWdtP4hDFIo" +
|
|
"ZsX6w1i-UMWMMwlIB5FdnUSi26igVGA" +
|
|
"DGpV_vGMP36bv-EHp0bY-Qp0gpIfLfgQ",
|
|
"dp": "Z1v5UceFfV2bhmbG19eGYb30jFxqoR" +
|
|
"Bq36PKNY7IunMs1keYy0FpLbyGhtgM" +
|
|
"Z1Ymmc8wEzGYsCPEP-ykcun_rlyu7Y" +
|
|
"xmcnyC9YQqTqLyqvO-7rUqDvk9TMfd" +
|
|
"qWFP6heADRhKZmEbmcau6_m2MwwK9k" +
|
|
"OkMKWvpqp8_TpJMnAH7zE",
|
|
"dq": "OBacRE15aY3NtCR4cvP5os3sT70JbD" +
|
|
"dDLHT3IHZM6rE35CYNpLDia2chm_wn" +
|
|
"McYvKFW9zC2ajRZ15i9c_VXQzS7ZlT" +
|
|
"aQYBFyMt7kVhxMEMFsPv1crD6t3uEI" +
|
|
"j0LNuNYyy0jkon_LPZKQFK654CiL-L" +
|
|
"2YaNXOH4HbHP02dWeVQIE",
|
|
"e": "AQAB",
|
|
"ext": true,
|
|
"key_ops": ["decrypt"],
|
|
"kty": "RSA",
|
|
"n": "m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3" +
|
|
"yThy1P_mcqrGDQkRiGVdcTxAk38T9Pg" +
|
|
"LztmspF-6U5TAHO-gSmmW88AC9m6f1M" +
|
|
"spps6r7zl-M_OG-TwvGzf3BDz8zEg1F" +
|
|
"PbZV7whO1M4TCAZ0PqwG7qCc6nK1WiA" +
|
|
"haKrSpzuPdL1igfNBsX7qu5wgw4ZTTG" +
|
|
"SLbVC_LfULQ5FADgFTRXUSaxm1F8C_L" +
|
|
"wy6a2e4nTcXilmtN2IHUjHegzm-Tq2H" +
|
|
"izmR3ARdWJpESYIW5-AXoiqj29tDrqC" +
|
|
"mu2WPkB2psVp83IzZfaQNQzjNfvA8Gp" +
|
|
"imkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q",
|
|
"p": "2Q_lNL7vCOBzAppYzCZo3WSh0hX-MOZ" +
|
|
"yPUznks5U2TjmfdNZoL6_FJRiGyyLvw" +
|
|
"SiZFdEAAvpAyESFfFigngAqMLSf448n" +
|
|
"Pg15VUGj533CotsEM0WpoEr1JCgqdUb" +
|
|
"gDAfJQIBcwOmegBqd7lWm7uzEnRCvou" +
|
|
"B70ybkJfpdprhkVE",
|
|
"q": "tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXG" +
|
|
"zslLt5nLmss8JqdLoDDrijjU-gjeRh7" +
|
|
"lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ" +
|
|
"1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXb" +
|
|
"Mq2sUZqJvYEyL74H2Zrt0RPAux7XQLE" +
|
|
"VgND6ROdXnMJ70O0",
|
|
"qi": "qfl4cXQkz4BNqa2De0-PfdU-8d1w3o" +
|
|
"nnaGqx1Ds2fHzD_SJ4cNghn2TksoT9" +
|
|
"Qo64b3pUjH9igi2pyEjomk6D12N6FG" +
|
|
"0e10u7vFKv3W5YqUOgTpYdbcWHdZ2q" +
|
|
"ZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7" +
|
|
"Wk_0MdqQy8qvixWD4zLcY",
|
|
};
|
|
key = await window.crypto.subtle.importKey("jwk", key, {
|
|
name: "RSA-OAEP",
|
|
hash: {name: "SHA-256"}
|
|
}, true, ["decrypt"]);
|
|
return {privateKey: key};
|
|
}
|
|
|
|
before(() => {
|
|
sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
|
|
sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey);
|
|
});
|
|
after(() => {
|
|
window.crypto.getRandomValues.restore();
|
|
window.crypto.subtle.generateKey.restore();
|
|
});
|
|
|
|
beforeEach(function () {
|
|
sendSecurity(6, client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([6]));
|
|
});
|
|
|
|
const serverPublicKey = [
|
|
0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42,
|
|
0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6,
|
|
0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f,
|
|
0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08,
|
|
0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a,
|
|
0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d,
|
|
0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40,
|
|
0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd,
|
|
0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33,
|
|
0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07,
|
|
0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7,
|
|
0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11,
|
|
0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7,
|
|
0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff,
|
|
0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca,
|
|
0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39,
|
|
0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda,
|
|
0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a,
|
|
0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07,
|
|
0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a,
|
|
0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a,
|
|
0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62,
|
|
0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f,
|
|
0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39,
|
|
0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3,
|
|
0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7,
|
|
0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d,
|
|
0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98,
|
|
0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71,
|
|
0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31,
|
|
0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f,
|
|
0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01,
|
|
0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x01, 0x00, 0x01,
|
|
];
|
|
|
|
const serverRandom = [
|
|
0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb,
|
|
0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe,
|
|
0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31,
|
|
0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b,
|
|
0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe,
|
|
0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0,
|
|
0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79,
|
|
0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6,
|
|
0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66,
|
|
0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a,
|
|
0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4,
|
|
0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5,
|
|
0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11,
|
|
0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35,
|
|
0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32,
|
|
0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29,
|
|
0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4,
|
|
0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6,
|
|
0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab,
|
|
0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48,
|
|
0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15,
|
|
0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5,
|
|
0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a,
|
|
0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36,
|
|
0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11,
|
|
0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11,
|
|
0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59,
|
|
0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d,
|
|
0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95,
|
|
0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14,
|
|
0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e,
|
|
0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8,
|
|
0xdf, 0xcb,
|
|
];
|
|
|
|
const serverHash = [
|
|
0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a,
|
|
0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47,
|
|
0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01,
|
|
0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e,
|
|
0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13,
|
|
];
|
|
|
|
const subType = [
|
|
0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c,
|
|
0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7,
|
|
0x94, 0xd0, 0x19,
|
|
];
|
|
|
|
const clientPublicKey = [
|
|
0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9,
|
|
0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8,
|
|
0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f,
|
|
0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f,
|
|
0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62,
|
|
0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4,
|
|
0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17,
|
|
0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12,
|
|
0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7,
|
|
0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce,
|
|
0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6,
|
|
0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d,
|
|
0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4,
|
|
0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06,
|
|
0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20,
|
|
0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74,
|
|
0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea,
|
|
0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64,
|
|
0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43,
|
|
0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12,
|
|
0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c,
|
|
0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a,
|
|
0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde,
|
|
0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39,
|
|
0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49,
|
|
0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3,
|
|
0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65,
|
|
0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc,
|
|
0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd,
|
|
0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0,
|
|
0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28,
|
|
0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80,
|
|
0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x01, 0x00, 0x01,
|
|
];
|
|
|
|
const clientRandom = [
|
|
0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6,
|
|
0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30,
|
|
0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c,
|
|
0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68,
|
|
0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4,
|
|
0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c,
|
|
0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b,
|
|
0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d,
|
|
0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e,
|
|
0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a,
|
|
0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a,
|
|
0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5,
|
|
0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62,
|
|
0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19,
|
|
0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23,
|
|
0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c,
|
|
0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed,
|
|
0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb,
|
|
0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b,
|
|
0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27,
|
|
0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde,
|
|
0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce,
|
|
0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2,
|
|
0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b,
|
|
0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd,
|
|
0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad,
|
|
0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e,
|
|
0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b,
|
|
0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83,
|
|
0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4,
|
|
0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71,
|
|
0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d,
|
|
0xed, 0x84,
|
|
];
|
|
|
|
const clientHash = [
|
|
0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e,
|
|
0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea,
|
|
0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f,
|
|
0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84,
|
|
0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe,
|
|
];
|
|
|
|
const credentialsData = [
|
|
0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f,
|
|
0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82,
|
|
0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29,
|
|
0x91, 0x38,
|
|
];
|
|
|
|
it('should fire the serververification event', async function () {
|
|
let verification = new Promise((resolve, reject) => {
|
|
client.addEventListener("serververification", (e) => {
|
|
resolve(e.detail.publickey);
|
|
});
|
|
});
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));
|
|
client._sock._websocket._receiveData(new Uint8Array(serverRandom));
|
|
|
|
expect(await verification).to.deep.equal(new Uint8Array(serverPublicKey));
|
|
});
|
|
|
|
it('should handle approveServer and fire the credentialsrequired event', async function () {
|
|
let verification = new Promise((resolve, reject) => {
|
|
client.addEventListener("serververification", (e) => {
|
|
resolve(e.detail.publickey);
|
|
});
|
|
});
|
|
let credentials = new Promise((resolve, reject) => {
|
|
client.addEventListener("credentialsrequired", (e) => {
|
|
resolve(e.detail.types);
|
|
});
|
|
});
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));
|
|
client._sock._websocket._receiveData(new Uint8Array(serverRandom));
|
|
|
|
await verification;
|
|
client.approveServer();
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(serverHash));
|
|
client._sock._websocket._receiveData(new Uint8Array(subType));
|
|
|
|
expect(await credentials).to.have.members(["password"]);
|
|
});
|
|
|
|
it('should send credentials to server', async function () {
|
|
let verification = new Promise((resolve, reject) => {
|
|
client.addEventListener("serververification", (e) => {
|
|
resolve(e.detail.publickey);
|
|
});
|
|
});
|
|
let credentials = new Promise((resolve, reject) => {
|
|
client.addEventListener("credentialsrequired", (e) => {
|
|
resolve(e.detail.types);
|
|
});
|
|
});
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(serverPublicKey));
|
|
client._sock._websocket._receiveData(new Uint8Array(serverRandom));
|
|
|
|
await verification;
|
|
client.approveServer();
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(serverHash));
|
|
client._sock._websocket._receiveData(new Uint8Array(subType));
|
|
|
|
await credentials;
|
|
|
|
let expected = [];
|
|
expected = expected.concat(clientPublicKey);
|
|
expected = expected.concat(clientRandom);
|
|
expected = expected.concat(clientHash);
|
|
expect(client._sock).to.have.sent(new Uint8Array(expected));
|
|
|
|
client.sendCredentials({ "password": "123456" });
|
|
clock.tick();
|
|
|
|
// FIXME: We don't have a good way to know when
|
|
// the async stuff is done, so we hook in
|
|
// to this internal function that is
|
|
// called at the end
|
|
await new Promise((resolve, reject) => {
|
|
sinon.stub(client._sock._websocket, "send")
|
|
.callsFake((data) => {
|
|
FakeWebSocket.prototype.send.call(client._sock._websocket, data);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(credentialsData));
|
|
});
|
|
});
|
|
|
|
describe('ARD authentication (type 30) handler', function () {
|
|
let byteArray = new Uint8Array(Array.from(new Uint8Array(128).keys()));
|
|
function fakeGetRandomValues(arr) {
|
|
if (arr.length == 128) {
|
|
arr.set(byteArray);
|
|
}
|
|
return arr;
|
|
}
|
|
before(() => {
|
|
sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
|
|
});
|
|
after(() => {
|
|
window.crypto.getRandomValues.restore();
|
|
});
|
|
it('should fire the credentialsrequired event if all credentials are missing', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
sendSecurity(30, client);
|
|
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["username", "password"]);
|
|
});
|
|
|
|
it('should fire the credentialsrequired event if some credentials are missing', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
client.sendCredentials({ password: 'password'});
|
|
sendSecurity(30, client);
|
|
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["username", "password"]);
|
|
});
|
|
|
|
it('should return properly encrypted credentials and public key', async function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'user',
|
|
password: 'password' });
|
|
});
|
|
sendSecurity(30, client);
|
|
|
|
expect(client._sock).to.have.sent([30]);
|
|
|
|
const generator = new Uint8Array([127, 255]);
|
|
const prime = new Uint8Array(byteArray);
|
|
const serverKey = legacyCrypto.generateKey(
|
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
|
const clientKey = legacyCrypto.generateKey(
|
|
{ name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
|
|
const serverPublicKey = legacyCrypto.exportKey("raw", serverKey.publicKey);
|
|
const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
|
|
|
|
let data = [];
|
|
|
|
data = data.concat(Array.from(generator));
|
|
push16(data, prime.length);
|
|
data = data.concat(Array.from(prime));
|
|
data = data.concat(Array.from(serverPublicKey));
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
|
|
// FIXME: We don't have a good way to know when the
|
|
// async stuff is done, so we hook in to this
|
|
// internal function that is called at the
|
|
// end
|
|
await new Promise((resolve, reject) => {
|
|
sinon.stub(client, "_resumeAuthentication")
|
|
.callsFake(() => {
|
|
RFB.prototype._resumeAuthentication.call(client);
|
|
resolve();
|
|
});
|
|
});
|
|
clock.tick();
|
|
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
|
|
let expectEncrypted = new Uint8Array([
|
|
199, 39, 204, 95, 190, 70, 127, 66, 5, 106, 153, 228, 123, 236, 150, 206,
|
|
62, 107, 11, 4, 21, 242, 92, 184, 9, 81, 35, 125, 56, 167, 1, 215,
|
|
182, 145, 183, 75, 245, 197, 47, 19, 122, 94, 64, 76, 77, 163, 222, 143,
|
|
186, 174, 84, 39, 244, 179, 227, 114, 83, 231, 42, 106, 205, 43, 159, 110,
|
|
209, 240, 157, 246, 237, 206, 134, 153, 195, 112, 92, 60, 28, 234, 91, 66,
|
|
131, 38, 187, 195, 110, 167, 212, 241, 32, 250, 212, 213, 202, 89, 180, 21,
|
|
71, 217, 209, 81, 42, 61, 118, 248, 65, 123, 98, 78, 139, 111, 202, 137,
|
|
50, 185, 37, 173, 58, 99, 187, 53, 42, 125, 13, 165, 232, 163, 151, 42, 0]);
|
|
|
|
let output = new Uint8Array(256);
|
|
output.set(expectEncrypted, 0);
|
|
output.set(clientPublicKey, 128);
|
|
|
|
expect(client._sock).to.have.sent(output);
|
|
});
|
|
});
|
|
|
|
describe('MSLogonII authentication (type 113) handler', function () {
|
|
function fakeGetRandomValues(arr) {
|
|
if (arr.length == 8) {
|
|
arr.set(new Uint8Array([0, 0, 0, 0, 5, 6, 7, 8]));
|
|
} else if (arr.length == 256) {
|
|
arr.set(new Uint8Array(256));
|
|
} else if (arr.length == 64) {
|
|
arr.set(new Uint8Array(64));
|
|
}
|
|
return arr;
|
|
}
|
|
const expected = new Uint8Array([
|
|
0x00, 0x00, 0x00, 0x00, 0x0a, 0xbc, 0x7c, 0xfd,
|
|
0x58, 0x34, 0xd2, 0x24, 0x44, 0x60, 0xf0, 0xd1,
|
|
0xa3, 0x73, 0x32, 0x02, 0x07, 0xce, 0xc1, 0x3f,
|
|
0x10, 0x53, 0xf1, 0xdd, 0x99, 0xad, 0x44, 0x18,
|
|
0xa1, 0xc4, 0xac, 0xc1, 0x1c, 0x13, 0x11, 0x85,
|
|
0x3a, 0x6f, 0xcb, 0xc6, 0xb1, 0x6c, 0x68, 0x47,
|
|
0x85, 0x01, 0xbb, 0xfa, 0x23, 0x8c, 0x59, 0x47,
|
|
0x67, 0x47, 0x56, 0x6e, 0x6f, 0x9f, 0x07, 0x76,
|
|
0x2e, 0x90, 0x1e, 0xdc, 0x80, 0xc4, 0x4b, 0x72,
|
|
0xd2, 0xd5, 0xcd, 0x4b, 0x14, 0xff, 0x05, 0x8b,
|
|
0x8d, 0xf1, 0x9b, 0xe0, 0xff, 0xa5, 0x3b, 0x56,
|
|
0xb9, 0x6f, 0x84, 0x3e, 0x15, 0x84, 0x31, 0x4e,
|
|
0x10, 0x0b, 0x56, 0xf4, 0x10, 0x05, 0x02, 0xc7,
|
|
0x05, 0x0b, 0xc9, 0x66, 0x75, 0x32, 0xd3, 0x74,
|
|
0xfc, 0x8c, 0xcf, 0xbd, 0x2d, 0x53, 0xd7, 0xa7,
|
|
0xca, 0x82, 0x12, 0xce, 0xbb, 0x33, 0x09, 0x3f,
|
|
0xff, 0x76, 0x7c, 0xdf, 0x2c, 0x2f, 0x4d, 0x95,
|
|
0x86, 0xe4, 0x10, 0x07, 0x75, 0x1a, 0x6d, 0xdb,
|
|
0x05, 0x91, 0x70, 0x34, 0x5c, 0x12, 0xbc, 0x4e,
|
|
0x5e, 0xd0, 0x21, 0x39, 0x25, 0x2b, 0x62, 0x19,
|
|
0x29, 0xa5, 0xe6, 0x93, 0x7b, 0xf8, 0x3f, 0xcf,
|
|
0xd7, 0x3f, 0x0c, 0xd2, 0x68, 0x2d, 0x1e, 0x01,
|
|
0x1a, 0x31, 0xc1, 0x59, 0x04, 0x06, 0xf6, 0x3b,
|
|
0xec, 0x38, 0xef, 0x1b, 0x5b, 0x39, 0x88, 0xd3,
|
|
0xe0, 0x5b, 0xb9, 0xef, 0xc3, 0x82, 0xfa, 0xdf,
|
|
0x04, 0xf7, 0x65, 0x56, 0x82, 0x77, 0xfd, 0x63,
|
|
0x10, 0xd7, 0xab, 0x0b, 0x5e, 0xd9, 0x07, 0x81,
|
|
0x9d, 0xce, 0x26, 0xfb, 0x5d, 0xa8, 0x59, 0x2a,
|
|
0xd9, 0xb8, 0xac, 0xcd, 0x6e, 0x61, 0x07, 0x39,
|
|
0x9f, 0x8d, 0xdf, 0x53, 0x44, 0xab, 0x28, 0x01,
|
|
0x86, 0x4d, 0x07, 0x8a, 0x5b, 0xdd, 0xc1, 0x18,
|
|
0x29, 0xaa, 0xa2, 0xbe, 0xe2, 0x9c, 0x9e, 0xb0,
|
|
0xb3, 0x2b, 0x2c, 0x93, 0x3e, 0x82, 0x07, 0xa6,
|
|
0xef, 0x21, 0x2c, 0xa7, 0xf0, 0x65, 0xba, 0xda,
|
|
0x13, 0xe4, 0x41, 0x87, 0x36, 0x1c, 0xa5, 0x81,
|
|
0xae, 0xf3, 0x3e, 0xda, 0x03, 0x09, 0x63, 0x4b,
|
|
0xb5, 0x29, 0x49, 0xfa, 0xbb, 0xa6, 0x31, 0x3c,
|
|
0xc8, 0x15, 0xfb, 0xfc, 0xd6, 0xff, 0x04, 0x92,
|
|
0x56, 0xbc, 0x66, 0xf1, 0x78, 0xfb, 0x14, 0x79,
|
|
0x48, 0xd2, 0xcf, 0x87, 0x60, 0x23, 0xcf, 0xdb,
|
|
0x1b, 0xad, 0x42, 0x32, 0x4e, 0x6d, 0x1f, 0x49,
|
|
]);
|
|
before(() => {
|
|
sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues);
|
|
});
|
|
after(() => {
|
|
window.crypto.getRandomValues.restore();
|
|
});
|
|
it('should send public value and encrypted credentials', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'username',
|
|
password: 'password123456' });
|
|
});
|
|
sendSecurity(113, client);
|
|
|
|
expect(client._sock).to.have.sent([113]);
|
|
|
|
const g = new Uint8Array([0, 0, 0, 0, 0, 1, 0, 1]);
|
|
const p = new Uint8Array([0, 0, 0, 0, 0x25, 0x18, 0x26, 0x17]);
|
|
const A = new Uint8Array([0, 0, 0, 0, 0x0e, 0x12, 0xd0, 0xf5]);
|
|
|
|
client._sock._websocket._receiveData(g);
|
|
client._sock._websocket._receiveData(p);
|
|
client._sock._websocket._receiveData(A);
|
|
clock.tick();
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
});
|
|
});
|
|
|
|
describe('XVP authentication (type 22) handler', function () {
|
|
it('should fall through to standard VNC authentication upon completion', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'user',
|
|
target: 'target',
|
|
password: 'password' });
|
|
});
|
|
sinon.spy(client, "_negotiateStdVNCAuth");
|
|
sendSecurity(22, client);
|
|
clock.tick();
|
|
expect(client._negotiateStdVNCAuth).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should fire the credentialsrequired event if all credentials are missing', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
sendSecurity(22, client);
|
|
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]);
|
|
});
|
|
|
|
it('should fire the credentialsrequired event if some credentials are missing', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
client.sendCredentials({ username: 'user',
|
|
target: 'target' });
|
|
sendSecurity(22, client);
|
|
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]);
|
|
});
|
|
|
|
it('should send user and target separately', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'user',
|
|
target: 'target',
|
|
password: 'password' });
|
|
});
|
|
sendSecurity(22, client);
|
|
clock.tick();
|
|
|
|
const expected = [22, 4, 6]; // auth selection, len user, len target
|
|
for (let i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); }
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('TightVNC authentication (type 16) handler', function () {
|
|
beforeEach(function () {
|
|
sendSecurity(16, client);
|
|
client._sock._websocket._getSentData(); // skip the security reply
|
|
});
|
|
|
|
function sendNumStrPairs(pairs, client) {
|
|
const data = [];
|
|
push32(data, pairs.length);
|
|
|
|
for (let i = 0; i < pairs.length; i++) {
|
|
push32(data, pairs[i][0]);
|
|
for (let j = 0; j < 4; j++) {
|
|
data.push(pairs[i][1].charCodeAt(j));
|
|
}
|
|
for (let j = 0; j < 8; j++) {
|
|
data.push(pairs[i][2].charCodeAt(j));
|
|
}
|
|
}
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
}
|
|
|
|
it('should skip tunnel negotiation if no tunnels are requested', function () {
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfbTightVNC).to.be.true;
|
|
});
|
|
|
|
it('should fail if no supported tunnels are listed', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
sendNumStrPairs([[123, 'OTHR', 'SOMETHNG']], client);
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
it('should choose the notunnel tunnel type', function () {
|
|
sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0]));
|
|
});
|
|
|
|
it('should choose the notunnel tunnel type for Siemens devices', function () {
|
|
sendNumStrPairs([[1, 'SICR', 'SCHANNEL'], [2, 'SICR', 'SCHANLPW']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0]));
|
|
});
|
|
|
|
it('should continue to sub-auth negotiation after tunnel negotiation', function () {
|
|
sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);
|
|
client._sock._websocket._getSentData(); // skip the tunnel choice here
|
|
sendNumStrPairs([[1, 'STDV', 'NOAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
});
|
|
|
|
it('should accept the "no auth" auth type and transition to SecurityResult', function () {
|
|
sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);
|
|
client._sock._websocket._getSentData(); // skip the tunnel choice here
|
|
sendNumStrPairs([[1, 'STDV', 'NOAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._rfbInitState).to.equal('SecurityResult');
|
|
});
|
|
|
|
it('should accept VNC authentication and transition to that', function () {
|
|
sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);
|
|
client._sock._websocket._getSentData(); // skip the tunnel choice here
|
|
sinon.spy(client, "_negotiateStdVNCAuth");
|
|
sendNumStrPairs([[2, 'STDV', 'VNCAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2]));
|
|
expect(client._negotiateStdVNCAuth).to.have.been.calledOnce;
|
|
expect(client._rfbAuthScheme).to.equal(2);
|
|
});
|
|
|
|
it('should fail if there are no supported auth types', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client);
|
|
client._sock._websocket._getSentData(); // skip the tunnel choice here
|
|
sendNumStrPairs([[23, 'stdv', 'badval__']], client);
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('VeNCrypt authentication (type 19) handler', function () {
|
|
beforeEach(function () {
|
|
sendSecurity(19, client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([19]));
|
|
});
|
|
|
|
it('should fail with non-0.2 versions', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 1]));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
it('should fail if there are no supported subtypes', function () {
|
|
// VeNCrypt version
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
|
|
// Server ACK.
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
// Subtype list
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4]));
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
it('should support standard types', function () {
|
|
// VeNCrypt version
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
|
|
// Server ACK.
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
// Subtype list
|
|
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 2, 0, 0, 1, 4]));
|
|
|
|
let expectedResponse = [];
|
|
push32(expectedResponse, 2); // Chosen subtype.
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
|
|
it('should respect server preference order', function () {
|
|
// VeNCrypt version
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
|
|
// Server ACK.
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
// Subtype list
|
|
let subtypes = [ 6 ];
|
|
push32(subtypes, 79);
|
|
push32(subtypes, 30);
|
|
push32(subtypes, 188);
|
|
push32(subtypes, 256);
|
|
push32(subtypes, 6);
|
|
push32(subtypes, 1);
|
|
client._sock._websocket._receiveData(new Uint8Array(subtypes));
|
|
|
|
let expectedResponse = [];
|
|
push32(expectedResponse, 30); // Chosen subtype.
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
|
|
it('should ignore redundant VeNCrypt subtype', function () {
|
|
// VeNCrypt version
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
|
|
// Server ACK.
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
// Subtype list
|
|
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 19, 0, 0, 0, 2]));
|
|
|
|
let expectedResponse = [];
|
|
push32(expectedResponse, 2); // Chosen subtype.
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
});
|
|
|
|
describe('Plain authentication (type 256) handler', function () {
|
|
beforeEach(function () {
|
|
sendSecurity(19, client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([19]));
|
|
// VeNCrypt version
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
|
|
// Server ACK.
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
});
|
|
|
|
it('should support Plain authentication', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'username', password: 'password' });
|
|
});
|
|
client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));
|
|
|
|
clock.tick();
|
|
|
|
const expectedResponse = [];
|
|
push32(expectedResponse, 8);
|
|
push32(expectedResponse, 8);
|
|
pushString(expectedResponse, 'username');
|
|
pushString(expectedResponse, 'password');
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
|
|
it('should support Plain authentication with an empty password', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'username', password: '' });
|
|
});
|
|
client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));
|
|
|
|
clock.tick();
|
|
|
|
const expectedResponse = [];
|
|
push32(expectedResponse, 8);
|
|
push32(expectedResponse, 0);
|
|
pushString(expectedResponse, 'username');
|
|
pushString(expectedResponse, '');
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
|
|
it('should support Plain authentication with a very long username and password', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ username: 'a'.repeat(300), password: 'b'.repeat(300) });
|
|
});
|
|
client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 1, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 1, 0]));
|
|
|
|
clock.tick();
|
|
|
|
const expectedResponse = [];
|
|
push32(expectedResponse, 300);
|
|
push32(expectedResponse, 300);
|
|
pushString(expectedResponse, 'a'.repeat(300));
|
|
pushString(expectedResponse, 'b'.repeat(300));
|
|
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Legacy SecurityResult', function () {
|
|
it('should not include reason in securityfailure event for versions < 3.7', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ password: 'passwd' });
|
|
});
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
sendVer('003.006\n', client);
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receiveData(new Uint8Array(challenge));
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.status).to.equal(2);
|
|
expect('reason' in spy.args[0][0].detail).to.be.false;
|
|
});
|
|
|
|
it('should not include reason in securityfailure event for versions < 3.8', function () {
|
|
client.addEventListener("credentialsrequired", () => {
|
|
client.sendCredentials({ password: 'passwd' });
|
|
});
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
sendVer('003.007\n', client);
|
|
sendSecurity(2, client);
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receiveData(new Uint8Array(challenge));
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.status).to.equal(2);
|
|
expect('reason' in spy.args[0][0].detail).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('SecurityResult', function () {
|
|
beforeEach(function () {
|
|
sendVer('003.008\n', client);
|
|
client._sock._websocket._getSentData();
|
|
sendSecurity(1, client);
|
|
client._sock._websocket._getSentData();
|
|
});
|
|
|
|
it('should fall through to ServerInitialisation on a response code of 0', function () {
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfbInitState).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should include reason when provided in securityfailure event', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
const failureData = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,
|
|
32, 102, 97, 105, 108, 117, 114, 101];
|
|
client._sock._websocket._receiveData(new Uint8Array(failureData));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.status).to.equal(1);
|
|
expect(spy.args[0][0].detail.reason).to.equal('such failure');
|
|
});
|
|
|
|
it('should not include reason when length is zero in securityfailure event', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
const failureData = [0, 0, 0, 1, 0, 0, 0, 0];
|
|
client._sock._websocket._receiveData(new Uint8Array(failureData));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.status).to.equal(1);
|
|
expect('reason' in spy.args[0][0].detail).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('ClientInitialisation', function () {
|
|
it('should transition to the ServerInitialisation state', function () {
|
|
const client = makeRFB();
|
|
client._rfbConnectionState = 'connecting';
|
|
client._rfbInitState = 'SecurityResult';
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfbInitState).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should send 1 if we are in shared mode', function () {
|
|
const client = makeRFB('wss://host:8675', { shared: true });
|
|
client._rfbConnectionState = 'connecting';
|
|
client._rfbInitState = 'SecurityResult';
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([1]));
|
|
});
|
|
|
|
it('should send 0 if we are not in shared mode', function () {
|
|
const client = makeRFB('wss://host:8675', { shared: false });
|
|
client._rfbConnectionState = 'connecting';
|
|
client._rfbInitState = 'SecurityResult';
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0]));
|
|
});
|
|
});
|
|
|
|
describe('ServerInitialisation', function () {
|
|
beforeEach(function () {
|
|
client._rfbInitState = 'ServerInitialisation';
|
|
});
|
|
|
|
function sendServerInit(opts, client) {
|
|
const fullOpts = { width: 10, height: 12, bpp: 24, depth: 24, bigEndian: 0,
|
|
trueColor: 1, redMax: 255, greenMax: 255, blueMax: 255,
|
|
redShift: 16, greenShift: 8, blueShift: 0, name: 'a name' };
|
|
for (let opt in opts) {
|
|
fullOpts[opt] = opts[opt];
|
|
}
|
|
const data = [];
|
|
|
|
push16(data, fullOpts.width);
|
|
push16(data, fullOpts.height);
|
|
|
|
data.push(fullOpts.bpp);
|
|
data.push(fullOpts.depth);
|
|
data.push(fullOpts.bigEndian);
|
|
data.push(fullOpts.trueColor);
|
|
|
|
push16(data, fullOpts.redMax);
|
|
push16(data, fullOpts.greenMax);
|
|
push16(data, fullOpts.blueMax);
|
|
push8(data, fullOpts.redShift);
|
|
push8(data, fullOpts.greenShift);
|
|
push8(data, fullOpts.blueShift);
|
|
|
|
// padding
|
|
push8(data, 0);
|
|
push8(data, 0);
|
|
push8(data, 0);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
|
|
const nameData = [];
|
|
let nameLen = [];
|
|
pushString(nameData, fullOpts.name);
|
|
push32(nameLen, nameData.length);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(nameLen));
|
|
client._sock._websocket._receiveData(new Uint8Array(nameData));
|
|
}
|
|
|
|
it('should set the framebuffer width and height', function () {
|
|
sendServerInit({ width: 32, height: 84 }, client);
|
|
expect(client._fbWidth).to.equal(32);
|
|
expect(client._fbHeight).to.equal(84);
|
|
});
|
|
|
|
// NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them
|
|
|
|
it('should set the framebuffer name and call the callback', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("desktopname", spy);
|
|
sendServerInit({ name: 'som€ nam€' }, client);
|
|
|
|
expect(client._fbName).to.equal('som€ nam€');
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.name).to.equal('som€ nam€');
|
|
});
|
|
|
|
it('should handle the extended init message of the tight encoding', function () {
|
|
// NB(sross): we don't actually do anything with it, so just test that we can
|
|
// read it w/o throwing an error
|
|
client._rfbTightVNC = true;
|
|
sendServerInit({}, client);
|
|
|
|
const tightData = [];
|
|
push16(tightData, 1);
|
|
push16(tightData, 2);
|
|
push16(tightData, 3);
|
|
push16(tightData, 0);
|
|
for (let i = 0; i < 16 + 32 + 48; i++) {
|
|
tightData.push(i);
|
|
}
|
|
client._sock._websocket._receiveData(new Uint8Array(tightData));
|
|
|
|
expect(client._rfbConnectionState).to.equal('connected');
|
|
});
|
|
|
|
it('should resize the display', function () {
|
|
sinon.spy(client._display, 'resize');
|
|
sendServerInit({ width: 27, height: 32 }, client);
|
|
|
|
expect(client._display.resize).to.have.been.calledOnce;
|
|
expect(client._display.resize).to.have.been.calledWith(27, 32);
|
|
});
|
|
|
|
it('should grab the keyboard', function () {
|
|
sinon.spy(client._keyboard, 'grab');
|
|
sendServerInit({}, client);
|
|
expect(client._keyboard.grab).to.have.been.calledOnce;
|
|
});
|
|
|
|
describe('Initial update request', function () {
|
|
beforeEach(function () {
|
|
sinon.spy(RFB.messages, "pixelFormat");
|
|
sinon.spy(RFB.messages, "clientEncodings");
|
|
sinon.spy(RFB.messages, "fbUpdateRequest");
|
|
});
|
|
|
|
afterEach(function () {
|
|
RFB.messages.pixelFormat.restore();
|
|
RFB.messages.clientEncodings.restore();
|
|
RFB.messages.fbUpdateRequest.restore();
|
|
});
|
|
|
|
// TODO(directxman12): test the various options in this configuration matrix
|
|
it('should reply with the pixel format, client encodings, and initial update request', function () {
|
|
sendServerInit({ width: 27, height: 32 }, client);
|
|
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledOnce;
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true);
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings);
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight);
|
|
RFB.messages.clientEncodings.getCall(0).args[1].forEach((enc) => {
|
|
expect(enc).to.be.a('number');
|
|
expect(Number.isInteger(enc)).to.be.true;
|
|
});
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest);
|
|
expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce;
|
|
expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32);
|
|
});
|
|
|
|
it('should reply with restricted settings for Intel AMT servers', function () {
|
|
sendServerInit({ width: 27, height: 32, name: "Intel(r) AMT KVM"}, client);
|
|
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledOnce;
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true);
|
|
expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings);
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight);
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile);
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest);
|
|
expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce;
|
|
expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32);
|
|
});
|
|
});
|
|
|
|
it('should send the "connect" event', function () {
|
|
let spy = sinon.spy();
|
|
client.addEventListener('connect', spy);
|
|
sendServerInit({}, client);
|
|
expect(spy).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Protocol message processing after completing initialization', function () {
|
|
let client;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
client._fbName = 'some device';
|
|
client._fbWidth = 640;
|
|
client._fbHeight = 20;
|
|
});
|
|
|
|
describe('Framebuffer update handling', function () {
|
|
function sendFbuMsg(rectInfo, rectData, client, rectCnt) {
|
|
let data = [];
|
|
|
|
if (!rectCnt || rectCnt > -1) {
|
|
// header
|
|
data.push(0); // msg type
|
|
data.push(0); // padding
|
|
push16(data, rectCnt || rectData.length);
|
|
}
|
|
|
|
for (let i = 0; i < rectData.length; i++) {
|
|
if (rectInfo[i]) {
|
|
push16(data, rectInfo[i].x);
|
|
push16(data, rectInfo[i].y);
|
|
push16(data, rectInfo[i].width);
|
|
push16(data, rectInfo[i].height);
|
|
push32(data, rectInfo[i].encoding);
|
|
}
|
|
data = data.concat(rectData[i]);
|
|
}
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
}
|
|
|
|
it('should send an update request if there is sufficient data', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20);
|
|
let expected = ews._getSentData();
|
|
|
|
client._framebufferUpdate = () => true;
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send an update request if we need more data', function () {
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
|
|
it('should resume receiving an update if we previously did not have enough data', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.fbUpdateRequest(esock, true, 0, 0, 640, 20);
|
|
let expected = ews._getSentData();
|
|
|
|
// just enough to set FBU.rects
|
|
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 3]));
|
|
expect(client._sock._websocket._getSentData()).to.have.length(0);
|
|
|
|
client._framebufferUpdate = function () { this._sock.rQskipBytes(1); return true; }; // we magically have enough data
|
|
// 247 should *not* be used as the message type here
|
|
client._sock._websocket._receiveData(new Uint8Array([247]));
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send a request in continuous updates mode', function () {
|
|
client._enabledContinuousUpdates = true;
|
|
client._framebufferUpdate = () => true;
|
|
client._sock._websocket._receiveData(new Uint8Array([0]));
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
});
|
|
|
|
it('should fail on an unsupported encoding', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
const rectInfo = { x: 8, y: 11, width: 27, height: 32, encoding: 234 };
|
|
sendFbuMsg([rectInfo], [[]], client);
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
|
|
describe('Message encoding handlers', function () {
|
|
beforeEach(function () {
|
|
// a really small frame
|
|
client._fbWidth = 4;
|
|
client._fbHeight = 4;
|
|
client._fbDepth = 24;
|
|
client._display.resize(4, 4);
|
|
});
|
|
|
|
it('should handle the DesktopSize pseduo-encoding', function () {
|
|
sinon.spy(client._display, 'resize');
|
|
sendFbuMsg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client);
|
|
|
|
expect(client._fbWidth).to.equal(20);
|
|
expect(client._fbHeight).to.equal(50);
|
|
|
|
expect(client._display.resize).to.have.been.calledOnce;
|
|
expect(client._display.resize).to.have.been.calledWith(20, 50);
|
|
});
|
|
|
|
describe('the ExtendedDesktopSize pseudo-encoding handler', function () {
|
|
beforeEach(function () {
|
|
// a really small frame
|
|
client._fbWidth = 4;
|
|
client._fbHeight = 4;
|
|
client._display.resize(4, 4);
|
|
sinon.spy(client._display, 'resize');
|
|
});
|
|
|
|
function makeScreenData(nrOfScreens) {
|
|
const data = [];
|
|
push8(data, nrOfScreens); // number-of-screens
|
|
push8(data, 0); // padding
|
|
push16(data, 0); // padding
|
|
for (let i=0; i<nrOfScreens; i += 1) {
|
|
push32(data, 0); // id
|
|
push16(data, 0); // x-position
|
|
push16(data, 0); // y-position
|
|
push16(data, 20); // width
|
|
push16(data, 50); // height
|
|
push32(data, 0); // flags
|
|
}
|
|
return data;
|
|
}
|
|
|
|
it('should handle a resize requested by this client', function () {
|
|
const reasonForChange = 1; // requested by this client
|
|
const statusCode = 0; // No error
|
|
|
|
sendFbuMsg([{ x: reasonForChange, y: statusCode,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
makeScreenData(1), client);
|
|
|
|
expect(client._fbWidth).to.equal(20);
|
|
expect(client._fbHeight).to.equal(50);
|
|
|
|
expect(client._display.resize).to.have.been.calledOnce;
|
|
expect(client._display.resize).to.have.been.calledWith(20, 50);
|
|
});
|
|
|
|
it('should handle a resize requested by another client', function () {
|
|
const reasonForChange = 2; // requested by another client
|
|
const statusCode = 0; // No error
|
|
|
|
sendFbuMsg([{ x: reasonForChange, y: statusCode,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
makeScreenData(1), client);
|
|
|
|
expect(client._fbWidth).to.equal(20);
|
|
expect(client._fbHeight).to.equal(50);
|
|
|
|
expect(client._display.resize).to.have.been.calledOnce;
|
|
expect(client._display.resize).to.have.been.calledWith(20, 50);
|
|
});
|
|
|
|
it('should be able to recieve requests which contain data for multiple screens', function () {
|
|
const reasonForChange = 2; // requested by another client
|
|
const statusCode = 0; // No error
|
|
|
|
sendFbuMsg([{ x: reasonForChange, y: statusCode,
|
|
width: 60, height: 50, encoding: -308 }],
|
|
makeScreenData(3), client);
|
|
|
|
expect(client._fbWidth).to.equal(60);
|
|
expect(client._fbHeight).to.equal(50);
|
|
|
|
expect(client._display.resize).to.have.been.calledOnce;
|
|
expect(client._display.resize).to.have.been.calledWith(60, 50);
|
|
});
|
|
|
|
it('should not handle a failed request', function () {
|
|
const reasonForChange = 1; // requested by this client
|
|
const statusCode = 1; // Resize is administratively prohibited
|
|
|
|
sendFbuMsg([{ x: reasonForChange, y: statusCode,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
makeScreenData(1), client);
|
|
|
|
expect(client._fbWidth).to.equal(4);
|
|
expect(client._fbHeight).to.equal(4);
|
|
|
|
expect(client._display.resize).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('the Cursor pseudo-encoding handler', function () {
|
|
beforeEach(function () {
|
|
sinon.spy(client._cursor, 'change');
|
|
});
|
|
|
|
it('should handle a standard cursor', function () {
|
|
const info = { x: 5, y: 7,
|
|
width: 4, height: 4,
|
|
encoding: -239};
|
|
let rect = [];
|
|
let expected = [];
|
|
|
|
for (let i = 0;i < info.width*info.height;i++) {
|
|
push32(rect, 0x11223300);
|
|
}
|
|
push32(rect, 0xa0a0a0a0);
|
|
|
|
for (let i = 0;i < info.width*info.height/2;i++) {
|
|
push32(expected, 0x332211ff);
|
|
push32(expected, 0x33221100);
|
|
}
|
|
expected = new Uint8Array(expected);
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);
|
|
});
|
|
|
|
it('should handle an empty cursor', function () {
|
|
const info = { x: 0, y: 0,
|
|
width: 0, height: 0,
|
|
encoding: -239};
|
|
const rect = [];
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(new Uint8Array, 0, 0, 0, 0);
|
|
});
|
|
|
|
it('should handle a transparent cursor', function () {
|
|
const info = { x: 5, y: 7,
|
|
width: 4, height: 4,
|
|
encoding: -239};
|
|
let rect = [];
|
|
let expected = [];
|
|
|
|
for (let i = 0;i < info.width*info.height;i++) {
|
|
push32(rect, 0x11223300);
|
|
}
|
|
push32(rect, 0x00000000);
|
|
|
|
for (let i = 0;i < info.width*info.height;i++) {
|
|
push32(expected, 0x33221100);
|
|
}
|
|
expected = new Uint8Array(expected);
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);
|
|
});
|
|
|
|
describe('dot for empty cursor', function () {
|
|
beforeEach(function () {
|
|
client.showDotCursor = true;
|
|
// Was called when we enabled dot cursor
|
|
client._cursor.change.resetHistory();
|
|
});
|
|
|
|
it('should show a standard cursor', function () {
|
|
const info = { x: 5, y: 7,
|
|
width: 4, height: 4,
|
|
encoding: -239};
|
|
let rect = [];
|
|
let expected = [];
|
|
|
|
for (let i = 0;i < info.width*info.height;i++) {
|
|
push32(rect, 0x11223300);
|
|
}
|
|
push32(rect, 0xa0a0a0a0);
|
|
|
|
for (let i = 0;i < info.width*info.height/2;i++) {
|
|
push32(expected, 0x332211ff);
|
|
push32(expected, 0x33221100);
|
|
}
|
|
expected = new Uint8Array(expected);
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(expected, 5, 7, 4, 4);
|
|
});
|
|
|
|
it('should handle an empty cursor', function () {
|
|
const info = { x: 0, y: 0,
|
|
width: 0, height: 0,
|
|
encoding: -239};
|
|
const rect = [];
|
|
const dot = RFB.cursors.dot;
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(dot.rgbaPixels,
|
|
dot.hotx,
|
|
dot.hoty,
|
|
dot.w,
|
|
dot.h);
|
|
});
|
|
|
|
it('should handle a transparent cursor', function () {
|
|
const info = { x: 5, y: 7,
|
|
width: 4, height: 4,
|
|
encoding: -239};
|
|
let rect = [];
|
|
const dot = RFB.cursors.dot;
|
|
|
|
for (let i = 0;i < info.width*info.height;i++) {
|
|
push32(rect, 0x11223300);
|
|
}
|
|
push32(rect, 0x00000000);
|
|
|
|
sendFbuMsg([info], [rect], client);
|
|
|
|
expect(client._cursor.change).to.have.been.calledOnce;
|
|
expect(client._cursor.change).to.have.been.calledWith(dot.rgbaPixels,
|
|
dot.hotx,
|
|
dot.hoty,
|
|
dot.w,
|
|
dot.h);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('the VMware cursor pseudo-encoding handler', function () {
|
|
beforeEach(function () {
|
|
sinon.spy(client._cursor, 'change');
|
|
});
|
|
afterEach(function () {
|
|
client._cursor.change.resetHistory();
|
|
});
|
|
|
|
it('should handle the VMware cursor pseudo-encoding', function () {
|
|
let data = [0x00, 0x00, 0xff, 0,
|
|
0x00, 0xff, 0x00, 0,
|
|
0x00, 0xff, 0x00, 0,
|
|
0x00, 0x00, 0xff, 0];
|
|
let rect = [];
|
|
push8(rect, 0);
|
|
push8(rect, 0);
|
|
|
|
//AND-mask
|
|
for (let i = 0; i < data.length; i++) {
|
|
push8(rect, data[i]);
|
|
}
|
|
//XOR-mask
|
|
for (let i = 0; i < data.length; i++) {
|
|
push8(rect, data[i]);
|
|
}
|
|
|
|
sendFbuMsg([{ x: 0, y: 0, width: 2, height: 2,
|
|
encoding: 0x574d5664}],
|
|
[rect], client);
|
|
expect(client._FBU.rects).to.equal(0);
|
|
});
|
|
|
|
it('should handle insufficient cursor pixel data', function () {
|
|
|
|
// Specified 14x23 pixels for the cursor,
|
|
// but only send 2x2 pixels worth of data
|
|
let w = 14;
|
|
let h = 23;
|
|
let data = [0x00, 0x00, 0xff, 0,
|
|
0x00, 0xff, 0x00, 0];
|
|
let rect = [];
|
|
|
|
push8(rect, 0);
|
|
push8(rect, 0);
|
|
|
|
//AND-mask
|
|
for (let i = 0; i < data.length; i++) {
|
|
push8(rect, data[i]);
|
|
}
|
|
//XOR-mask
|
|
for (let i = 0; i < data.length; i++) {
|
|
push8(rect, data[i]);
|
|
}
|
|
|
|
sendFbuMsg([{ x: 0, y: 0, width: w, height: h,
|
|
encoding: 0x574d5664}],
|
|
[rect], client);
|
|
|
|
// expect one FBU to remain unhandled
|
|
expect(client._FBU.rects).to.equal(1);
|
|
});
|
|
|
|
it('should update the cursor when type is classic', function () {
|
|
let andMask =
|
|
[0xff, 0xff, 0xff, 0xff, //Transparent
|
|
0xff, 0xff, 0xff, 0xff, //Transparent
|
|
0x00, 0x00, 0x00, 0x00, //Opaque
|
|
0xff, 0xff, 0xff, 0xff]; //Inverted
|
|
|
|
let xorMask =
|
|
[0x00, 0x00, 0x00, 0x00, //Transparent
|
|
0x00, 0x00, 0x00, 0x00, //Transparent
|
|
0x11, 0x22, 0x33, 0x44, //Opaque
|
|
0xff, 0xff, 0xff, 0x44]; //Inverted
|
|
|
|
let rect = [];
|
|
push8(rect, 0); //cursor_type
|
|
push8(rect, 0); //padding
|
|
let hotx = 0;
|
|
let hoty = 0;
|
|
let w = 2;
|
|
let h = 2;
|
|
|
|
//AND-mask
|
|
for (let i = 0; i < andMask.length; i++) {
|
|
push8(rect, andMask[i]);
|
|
}
|
|
//XOR-mask
|
|
for (let i = 0; i < xorMask.length; i++) {
|
|
push8(rect, xorMask[i]);
|
|
}
|
|
|
|
let expectedRgba = [0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x33, 0x22, 0x11, 0xff,
|
|
0x00, 0x00, 0x00, 0xff];
|
|
|
|
sendFbuMsg([{ x: hotx, y: hoty,
|
|
width: w, height: h,
|
|
encoding: 0x574d5664}],
|
|
[rect], client);
|
|
|
|
expect(client._cursor.change)
|
|
.to.have.been.calledOnce;
|
|
expect(client._cursor.change)
|
|
.to.have.been.calledWith(expectedRgba,
|
|
hotx, hoty,
|
|
w, h);
|
|
});
|
|
|
|
it('should update the cursor when type is alpha', function () {
|
|
let data = [0xee, 0x55, 0xff, 0x00, // rgba
|
|
0x00, 0xff, 0x00, 0xff,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0x00, 0xff, 0xee];
|
|
let rect = [];
|
|
push8(rect, 1); //cursor_type
|
|
push8(rect, 0); //padding
|
|
let hotx = 0;
|
|
let hoty = 0;
|
|
let w = 3;
|
|
let h = 2;
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
push8(rect, data[i]);
|
|
}
|
|
|
|
let expectedRgba = [0xee, 0x55, 0xff, 0x00,
|
|
0x00, 0xff, 0x00, 0xff,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0x00, 0xff, 0xee];
|
|
|
|
sendFbuMsg([{ x: hotx, y: hoty,
|
|
width: w, height: h,
|
|
encoding: 0x574d5664}],
|
|
[rect], client);
|
|
|
|
expect(client._cursor.change)
|
|
.to.have.been.calledOnce;
|
|
expect(client._cursor.change)
|
|
.to.have.been.calledWith(expectedRgba,
|
|
hotx, hoty,
|
|
w, h);
|
|
});
|
|
|
|
it('should not update cursor when incorrect cursor type given', function () {
|
|
let rect = [];
|
|
push8(rect, 3); // invalid cursor type
|
|
push8(rect, 0); // padding
|
|
|
|
client._cursor.change.resetHistory();
|
|
sendFbuMsg([{ x: 0, y: 0, width: 2, height: 2,
|
|
encoding: 0x574d5664}],
|
|
[rect], client);
|
|
|
|
expect(client._cursor.change)
|
|
.to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
it('should handle the last_rect pseudo-encoding', function () {
|
|
sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100);
|
|
// Send a bell message and make sure it is parsed
|
|
let spy = sinon.spy();
|
|
client.addEventListener("bell", spy);
|
|
client._sock._websocket._receiveData(new Uint8Array([0x02]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should handle the DesktopName pseudo-encoding', function () {
|
|
let data = [];
|
|
push32(data, 13);
|
|
pushString(data, "som€ nam€");
|
|
|
|
const spy = sinon.spy();
|
|
client.addEventListener("desktopname", spy);
|
|
|
|
sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -307 }], [data], client);
|
|
|
|
expect(client._fbName).to.equal('som€ nam€');
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.name).to.equal('som€ nam€');
|
|
});
|
|
});
|
|
|
|
describe('Caps Lock and Num Lock remote fixup', function () {
|
|
function sendLedStateUpdate(state) {
|
|
let data = [];
|
|
push8(data, state);
|
|
sendFbuMsg([{ x: 0, y: 0, width: 0, height: 0, encoding: -261 }], [data], client);
|
|
}
|
|
|
|
let client;
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
sinon.stub(client, 'sendKey');
|
|
});
|
|
|
|
it('should toggle caps lock if remote caps lock is on and local is off', function () {
|
|
sendLedStateUpdate(0b100);
|
|
client._handleKeyEvent(0x61, 'KeyA', true, null, false);
|
|
|
|
expect(client.sendKey).to.have.been.calledThrice;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, "KeyA", true);
|
|
});
|
|
|
|
it('should toggle caps lock if remote caps lock is off and local is on', function () {
|
|
sendLedStateUpdate(0b011);
|
|
client._handleKeyEvent(0x41, 'KeyA', true, null, true);
|
|
|
|
expect(client.sendKey).to.have.been.calledThrice;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0x41, "KeyA", true);
|
|
});
|
|
|
|
it('should not toggle caps lock if remote caps lock is on and local is on', function () {
|
|
sendLedStateUpdate(0b100);
|
|
client._handleKeyEvent(0x41, 'KeyA', true, null, true);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0x41, "KeyA", true);
|
|
});
|
|
|
|
it('should not toggle caps lock if remote caps lock is off and local is off', function () {
|
|
sendLedStateUpdate(0b011);
|
|
client._handleKeyEvent(0x61, 'KeyA', true, null, false);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0x61, "KeyA", true);
|
|
});
|
|
|
|
it('should not toggle caps lock if the key is caps lock', function () {
|
|
sendLedStateUpdate(0b011);
|
|
client._handleKeyEvent(0xFFE5, 'CapsLock', true, null, true);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
|
|
});
|
|
|
|
it('should toggle caps lock only once', function () {
|
|
sendLedStateUpdate(0b100);
|
|
client._handleKeyEvent(0x61, 'KeyA', true, null, false);
|
|
client._handleKeyEvent(0x61, 'KeyA', true, null, false);
|
|
|
|
expect(client.sendKey).to.have.callCount(4);
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0x61, "KeyA", true);
|
|
expect(client.sendKey.lastCall).to.have.been.calledWith(0x61, "KeyA", true);
|
|
});
|
|
|
|
it('should retain remote caps lock state on capslock key up', function () {
|
|
sendLedStateUpdate(0b100);
|
|
client._handleKeyEvent(0xFFE5, 'CapsLock', false, null, true);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFE5, "CapsLock", false);
|
|
expect(client._remoteCapsLock).to.equal(true);
|
|
});
|
|
|
|
it('should toggle num lock if remote num lock is on and local is off', function () {
|
|
sendLedStateUpdate(0b010);
|
|
client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledThrice;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, "NumPad1", true);
|
|
});
|
|
|
|
it('should toggle num lock if remote num lock is off and local is on', function () {
|
|
sendLedStateUpdate(0b101);
|
|
client._handleKeyEvent(0xFFB1, 'NumPad1', true, true, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledThrice;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFFB1, "NumPad1", true);
|
|
});
|
|
|
|
it('should not toggle num lock if remote num lock is on and local is on', function () {
|
|
sendLedStateUpdate(0b010);
|
|
client._handleKeyEvent(0xFFB1, 'NumPad1', true, true, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, "NumPad1", true);
|
|
});
|
|
|
|
it('should not toggle num lock if remote num lock is off and local is off', function () {
|
|
sendLedStateUpdate(0b101);
|
|
client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF9C, "NumPad1", true);
|
|
});
|
|
|
|
it('should not toggle num lock if the key is num lock', function () {
|
|
sendLedStateUpdate(0b101);
|
|
client._handleKeyEvent(0xFF7F, 'NumLock', true, true, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true);
|
|
});
|
|
|
|
it('should not toggle num lock if local state is unknown', function () {
|
|
sendLedStateUpdate(0b010);
|
|
client._handleKeyEvent(0xFFB1, 'NumPad1', true, null, null);
|
|
|
|
expect(client.sendKey).to.have.been.calledOnce;
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFFB1, "NumPad1", true);
|
|
});
|
|
|
|
it('should toggle num lock only once', function () {
|
|
sendLedStateUpdate(0b010);
|
|
client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);
|
|
client._handleKeyEvent(0xFF9C, 'NumPad1', true, false, null);
|
|
|
|
expect(client.sendKey).to.have.callCount(4);
|
|
expect(client.sendKey.firstCall).to.have.been.calledWith(0xFF7F, "NumLock", true);
|
|
expect(client.sendKey.secondCall).to.have.been.calledWith(0xFF7F, "NumLock", false);
|
|
expect(client.sendKey.thirdCall).to.have.been.calledWith(0xFF9C, "NumPad1", true);
|
|
expect(client.sendKey.lastCall).to.have.been.calledWith(0xFF9C, "NumPad1", true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('XVP message handling', function () {
|
|
it('should set the XVP version and fire the callback with the version on XVP_INIT', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("capabilities", spy);
|
|
client._sock._websocket._receiveData(new Uint8Array([250, 0, 10, 1]));
|
|
expect(client._rfbXvpVer).to.equal(10);
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.capabilities.power).to.be.true;
|
|
expect(client.capabilities.power).to.be.true;
|
|
});
|
|
|
|
it('should fail on unknown XVP message types', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([250, 0, 10, 237]));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('Normal clipboard handling receive', function () {
|
|
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
|
|
const expectedStr = 'cheese!';
|
|
const data = [3, 0, 0, 0];
|
|
push32(data, expectedStr.length);
|
|
for (let i = 0; i < expectedStr.length; i++) { data.push(expectedStr.charCodeAt(i)); }
|
|
const spy = sinon.spy();
|
|
client.addEventListener("clipboard", spy);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.text).to.equal(expectedStr);
|
|
});
|
|
});
|
|
|
|
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._receiveData(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._receiveData(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];
|
|
|
|
let text = encodeUTF8("Aå漢字!");
|
|
let deflatedText = deflateWithSize(text);
|
|
|
|
// How much data we are sending.
|
|
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
|
|
|
|
data = data.concat(flags);
|
|
data = data.concat(Array.from(deflatedText));
|
|
|
|
const spy = sinon.spy();
|
|
client.addEventListener("clipboard", spy);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
|
client.removeEventListener("clipboard", spy);
|
|
});
|
|
|
|
it('should update clipboard with correct escape characters from a Provide message ', function () {
|
|
let expectedData = "Oh\nmy\n!";
|
|
let data = [3, 0, 0, 0];
|
|
const flags = [0x10, 0x00, 0x00, 0x01];
|
|
|
|
let text = encodeUTF8("Oh\r\nmy\r\n!\0");
|
|
|
|
let deflatedText = deflateWithSize(text);
|
|
|
|
// How much data we are sending.
|
|
push32(data, toUnsigned32bit(-(4 + deflatedText.length)));
|
|
|
|
data = data.concat(flags);
|
|
data = data.concat(Array.from(deflatedText));
|
|
|
|
const spy = sinon.spy();
|
|
client.addEventListener("clipboard", spy);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
|
client.removeEventListener("clipboard", spy);
|
|
});
|
|
|
|
it('should be able to handle large Provide messages', function () {
|
|
let expectedData = "hello".repeat(100000);
|
|
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);
|
|
data = data.concat(Array.from(deflatedText));
|
|
|
|
const spy = sinon.spy();
|
|
client.addEventListener("clipboard", spy);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array(data));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.text).to.equal(expectedData);
|
|
client.removeEventListener("clipboard", spy);
|
|
});
|
|
|
|
});
|
|
|
|
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._receiveData(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._receiveData(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._receiveData(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._receiveData(new Uint8Array(data));
|
|
|
|
expect(RFB.messages.extendedClipboardProvide).to.have.been.calledOnce;
|
|
expect(RFB.messages.extendedClipboardProvide).to.have.been.calledWith(client._sock, expectedData, ["HejHej"]);
|
|
});
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
it('should fire the bell callback on Bell', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("bell", spy);
|
|
client._sock._websocket._receiveData(new Uint8Array([2]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should respond correctly to ServerFence', function () {
|
|
const payload = "foo\x00ab9";
|
|
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
|
|
// ClientFence and ServerFence are identical in structure
|
|
RFB.messages.clientFence(esock, (1<<0) | (1<<1), payload);
|
|
let expected = ews._getSentData();
|
|
RFB.messages.clientFence(esock, 0xffffffff, payload);
|
|
let incoming = ews._getSentData();
|
|
|
|
client._sock._websocket._receiveData(incoming);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
|
|
RFB.messages.clientFence(esock, (1<<0), payload);
|
|
expected = ews._getSentData();
|
|
RFB.messages.clientFence(esock, (1<<0) | (1<<31), payload);
|
|
incoming = ews._getSentData();
|
|
|
|
client._sock._websocket._receiveData(incoming);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should enable continuous updates on first EndOfContinousUpdates', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 640, 20);
|
|
let expected = ews._getSentData();
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.false;
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([150]));
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.true;
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should disable continuous updates on subsequent EndOfContinousUpdates', function () {
|
|
client._enabledContinuousUpdates = true;
|
|
client._supportsContinuousUpdates = true;
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([150]));
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.false;
|
|
});
|
|
|
|
it('should update continuous updates on resize', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.enableContinuousUpdates(esock, true, 0, 0, 90, 700);
|
|
let expected = ews._getSentData();
|
|
|
|
client._resize(450, 160);
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array([]));
|
|
|
|
client._enabledContinuousUpdates = true;
|
|
|
|
client._resize(90, 700);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should fail on an unknown message type', function () {
|
|
let callback = sinon.spy();
|
|
client.addEventListener("disconnect", callback);
|
|
|
|
client._sock._websocket._receiveData(new Uint8Array([87]));
|
|
|
|
expect(callback).to.have.been.calledOnce;
|
|
expect(callback.args[0][0].detail.clean).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('Asynchronous events', function () {
|
|
let client;
|
|
let pointerEvent;
|
|
let keyEvent;
|
|
let qemuKeyEvent;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
client._display.resize(100, 100);
|
|
|
|
// We need to disable this as focusing the canvas will
|
|
// cause the browser to scoll to it, messing up our
|
|
// client coordinate calculations
|
|
client.focusOnClick = false;
|
|
|
|
pointerEvent = sinon.spy(RFB.messages, 'pointerEvent');
|
|
keyEvent = sinon.spy(RFB.messages, 'keyEvent');
|
|
qemuKeyEvent = sinon.spy(RFB.messages, 'QEMUExtendedKeyEvent');
|
|
});
|
|
|
|
afterEach(function () {
|
|
pointerEvent.restore();
|
|
keyEvent.restore();
|
|
qemuKeyEvent.restore();
|
|
});
|
|
|
|
function elementToClient(x, y) {
|
|
let res = { x: 0, y: 0 };
|
|
|
|
let bounds = client._canvas.getBoundingClientRect();
|
|
|
|
/*
|
|
* If the canvas is on a fractional position we will calculate
|
|
* a fractional mouse position. But that gets truncated when we
|
|
* send the event, AND the same thing happens in RFB when it
|
|
* generates the PointerEvent message. To compensate for that
|
|
* fact we round the value upwards here.
|
|
*/
|
|
res.x = Math.ceil(bounds.left + x);
|
|
res.y = Math.ceil(bounds.top + y);
|
|
|
|
return res;
|
|
}
|
|
|
|
describe('Mouse events', function () {
|
|
function sendMouseMoveEvent(x, y) {
|
|
let pos = elementToClient(x, y);
|
|
let ev;
|
|
|
|
ev = new MouseEvent('mousemove',
|
|
{ 'screenX': pos.x + window.screenX,
|
|
'screenY': pos.y + window.screenY,
|
|
'clientX': pos.x,
|
|
'clientY': pos.y });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
function sendMouseButtonEvent(x, y, down, button) {
|
|
let pos = elementToClient(x, y);
|
|
let ev;
|
|
|
|
ev = new MouseEvent(down ? 'mousedown' : 'mouseup',
|
|
{ 'screenX': pos.x + window.screenX,
|
|
'screenY': pos.y + window.screenY,
|
|
'clientX': pos.x,
|
|
'clientY': pos.y,
|
|
'button': button,
|
|
'buttons': 1 << button });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
it('should not send button messages in view-only mode', function () {
|
|
client._viewOnly = true;
|
|
sendMouseButtonEvent(10, 10, true, 0);
|
|
clock.tick(50);
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not send movement messages in view-only mode', function () {
|
|
client._viewOnly = true;
|
|
sendMouseMoveEvent(10, 10);
|
|
clock.tick(50);
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle left mouse button', function () {
|
|
sendMouseButtonEvent(10, 10, true, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x1);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseButtonEvent(10, 10, false, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x0);
|
|
});
|
|
|
|
it('should handle middle mouse button', function () {
|
|
sendMouseButtonEvent(10, 10, true, 1);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x2);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseButtonEvent(10, 10, false, 1);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x0);
|
|
});
|
|
|
|
it('should handle right mouse button', function () {
|
|
sendMouseButtonEvent(10, 10, true, 2);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x4);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseButtonEvent(10, 10, false, 2);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 10, 0x0);
|
|
});
|
|
|
|
it('should handle multiple mouse buttons', function () {
|
|
sendMouseButtonEvent(10, 10, true, 0);
|
|
sendMouseButtonEvent(10, 10, true, 2);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0x1);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0x5);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseButtonEvent(10, 10, false, 0);
|
|
sendMouseButtonEvent(10, 10, false, 2);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0x4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0x0);
|
|
});
|
|
|
|
it('should handle mouse movement', function () {
|
|
sendMouseMoveEvent(50, 70);
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
50, 70, 0x0);
|
|
});
|
|
|
|
it('should handle click and drag', function () {
|
|
sendMouseButtonEvent(10, 10, true, 0);
|
|
sendMouseMoveEvent(50, 70);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0x1);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
50, 70, 0x1);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseButtonEvent(50, 70, false, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
50, 70, 0x0);
|
|
});
|
|
|
|
describe('Event aggregation', function () {
|
|
it('should send a single pointer event on mouse movement', function () {
|
|
sendMouseMoveEvent(50, 70);
|
|
clock.tick(100);
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
50, 70, 0x0);
|
|
});
|
|
|
|
it('should delay one move if two events are too close', function () {
|
|
sendMouseMoveEvent(18, 30);
|
|
sendMouseMoveEvent(20, 50);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
18, 30, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
clock.tick(100);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 50, 0x0);
|
|
});
|
|
|
|
it('should only send first and last move of many close events', function () {
|
|
sendMouseMoveEvent(18, 30);
|
|
sendMouseMoveEvent(20, 50);
|
|
sendMouseMoveEvent(21, 55);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
18, 30, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
clock.tick(100);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
21, 55, 0x0);
|
|
});
|
|
|
|
// We selected the 17ms since that is ~60 FPS
|
|
it('should send move events every 17 ms', function () {
|
|
sendMouseMoveEvent(1, 10); // instant send
|
|
clock.tick(10);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
1, 10, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseMoveEvent(2, 20); // delayed
|
|
clock.tick(10); // timeout send
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
2, 20, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseMoveEvent(3, 30); // delayed
|
|
clock.tick(10);
|
|
sendMouseMoveEvent(4, 40); // delayed
|
|
clock.tick(10); // timeout send
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
4, 40, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseMoveEvent(5, 50); // delayed
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send waiting move events before a button press', function () {
|
|
sendMouseMoveEvent(13, 9);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
13, 9, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseMoveEvent(20, 70);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
|
|
sendMouseButtonEvent(20, 70, true, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 70, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 70, 0x1);
|
|
});
|
|
|
|
it('should send move events with enough time apart normally', function () {
|
|
sendMouseMoveEvent(58, 60);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
58, 60, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
clock.tick(20);
|
|
|
|
sendMouseMoveEvent(25, 60);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
25, 60, 0x0);
|
|
pointerEvent.resetHistory();
|
|
});
|
|
|
|
it('should not send waiting move events if disconnected', function () {
|
|
sendMouseMoveEvent(88, 99);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
88, 99, 0x0);
|
|
pointerEvent.resetHistory();
|
|
|
|
sendMouseMoveEvent(66, 77);
|
|
client.disconnect();
|
|
clock.tick(20);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
it.skip('should block click events', function () {
|
|
/* FIXME */
|
|
});
|
|
|
|
it.skip('should block contextmenu events', function () {
|
|
/* FIXME */
|
|
});
|
|
});
|
|
|
|
describe('Wheel events', function () {
|
|
function sendWheelEvent(x, y, dx, dy, mode=0) {
|
|
let pos = elementToClient(x, y);
|
|
let ev;
|
|
|
|
ev = new WheelEvent('wheel',
|
|
{ 'screenX': pos.x + window.screenX,
|
|
'screenY': pos.y + window.screenY,
|
|
'clientX': pos.x,
|
|
'clientY': pos.y,
|
|
'deltaX': dx,
|
|
'deltaY': dy,
|
|
'deltaMode': mode });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
it('should handle wheel up event', function () {
|
|
sendWheelEvent(10, 10, 0, -50);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<3);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should handle wheel down event', function () {
|
|
sendWheelEvent(10, 10, 0, 50);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should handle wheel left event', function () {
|
|
sendWheelEvent(10, 10, -50, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<5);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should handle wheel right event', function () {
|
|
sendWheelEvent(10, 10, 50, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<6);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should ignore wheel when in view only', function () {
|
|
client._viewOnly = true;
|
|
|
|
sendWheelEvent(10, 10, 50, 0);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should accumulate wheel events if small enough', function () {
|
|
sendWheelEvent(10, 10, 0, 20);
|
|
sendWheelEvent(10, 10, 0, 20);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
|
|
sendWheelEvent(10, 10, 0, 20);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should not accumulate large wheel events', function () {
|
|
sendWheelEvent(10, 10, 0, 400);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should handle line based wheel event', function () {
|
|
sendWheelEvent(10, 10, 0, 3, 1);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
|
|
it('should handle page based wheel event', function () {
|
|
sendWheelEvent(10, 10, 0, 3, 2);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 1<<4);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 10, 0);
|
|
});
|
|
});
|
|
|
|
describe('Keyboard events', function () {
|
|
it('should send a key message on a key press', function () {
|
|
let esock = new Websock();
|
|
let ews = new FakeWebSocket();
|
|
ews._open();
|
|
esock.attach(ews);
|
|
RFB.messages.keyEvent(esock, 0x41, 1);
|
|
let expected = ews._getSentData();
|
|
|
|
client._handleKeyEvent(0x41, 'KeyA', true);
|
|
|
|
expect(client._sock).to.have.sent(expected);
|
|
});
|
|
|
|
it('should not send messages in view-only mode', function () {
|
|
client._viewOnly = true;
|
|
sinon.spy(client._sock, 'flush');
|
|
client._handleKeyEvent('a', 'KeyA', true);
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('Gesture event handlers', function () {
|
|
function gestureStart(gestureType, x, y,
|
|
magnitudeX = 0, magnitudeY = 0) {
|
|
let pos = elementToClient(x, y);
|
|
let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
|
|
|
|
detail.magnitudeX = magnitudeX;
|
|
detail.magnitudeY = magnitudeY;
|
|
|
|
let ev = new CustomEvent('gesturestart', { detail: detail });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
function gestureMove(gestureType, x, y,
|
|
magnitudeX = 0, magnitudeY = 0) {
|
|
let pos = elementToClient(x, y);
|
|
let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
|
|
|
|
detail.magnitudeX = magnitudeX;
|
|
detail.magnitudeY = magnitudeY;
|
|
|
|
let ev = new CustomEvent('gesturemove', { detail: detail });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
function gestureEnd(gestureType, x, y) {
|
|
let pos = elementToClient(x, y);
|
|
let detail = {type: gestureType, clientX: pos.x, clientY: pos.y};
|
|
let ev = new CustomEvent('gestureend', { detail: detail });
|
|
client._canvas.dispatchEvent(ev);
|
|
}
|
|
|
|
describe('Gesture onetap', function () {
|
|
it('should handle onetap events', function () {
|
|
let bmask = 0x1;
|
|
|
|
gestureStart('onetap', 20, 40);
|
|
gestureEnd('onetap', 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should keep same position for multiple onetap events', function () {
|
|
let bmask = 0x1;
|
|
|
|
gestureStart('onetap', 20, 40);
|
|
gestureEnd('onetap', 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureStart('onetap', 20, 50);
|
|
gestureEnd('onetap', 20, 50);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureStart('onetap', 30, 50);
|
|
gestureEnd('onetap', 30, 50);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should not keep same position for onetap events when too far apart', function () {
|
|
let bmask = 0x1;
|
|
|
|
gestureStart('onetap', 20, 40);
|
|
gestureEnd('onetap', 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureStart('onetap', 80, 95);
|
|
gestureEnd('onetap', 80, 95);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
80, 95, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
80, 95, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
80, 95, 0x0);
|
|
});
|
|
|
|
it('should not keep same position for onetap events when enough time inbetween', function () {
|
|
let bmask = 0x1;
|
|
|
|
gestureStart('onetap', 10, 20);
|
|
gestureEnd('onetap', 10, 20);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 20, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 20, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
10, 20, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
this.clock.tick(1500);
|
|
|
|
gestureStart('onetap', 15, 20);
|
|
gestureEnd('onetap', 15, 20);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
15, 20, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
15, 20, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
15, 20, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
});
|
|
});
|
|
|
|
describe('Gesture twotap', function () {
|
|
it('should handle gesture twotap events', function () {
|
|
let bmask = 0x4;
|
|
|
|
gestureStart("twotap", 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should keep same position for multiple twotap events', function () {
|
|
let bmask = 0x4;
|
|
|
|
for (let offset = 0;offset < 30;offset += 10) {
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureStart('twotap', 20, 40 + offset);
|
|
gestureEnd('twotap', 20, 40 + offset);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Gesture threetap', function () {
|
|
it('should handle gesture start for threetap events', function () {
|
|
let bmask = 0x2;
|
|
|
|
gestureStart("threetap", 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should keep same position for multiple threetap events', function () {
|
|
let bmask = 0x2;
|
|
|
|
for (let offset = 0;offset < 30;offset += 10) {
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureStart('threetap', 20, 40 + offset);
|
|
gestureEnd('threetap', 20, 40 + offset);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Gesture drag', function () {
|
|
it('should handle gesture drag events', function () {
|
|
let bmask = 0x1;
|
|
|
|
gestureStart('drag', 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('drag', 30, 50);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnce;
|
|
expect(pointerEvent).to.have.been.calledWith(client._sock,
|
|
30, 50, bmask);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureEnd('drag', 30, 50);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
30, 50, bmask);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
30, 50, 0x0);
|
|
});
|
|
});
|
|
|
|
describe('Gesture long press', function () {
|
|
it('should handle long press events', function () {
|
|
let bmask = 0x4;
|
|
|
|
gestureStart('longpress', 20, 40);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('longpress', 40, 60);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
40, 60, bmask);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureEnd('longpress', 40, 60);
|
|
|
|
expect(pointerEvent).to.have.been.calledTwice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
40, 60, bmask);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
40, 60, 0x0);
|
|
});
|
|
});
|
|
|
|
describe('Gesture twodrag', function () {
|
|
it('should handle gesture twodrag up events', function () {
|
|
let bmask = 0x10; // Button mask for scroll down
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 0, -60);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle gesture twodrag down events', function () {
|
|
let bmask = 0x8; // Button mask for scroll up
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 0, 60);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle gesture twodrag right events', function () {
|
|
let bmask = 0x20; // Button mask for scroll right
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 60, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle gesture twodrag left events', function () {
|
|
let bmask = 0x40; // Button mask for scroll left
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, -60, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle gesture twodrag diag events', function () {
|
|
let scrlUp = 0x8; // Button mask for scroll up
|
|
let scrlRight = 0x20; // Button mask for scroll right
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 60, 60);
|
|
|
|
expect(pointerEvent).to.have.been.callCount(5);
|
|
expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
|
|
20, 40, scrlUp);
|
|
expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
|
|
20, 40, scrlRight);
|
|
expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle multiple small gesture twodrag events', function () {
|
|
let bmask = 0x8; // Button mask for scroll up
|
|
|
|
gestureStart('twodrag', 20, 40, 0, 0);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 0, 10);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 0, 20);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 20, 40, 0, 60);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
});
|
|
|
|
it('should handle large gesture twodrag events', function () {
|
|
let bmask = 0x8; // Button mask for scroll up
|
|
|
|
gestureStart('twodrag', 30, 50, 0, 0);
|
|
|
|
expect(pointerEvent).
|
|
to.have.been.calledOnceWith(client._sock, 30, 50, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('twodrag', 30, 50, 0, 200);
|
|
|
|
expect(pointerEvent).to.have.callCount(7);
|
|
expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
|
|
30, 50, 0x0);
|
|
expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
|
|
30, 50, bmask);
|
|
expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
|
|
30, 50, 0x0);
|
|
expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
|
|
30, 50, bmask);
|
|
expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
|
|
30, 50, 0x0);
|
|
expect(pointerEvent.getCall(5)).to.have.been.calledWith(client._sock,
|
|
30, 50, bmask);
|
|
expect(pointerEvent.getCall(6)).to.have.been.calledWith(client._sock,
|
|
30, 50, 0x0);
|
|
});
|
|
});
|
|
|
|
describe('Gesture pinch', function () {
|
|
it('should handle gesture pinch in events', function () {
|
|
let keysym = KeyTable.XK_Control_L;
|
|
let bmask = 0x10; // Button mask for scroll down
|
|
|
|
gestureStart('pinch', 20, 40, 90, 90);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(keyEvent).to.not.have.been.called;
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 30, 30);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
expect(keyEvent).to.have.been.calledTwice;
|
|
expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
keysym, 1);
|
|
expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
keysym, 0);
|
|
|
|
expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
|
|
expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
|
|
|
|
pointerEvent.resetHistory();
|
|
keyEvent.resetHistory();
|
|
|
|
gestureEnd('pinch', 20, 40);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
expect(keyEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle gesture pinch out events', function () {
|
|
let keysym = KeyTable.XK_Control_L;
|
|
let bmask = 0x8; // Button mask for scroll up
|
|
|
|
gestureStart('pinch', 10, 20, 10, 20);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
10, 20, 0x0);
|
|
expect(keyEvent).to.not.have.been.called;
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 10, 20, 70, 80);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
10, 20, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
10, 20, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
10, 20, 0x0);
|
|
|
|
expect(keyEvent).to.have.been.calledTwice;
|
|
expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
keysym, 1);
|
|
expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
keysym, 0);
|
|
|
|
expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
|
|
expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
|
|
|
|
pointerEvent.resetHistory();
|
|
keyEvent.resetHistory();
|
|
|
|
gestureEnd('pinch', 10, 20);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
expect(keyEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle large gesture pinch', function () {
|
|
let keysym = KeyTable.XK_Control_L;
|
|
let bmask = 0x10; // Button mask for scroll down
|
|
|
|
gestureStart('pinch', 20, 40, 150, 150);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(keyEvent).to.not.have.been.called;
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 30, 30);
|
|
|
|
expect(pointerEvent).to.have.been.callCount(5);
|
|
expect(pointerEvent.getCall(0)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.getCall(1)).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.getCall(2)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.getCall(3)).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.getCall(4)).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
expect(keyEvent).to.have.been.calledTwice;
|
|
expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
keysym, 1);
|
|
expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
keysym, 0);
|
|
|
|
expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
|
|
expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
|
|
|
|
pointerEvent.resetHistory();
|
|
keyEvent.resetHistory();
|
|
|
|
gestureEnd('pinch', 20, 40);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
expect(keyEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle multiple small gesture pinch out events', function () {
|
|
let keysym = KeyTable.XK_Control_L;
|
|
let bmask = 0x8; // Button mask for scroll down
|
|
|
|
gestureStart('pinch', 20, 40, 0, 10);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(keyEvent).to.not.have.been.called;
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 0, 30);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 0, 60);
|
|
clock.tick(50);
|
|
|
|
expect(pointerEvent).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
pointerEvent.resetHistory();
|
|
keyEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 0, 90);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
expect(keyEvent).to.have.been.calledTwice;
|
|
expect(keyEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
keysym, 1);
|
|
expect(keyEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
keysym, 0);
|
|
|
|
expect(keyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
|
|
expect(keyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
|
|
|
|
pointerEvent.resetHistory();
|
|
keyEvent.resetHistory();
|
|
|
|
gestureEnd('pinch', 20, 40);
|
|
|
|
expect(keyEvent).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send correct key control code', function () {
|
|
let keysym = KeyTable.XK_Control_L;
|
|
let code = 0x1d;
|
|
let bmask = 0x10; // Button mask for scroll down
|
|
|
|
client._qemuExtKeyEventSupported = true;
|
|
|
|
gestureStart('pinch', 20, 40, 90, 90);
|
|
|
|
expect(pointerEvent).to.have.been.calledOnceWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(qemuKeyEvent).to.not.have.been.called;
|
|
|
|
pointerEvent.resetHistory();
|
|
|
|
gestureMove('pinch', 20, 40, 30, 30);
|
|
|
|
expect(pointerEvent).to.have.been.calledThrice;
|
|
expect(pointerEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
expect(pointerEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
20, 40, bmask);
|
|
expect(pointerEvent.thirdCall).to.have.been.calledWith(client._sock,
|
|
20, 40, 0x0);
|
|
|
|
expect(qemuKeyEvent).to.have.been.calledTwice;
|
|
expect(qemuKeyEvent.firstCall).to.have.been.calledWith(client._sock,
|
|
keysym,
|
|
true,
|
|
code);
|
|
expect(qemuKeyEvent.secondCall).to.have.been.calledWith(client._sock,
|
|
keysym,
|
|
false,
|
|
code);
|
|
|
|
expect(qemuKeyEvent.firstCall).to.have.been.calledBefore(pointerEvent.secondCall);
|
|
expect(qemuKeyEvent.lastCall).to.have.been.calledAfter(pointerEvent.lastCall);
|
|
|
|
pointerEvent.resetHistory();
|
|
qemuKeyEvent.resetHistory();
|
|
|
|
gestureEnd('pinch', 20, 40);
|
|
|
|
expect(pointerEvent).to.not.have.been.called;
|
|
expect(qemuKeyEvent).to.not.have.been.called;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('WebSocket events', function () {
|
|
// message events
|
|
it('should do nothing if we receive an empty message and have nothing in the queue', function () {
|
|
sinon.spy(client, "_normalMsg");
|
|
client._sock._websocket._receiveData(new Uint8Array([]));
|
|
expect(client._normalMsg).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle a message in the connected state as a normal message', function () {
|
|
sinon.spy(client, "_normalMsg");
|
|
client._sock._websocket._receiveData(new Uint8Array([1, 2, 3]));
|
|
expect(client._normalMsg).to.have.been.called;
|
|
});
|
|
|
|
it('should handle a message in any non-disconnected/failed state like an init message', function () {
|
|
client._rfbConnectionState = 'connecting';
|
|
client._rfbInitState = 'ProtocolVersion';
|
|
sinon.spy(client, "_initMsg");
|
|
client._sock._websocket._receiveData(new Uint8Array([1, 2, 3]));
|
|
expect(client._initMsg).to.have.been.called;
|
|
});
|
|
|
|
it('should process all normal messages directly', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("bell", spy);
|
|
client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02]));
|
|
expect(spy).to.have.been.calledTwice;
|
|
});
|
|
|
|
// open events
|
|
it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () {
|
|
client = new RFB(document.createElement('div'), 'wss://host:8675');
|
|
this.clock.tick();
|
|
client._sock._websocket._open();
|
|
expect(client._rfbInitState).to.equal('ProtocolVersion');
|
|
});
|
|
|
|
it('should fail if we are not currently ready to connect and we get an "open" event', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfbConnectionState = 'connected';
|
|
client._sock._websocket._open();
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
// close events
|
|
it('should transition to "disconnected" from "disconnecting" on a close event', function () {
|
|
const real = client._sock._websocket.close;
|
|
client._sock._websocket.close = () => {};
|
|
client.disconnect();
|
|
expect(client._rfbConnectionState).to.equal('disconnecting');
|
|
client._sock._websocket.close = real;
|
|
client._sock._websocket.close();
|
|
expect(client._rfbConnectionState).to.equal('disconnected');
|
|
});
|
|
|
|
it('should fail if we get a close event while connecting', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfbConnectionState = 'connecting';
|
|
client._sock._websocket.close();
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should unregister close event handler', function () {
|
|
sinon.spy(client._sock, 'off');
|
|
client.disconnect();
|
|
client._sock._websocket.close();
|
|
expect(client._sock.off).to.have.been.calledWith('close');
|
|
});
|
|
|
|
// error events do nothing
|
|
});
|
|
});
|
|
|
|
describe('Quality level setting', function () {
|
|
const defaultQuality = 6;
|
|
|
|
let client;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
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._rfbConnectionState = '';
|
|
newQuality = 2;
|
|
client.qualityLevel = newQuality;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client._rfbConnectionState = 'connnecting';
|
|
newQuality = 6;
|
|
client.qualityLevel = newQuality;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client._rfbConnectionState = '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('Compression level setting', function () {
|
|
const defaultCompression = 2;
|
|
|
|
let client;
|
|
|
|
beforeEach(function () {
|
|
client = makeRFB();
|
|
sinon.spy(RFB.messages, "clientEncodings");
|
|
});
|
|
|
|
afterEach(function () {
|
|
RFB.messages.clientEncodings.restore();
|
|
});
|
|
|
|
it(`should equal ${defaultCompression} by default`, function () {
|
|
expect(client._compressionLevel).to.equal(defaultCompression);
|
|
});
|
|
|
|
it('should ignore non-integers when set', function () {
|
|
client.compressionLevel = '1';
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = 1.5;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = null;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = undefined;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = {};
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
});
|
|
|
|
it('should ignore integers out of range [0, 9]', function () {
|
|
client.compressionLevel = -1;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = 10;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send clientEncodings with new compression value', function () {
|
|
let newCompression;
|
|
|
|
newCompression = 5;
|
|
client.compressionLevel = newCompression;
|
|
expect(client.compressionLevel).to.equal(newCompression);
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);
|
|
});
|
|
|
|
it('should not send clientEncodings if compression is the same', function () {
|
|
let newCompression;
|
|
|
|
newCompression = 9;
|
|
client.compressionLevel = newCompression;
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client.compressionLevel = newCompression;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not send clientEncodings if not in connected state', function () {
|
|
let newCompression;
|
|
|
|
client._rfbConnectionState = '';
|
|
newCompression = 7;
|
|
client.compressionLevel = newCompression;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client._rfbConnectionState = 'connnecting';
|
|
newCompression = 6;
|
|
client.compressionLevel = newCompression;
|
|
expect(RFB.messages.clientEncodings).to.not.have.been.called;
|
|
|
|
RFB.messages.clientEncodings.resetHistory();
|
|
|
|
client._rfbConnectionState = 'connected';
|
|
newCompression = 5;
|
|
client.compressionLevel = newCompression;
|
|
expect(RFB.messages.clientEncodings).to.have.been.calledOnce;
|
|
expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.pseudoEncodingCompressLevel0 + newCompression);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('RFB messages', function () {
|
|
let sock;
|
|
|
|
beforeEach(function () {
|
|
let websock = new FakeWebSocket();
|
|
websock._open();
|
|
sock = new Websock();
|
|
sock.attach(websock);
|
|
});
|
|
|
|
describe('Input events', function () {
|
|
it('should send correct data for keyboard events', function () {
|
|
// FIXME: down should be boolean
|
|
RFB.messages.keyEvent(sock, 0x12345678, 0);
|
|
let expected =
|
|
[ 4, 0, 0, 0, 0x12, 0x34, 0x56, 0x78];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
|
|
RFB.messages.keyEvent(sock, 0x90abcdef, 1);
|
|
expected =
|
|
[ 4, 1, 0, 0, 0x90, 0xab, 0xcd, 0xef];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should send correct data for QEMU keyboard events', function () {
|
|
// FIXME: down should be boolean
|
|
RFB.messages.QEMUExtendedKeyEvent(sock, 0x12345678, 0, 0x55);
|
|
let expected =
|
|
[ 255, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x55];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
|
|
RFB.messages.QEMUExtendedKeyEvent(sock, 0x90abcdef, 1, 0xe055);
|
|
expected =
|
|
[ 255, 0, 0, 1, 0x90, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0xd5];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should send correct data for pointer events', function () {
|
|
RFB.messages.pointerEvent(sock, 12345, 54321, 0xab);
|
|
let expected =
|
|
[ 5, 0xab, 0x30, 0x39, 0xd4, 0x31];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Clipboard events', function () {
|
|
it('should send correct data for clipboard events', function () {
|
|
RFB.messages.clientCutText(sock, new Uint8Array([ 0x01, 0x23, 0x45, 0x67 ]));
|
|
let expected =
|
|
[ 6, 0, 0, 0, 0x00, 0x00, 0x00, 0x04,
|
|
0x01, 0x23, 0x45, 0x67 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Extended clipboard handling send', function () {
|
|
it('should call clientCutText with correct Caps data', function () {
|
|
let formats = {
|
|
0: 2,
|
|
2: 4121
|
|
};
|
|
let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,
|
|
0xFF, 0xFF, 0xFF, 0xF4,
|
|
0x1F, 0x00, 0x00, 0x05,
|
|
0x00, 0x00, 0x00, 0x02,
|
|
0x00, 0x00, 0x10, 0x19]);
|
|
let actions = [
|
|
1 << 24, // Caps
|
|
1 << 25, // Request
|
|
1 << 26, // Peek
|
|
1 << 27, // Notify
|
|
1 << 28 // Provide
|
|
];
|
|
|
|
RFB.messages.extendedClipboardCaps(sock, actions, formats);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
|
|
it('should call clientCutText with correct Request data', function () {
|
|
let formats = new Uint8Array([0x01]);
|
|
let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,
|
|
0xFF, 0xFF, 0xFF, 0xFC,
|
|
0x02, 0x00, 0x00, 0x01]);
|
|
|
|
RFB.messages.extendedClipboardRequest(sock, formats);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
|
|
it('should call clientCutText with correct Notify data', function () {
|
|
let formats = new Uint8Array([0x01]);
|
|
let expectedData = new Uint8Array([0x06, 0x00, 0x00, 0x00,
|
|
0xFF, 0xFF, 0xFF, 0xFC,
|
|
0x08, 0x00, 0x00, 0x01]);
|
|
|
|
RFB.messages.extendedClipboardNotify(sock, formats);
|
|
|
|
expect(sock).to.have.sent(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(8 + 4 + deflatedData.length);
|
|
expectedData[0] = 0x06; // Message type
|
|
expectedData[1] = 0x00;
|
|
expectedData[2] = 0x00;
|
|
expectedData[3] = 0x00;
|
|
expectedData[4] = 0xFF; // Size
|
|
expectedData[5] = 0xFF;
|
|
expectedData[6] = 0xFF;
|
|
expectedData[7] = 256 - (4 + deflatedData.length);
|
|
expectedData[8] = 0x10; // The client capabilities
|
|
expectedData[9] = 0x00; // Reserved flags
|
|
expectedData[10] = 0x00; // Reserved flags
|
|
expectedData[11] = 0x01; // The formats client supports
|
|
expectedData.set(deflatedData, 12);
|
|
|
|
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
|
|
});
|
|
|
|
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(8 + 4 + deflatedData.length);
|
|
expectedData[0] = 0x06; // Message type
|
|
expectedData[1] = 0x00;
|
|
expectedData[2] = 0x00;
|
|
expectedData[3] = 0x00;
|
|
expectedData[4] = 0xFF; // Size
|
|
expectedData[5] = 0xFF;
|
|
expectedData[6] = 0xFF;
|
|
expectedData[7] = 256 - (4 + deflatedData.length);
|
|
expectedData[8] = 0x10; // The client capabilities
|
|
expectedData[9] = 0x00; // Reserved flags
|
|
expectedData[10] = 0x00; // Reserved flags
|
|
expectedData[11] = 0x01; // The formats client supports
|
|
expectedData.set(deflatedData, 12);
|
|
|
|
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
|
|
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(8 + 4 + deflatedData.length);
|
|
expectedData[0] = 0x06; // Message type
|
|
expectedData[1] = 0x00;
|
|
expectedData[2] = 0x00;
|
|
expectedData[3] = 0x00;
|
|
expectedData[4] = 0xFF; // Size
|
|
expectedData[5] = 0xFF;
|
|
expectedData[6] = 0xFF;
|
|
expectedData[7] = 256 - (4 + deflatedData.length);
|
|
expectedData[8] = 0x10; // The client capabilities
|
|
expectedData[9] = 0x00; // Reserved flags
|
|
expectedData[10] = 0x00; // Reserved flags
|
|
expectedData[11] = 0x01; // The formats client supports
|
|
expectedData.set(deflatedData, 12);
|
|
|
|
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
|
|
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(8 + 4 + deflatedData.length);
|
|
expectedData[0] = 0x06; // Message type
|
|
expectedData[1] = 0x00;
|
|
expectedData[2] = 0x00;
|
|
expectedData[3] = 0x00;
|
|
expectedData[4] = 0xFF; // Size
|
|
expectedData[5] = 0xFF;
|
|
expectedData[6] = 0xFF;
|
|
expectedData[7] = 256 - (4 + deflatedData.length);
|
|
expectedData[8] = 0x10; // The client capabilities
|
|
expectedData[9] = 0x00; // Reserved flags
|
|
expectedData[10] = 0x00; // Reserved flags
|
|
expectedData[11] = 0x01; // The formats client supports
|
|
expectedData.set(deflatedData, 12);
|
|
|
|
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
|
|
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(8 + 4 + deflatedData.length);
|
|
expectedData[0] = 0x06; // Message type
|
|
expectedData[1] = 0x00;
|
|
expectedData[2] = 0x00;
|
|
expectedData[3] = 0x00;
|
|
expectedData[4] = 0xFF; // Size
|
|
expectedData[5] = 0xFF;
|
|
expectedData[6] = 0xFF;
|
|
expectedData[7] = 256 - (4 + deflatedData.length);
|
|
expectedData[8] = 0x10; // The client capabilities
|
|
expectedData[9] = 0x00; // Reserved flags
|
|
expectedData[10] = 0x00; // Reserved flags
|
|
expectedData[11] = 0x01; // The formats client supports
|
|
expectedData.set(deflatedData, 12);
|
|
|
|
RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]);
|
|
|
|
expect(sock).to.have.sent(expectedData);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Screen layout', function () {
|
|
it('should send correct data for screen layout changes', function () {
|
|
RFB.messages.setDesktopSize(sock, 12345, 54321, 0x12345678, 0x90abcdef);
|
|
let expected =
|
|
[ 251, 0, 0x30, 0x39, 0xd4, 0x31, 0x01, 0x00,
|
|
0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00,
|
|
0x30, 0x39, 0xd4, 0x31, 0x90, 0xab, 0xcd, 0xef ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Fences', function () {
|
|
it('should send correct data for fences', function () {
|
|
// FIXME: Payload should be a byte array
|
|
RFB.messages.clientFence(sock, 0x12345678, "text");
|
|
let expected =
|
|
[ 248, 0, 0, 0, 0x12, 0x34, 0x56, 0x78,
|
|
4, 0x74, 0x65, 0x78, 0x74 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Continuous updates', function () {
|
|
it('should send correct data for continuous updates configuration', function () {
|
|
// FIXME: enable should be boolean
|
|
RFB.messages.enableContinuousUpdates(sock, 0, 12345, 54321, 34343, 18181);
|
|
let expected =
|
|
[ 150, 0, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Pixel format', function () {
|
|
it('should send correct data for normal depth', function () {
|
|
RFB.messages.pixelFormat(sock, 24, true);
|
|
let expected =
|
|
[ 0, 0, 0, 0, 32, 24, 0, 1,
|
|
0, 255, 0, 255, 0, 255, 0, 8, 16, 0, 0, 0 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should send correct data for low depth', function () {
|
|
RFB.messages.pixelFormat(sock, 8, true);
|
|
let expected =
|
|
[ 0, 0, 0, 0, 8, 8, 0, 1,
|
|
0, 3, 0, 3, 0, 3, 0, 2, 4, 0, 0, 0 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Encodings', function () {
|
|
it('should send correct data for supported encodings', function () {
|
|
RFB.messages.clientEncodings(sock, [ 0x12345678,
|
|
0x90abcdef,
|
|
0x10293847 ]);
|
|
let expected =
|
|
[ 2, 0, 0, 3, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd,
|
|
0xef, 0x10, 0x29, 0x38, 0x47 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('Update request', function () {
|
|
it('should send correct data for update request', function () {
|
|
RFB.messages.fbUpdateRequest(sock, true, 12345, 54321, 34343, 18181);
|
|
let expected =
|
|
[ 3, 1, 0x30, 0x39, 0xd4, 0x31, 0x86, 0x27, 0x47, 0x05 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
|
|
describe('XVP operations', function () {
|
|
it('should send correct data for XVP operations', function () {
|
|
RFB.messages.xvpOp(sock, 123, 45);
|
|
let expected =
|
|
[ 250, 0, 123, 45 ];
|
|
expect(sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
});
|
|
});
|