mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-11-26 01:53:11 +01:00
766fc43855
Supports both classic cursor type and alpha cursor type. In classic mode the server can send 'inverted' pixels for the cursor, our code does not support this but handles these pixels as opaque black. Co-authored-by: Samuel Mannehed <samuel@cendio.se>
2577 lines
115 KiB
JavaScript
2577 lines
115 KiB
JavaScript
const expect = chai.expect;
|
|
|
|
import RFB from '../core/rfb.js';
|
|
import Websock from '../core/websock.js';
|
|
import { encodings } from '../core/encodings.js';
|
|
|
|
import FakeWebSocket from './fake.websocket.js';
|
|
|
|
/* UIEvent constructor polyfill for IE */
|
|
(() => {
|
|
if (typeof window.UIEvent === "function") return;
|
|
|
|
function UIEvent( event, params ) {
|
|
params = params || { bubbles: false, cancelable: false, view: window, detail: undefined };
|
|
const evt = document.createEvent( 'UIEvent' );
|
|
evt.initUIEvent( event, params.bubbles, params.cancelable, params.view, params.detail );
|
|
return evt;
|
|
}
|
|
|
|
UIEvent.prototype = window.UIEvent.prototype;
|
|
|
|
window.UIEvent = UIEvent;
|
|
})();
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
describe('Remote Frame Buffer Protocol Client', function () {
|
|
let clock;
|
|
let raf;
|
|
|
|
before(FakeWebSocket.replace);
|
|
after(FakeWebSocket.restore);
|
|
|
|
before(function () {
|
|
this.clock = clock = sinon.useFakeTimers();
|
|
// sinon doesn't support this yet
|
|
raf = window.requestAnimationFrame;
|
|
window.requestAnimationFrame = setTimeout;
|
|
// 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._old_allocate_buffers = Websock.prototype._allocate_buffers;
|
|
Websock.prototype._allocate_buffers = function () {
|
|
this._sQ = _sQ;
|
|
this._rQ = rQ;
|
|
};
|
|
|
|
});
|
|
|
|
after(function () {
|
|
Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers;
|
|
this.clock.restore();
|
|
window.requestAnimationFrame = raf;
|
|
});
|
|
|
|
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 make_rfb(url, options) {
|
|
url = url || 'wss://host:8675';
|
|
const rfb = new RFB(container, url, options);
|
|
clock.tick();
|
|
rfb._sock._websocket._open();
|
|
rfb._rfb_connection_state = 'connected';
|
|
sinon.spy(rfb, "_disconnect");
|
|
rfbs.push(rfb);
|
|
return rfb;
|
|
}
|
|
|
|
describe('Connecting/Disconnecting', function () {
|
|
describe('#RFB', function () {
|
|
it('should set the current state to "connecting"', function () {
|
|
const client = new RFB(document.createElement('div'), 'wss://host:8675');
|
|
client._rfb_connection_state = '';
|
|
this.clock.tick();
|
|
expect(client._rfb_connection_state).to.equal('connecting');
|
|
});
|
|
|
|
it('should actually connect to the websocket', function () {
|
|
const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
|
|
sinon.spy(client._sock, 'open');
|
|
this.clock.tick();
|
|
expect(client._sock.open).to.have.been.calledOnce;
|
|
expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH');
|
|
});
|
|
});
|
|
|
|
describe('#disconnect', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
it('should go to state "disconnecting" before "disconnected"', function () {
|
|
sinon.spy(client, '_updateConnectionState');
|
|
client.disconnect();
|
|
expect(client._updateConnectionState).to.have.been.calledTwice;
|
|
expect(client._updateConnectionState.getCall(0).args[0])
|
|
.to.equal('disconnecting');
|
|
expect(client._updateConnectionState.getCall(1).args[0])
|
|
.to.equal('disconnected');
|
|
expect(client._rfb_connection_state).to.equal('disconnected');
|
|
});
|
|
|
|
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('#sendCredentials', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
client._rfb_connection_state = 'connecting';
|
|
});
|
|
|
|
it('should set the rfb credentials properly"', function () {
|
|
client.sendCredentials({ password: 'pass' });
|
|
expect(client._rfb_credentials).to.deep.equal({ password: 'pass' });
|
|
});
|
|
|
|
it('should call init_msg "soon"', function () {
|
|
client._init_msg = sinon.spy();
|
|
client.sendCredentials({ password: 'pass' });
|
|
this.clock.tick(5);
|
|
expect(client._init_msg).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Public API Basic Behavior', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
describe('#sendCtrlAlDel', function () {
|
|
it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () {
|
|
const expected = {_sQ: new Uint8Array(48), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.keyEvent(expected, 0xFFE3, 1);
|
|
RFB.messages.keyEvent(expected, 0xFFE9, 1);
|
|
RFB.messages.keyEvent(expected, 0xFFFF, 1);
|
|
RFB.messages.keyEvent(expected, 0xFFFF, 0);
|
|
RFB.messages.keyEvent(expected, 0xFFE9, 0);
|
|
RFB.messages.keyEvent(expected, 0xFFE3, 0);
|
|
|
|
client.sendCtrlAltDel();
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
|
|
it('should not send the keys if we are not in a normal state', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._rfb_connection_state = "connecting";
|
|
client.sendCtrlAltDel();
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not send the keys if we are set as view_only', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._viewOnly = true;
|
|
client.sendCtrlAltDel();
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('#sendKey', function () {
|
|
it('should send a single key with the given code and state (down = true)', function () {
|
|
const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.keyEvent(expected, 123, 1);
|
|
client.sendKey(123, 'Key123', true);
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
|
|
it('should send both a down and up event if the state is not specified', function () {
|
|
const expected = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.keyEvent(expected, 123, 1);
|
|
RFB.messages.keyEvent(expected, 123, 0);
|
|
client.sendKey(123, 'Key123');
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
|
|
it('should not send the key if we are not in a normal state', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._rfb_connection_state = "connecting";
|
|
client.sendKey(123, 'Key123');
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not send the key if we are set as view_only', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
client._viewOnly = true;
|
|
client.sendKey(123, 'Key123');
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send QEMU extended events if supported', function () {
|
|
client._qemuExtKeyEventSupported = true;
|
|
const expected = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.QEMUExtendedKeyEvent(expected, 0x20, true, 0x0039);
|
|
client.sendKey(0x20, 'Space', true);
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
|
|
it('should not send QEMU extended events if unknown key code', function () {
|
|
client._qemuExtKeyEventSupported = true;
|
|
const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.keyEvent(expected, 123, 1);
|
|
client.sendKey(123, 'FooBar', true);
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
});
|
|
|
|
describe('#focus', function () {
|
|
it('should move focus to canvas object', function () {
|
|
client._canvas.focus = sinon.spy();
|
|
client.focus();
|
|
expect(client._canvas.focus).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('#blur', function () {
|
|
it('should remove focus from canvas object', function () {
|
|
client._canvas.blur = sinon.spy();
|
|
client.blur();
|
|
expect(client._canvas.blur).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('#clipboardPasteFrom', function () {
|
|
it('should send the given text in a paste event', function () {
|
|
const expected = {_sQ: new Uint8Array(11), _sQlen: 0,
|
|
_sQbufferSize: 11, flush: () => {}};
|
|
RFB.messages.clientCutText(expected, 'abc');
|
|
client.clipboardPasteFrom('abc');
|
|
expect(client._sock).to.have.sent(expected._sQ);
|
|
});
|
|
|
|
it('should flush multiple times for large clipboards', function () {
|
|
sinon.spy(client._sock, 'flush');
|
|
let long_text = "";
|
|
for (let i = 0; i < client._sock._sQbufferSize + 100; i++) {
|
|
long_text += 'a';
|
|
}
|
|
client.clipboardPasteFrom(long_text);
|
|
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._rfb_connection_state = "connecting";
|
|
client.clipboardPasteFrom('abc');
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe("XVP operations", function () {
|
|
beforeEach(function () {
|
|
client._rfb_xvp_ver = 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 = make_rfb();
|
|
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.reset();
|
|
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick();
|
|
|
|
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._receive_data(new Uint8Array(incoming));
|
|
|
|
// 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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick();
|
|
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick();
|
|
|
|
expect(client._display.viewportChangeSize).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.reset();
|
|
|
|
// 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 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.reset();
|
|
|
|
// 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 = make_rfb();
|
|
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.reset();
|
|
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick();
|
|
|
|
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._receive_data(new Uint8Array(incoming));
|
|
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick();
|
|
|
|
expect(client._display.autoscale).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('Remote resize', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
client._supportsSetDesktopSize = true;
|
|
client.resizeSession = true;
|
|
container.style.width = '70px';
|
|
container.style.height = '80px';
|
|
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 () {
|
|
// Simple ExtendedDesktopSize FBU message
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04,
|
|
0x00, 0x00, 0x00, 0x00 ];
|
|
|
|
// First message should trigger a resize
|
|
|
|
client._supportsSetDesktopSize = false;
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(incoming));
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0);
|
|
|
|
RFB.messages.setDesktopSize.reset();
|
|
|
|
// Second message should not trigger a resize
|
|
|
|
client._sock._websocket._receive_data(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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
clock.tick(1000);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledOnce;
|
|
expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0);
|
|
});
|
|
|
|
it('should not resize until the container size is stable', function () {
|
|
container.style.width = '20px';
|
|
container.style.height = '30px';
|
|
const event1 = new UIEvent('resize');
|
|
window.dispatchEvent(event1);
|
|
clock.tick(400);
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
const event2 = new UIEvent('resize');
|
|
window.dispatchEvent(event2);
|
|
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, 0, 0);
|
|
});
|
|
|
|
it('should not resize when resize is disabled', function () {
|
|
client._resizeSession = false;
|
|
|
|
container.style.width = '40px';
|
|
container.style.height = '50px';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
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';
|
|
const event = new UIEvent('resize');
|
|
window.dispatchEvent(event);
|
|
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
|
|
const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04,
|
|
0x00, 0x00, 0x00, 0x00 ];
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(incoming));
|
|
|
|
expect(RFB.messages.setDesktopSize).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('Misc Internals', function () {
|
|
describe('#_updateConnectionState', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
it('should clear the disconnect timer if the state is not "disconnecting"', function () {
|
|
const spy = sinon.spy();
|
|
client._disconnTimer = setTimeout(spy, 50);
|
|
client._rfb_connection_state = 'connecting';
|
|
client._updateConnectionState('connected');
|
|
this.clock.tick(51);
|
|
expect(spy).to.not.have.been.called;
|
|
expect(client._disconnTimer).to.be.null;
|
|
});
|
|
|
|
it('should set the rfb_connection_state', function () {
|
|
client._rfb_connection_state = 'connecting';
|
|
client._updateConnectionState('connected');
|
|
expect(client._rfb_connection_state).to.equal('connected');
|
|
});
|
|
|
|
it('should not change the state when we are disconnected', function () {
|
|
client.disconnect();
|
|
expect(client._rfb_connection_state).to.equal('disconnected');
|
|
client._updateConnectionState('connecting');
|
|
expect(client._rfb_connection_state).to.not.equal('connecting');
|
|
});
|
|
|
|
it('should ignore state changes to the same state', function () {
|
|
const connectSpy = sinon.spy();
|
|
client.addEventListener("connect", connectSpy);
|
|
|
|
expect(client._rfb_connection_state).to.equal('connected');
|
|
client._updateConnectionState('connected');
|
|
expect(connectSpy).to.not.have.been.called;
|
|
|
|
client.disconnect();
|
|
|
|
const disconnectSpy = sinon.spy();
|
|
client.addEventListener("disconnect", disconnectSpy);
|
|
|
|
expect(client._rfb_connection_state).to.equal('disconnected');
|
|
client._updateConnectionState('disconnected');
|
|
expect(disconnectSpy).to.not.have.been.called;
|
|
});
|
|
|
|
it('should ignore illegal state changes', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("disconnect", spy);
|
|
client._updateConnectionState('disconnected');
|
|
expect(client._rfb_connection_state).to.not.equal('disconnected');
|
|
expect(spy).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('#_fail', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
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._rfb_connection_state).to.equal('disconnected');
|
|
});
|
|
|
|
it('should set clean_disconnect variable', function () {
|
|
client._rfb_clean_disconnect = true;
|
|
client._rfb_connection_state = 'connected';
|
|
client._fail();
|
|
expect(client._rfb_clean_disconnect).to.be.false;
|
|
});
|
|
|
|
it('should result in disconnect event with clean set to false', function () {
|
|
client._rfb_connection_state = '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('Connection States', function () {
|
|
describe('connecting', function () {
|
|
it('should open the websocket connection', function () {
|
|
const client = new RFB(document.createElement('div'),
|
|
'ws://HOST:8675/PATH');
|
|
sinon.spy(client._sock, 'open');
|
|
this.clock.tick();
|
|
expect(client._sock.open).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('connected', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
it('should result in a connect event if state becomes connected', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("connect", spy);
|
|
client._rfb_connection_state = 'connecting';
|
|
client._updateConnectionState('connected');
|
|
expect(spy).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should not result in a connect event if the state is not "connected"', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("connect", spy);
|
|
client._sock._websocket.open = () => {}; // explicitly don't call onopen
|
|
client._updateConnectionState('connecting');
|
|
expect(spy).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('disconnecting', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () {
|
|
sinon.spy(client, '_updateConnectionState');
|
|
client._sock._websocket.close = () => {}; // explicitly don't call onclose
|
|
client._updateConnectionState('disconnecting');
|
|
this.clock.tick(3 * 1000);
|
|
expect(client._updateConnectionState).to.have.been.calledTwice;
|
|
expect(client._rfb_disconnect_reason).to.not.equal("");
|
|
expect(client._rfb_connection_state).to.equal("disconnected");
|
|
});
|
|
|
|
it('should not fail if Websock.onclose gets called within the disconnection timeout', function () {
|
|
client._updateConnectionState('disconnecting');
|
|
this.clock.tick(3 * 1000 / 2);
|
|
client._sock._websocket.close();
|
|
this.clock.tick(3 * 1000 / 2 + 1);
|
|
expect(client._rfb_connection_state).to.equal('disconnected');
|
|
});
|
|
|
|
it('should close the WebSocket connection', function () {
|
|
sinon.spy(client._sock, 'close');
|
|
client._updateConnectionState('disconnecting');
|
|
expect(client._sock.close).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should not result in a disconnect event', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("disconnect", spy);
|
|
client._sock._websocket.close = () => {}; // explicitly don't call onclose
|
|
client._updateConnectionState('disconnecting');
|
|
expect(spy).to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
describe('disconnected', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH');
|
|
});
|
|
|
|
it('should result in a disconnect event if state becomes "disconnected"', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("disconnect", spy);
|
|
client._rfb_connection_state = 'disconnecting';
|
|
client._updateConnectionState('disconnected');
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.clean).to.be.true;
|
|
});
|
|
|
|
it('should result in a disconnect event without msg when no reason given', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("disconnect", spy);
|
|
client._rfb_connection_state = 'disconnecting';
|
|
client._rfb_disconnect_reason = "";
|
|
client._updateConnectionState('disconnected');
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0].length).to.equal(1);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Protocol Initialization States', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
client._rfb_connection_state = 'connecting';
|
|
});
|
|
|
|
describe('ProtocolVersion', function () {
|
|
function send_ver(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._receive_data(arr);
|
|
}
|
|
|
|
describe('version parsing', function () {
|
|
it('should interpret version 003.003 as version 3.3', function () {
|
|
send_ver('003.003', client);
|
|
expect(client._rfb_version).to.equal(3.3);
|
|
});
|
|
|
|
it('should interpret version 003.006 as version 3.3', function () {
|
|
send_ver('003.006', client);
|
|
expect(client._rfb_version).to.equal(3.3);
|
|
});
|
|
|
|
it('should interpret version 003.889 as version 3.3', function () {
|
|
send_ver('003.889', client);
|
|
expect(client._rfb_version).to.equal(3.3);
|
|
});
|
|
|
|
it('should interpret version 003.007 as version 3.7', function () {
|
|
send_ver('003.007', client);
|
|
expect(client._rfb_version).to.equal(3.7);
|
|
});
|
|
|
|
it('should interpret version 003.008 as version 3.8', function () {
|
|
send_ver('003.008', client);
|
|
expect(client._rfb_version).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 004.000 as version 3.8', function () {
|
|
send_ver('004.000', client);
|
|
expect(client._rfb_version).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 004.001 as version 3.8', function () {
|
|
send_ver('004.001', client);
|
|
expect(client._rfb_version).to.equal(3.8);
|
|
});
|
|
|
|
it('should interpret version 005.000 as version 3.8', function () {
|
|
send_ver('005.000', client);
|
|
expect(client._rfb_version).to.equal(3.8);
|
|
});
|
|
|
|
it('should fail on an invalid version', function () {
|
|
sinon.spy(client, "_fail");
|
|
send_ver('002.000', client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
it('should send back the interpreted version', function () {
|
|
send_ver('004.000', client);
|
|
|
|
const expected_str = 'RFB 003.008\n';
|
|
const expected = [];
|
|
for (let i = 0; i < expected_str.length; i++) {
|
|
expected[i] = expected_str.charCodeAt(i);
|
|
}
|
|
|
|
expect(client._sock).to.have.sent(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should transition to the Security state on successful negotiation', function () {
|
|
send_ver('003.008', client);
|
|
expect(client._rfb_init_state).to.equal('Security');
|
|
});
|
|
|
|
describe('Repeater', function () {
|
|
beforeEach(function () {
|
|
client = make_rfb('wss://host:8675', { repeaterID: "12345" });
|
|
client._rfb_connection_state = 'connecting';
|
|
});
|
|
|
|
it('should interpret version 000.000 as a repeater', function () {
|
|
send_ver('000.000', client);
|
|
expect(client._rfb_version).to.equal(0);
|
|
|
|
const sent_data = client._sock._websocket._get_sent_data();
|
|
expect(new Uint8Array(sent_data.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0]));
|
|
expect(sent_data).to.have.length(250);
|
|
});
|
|
|
|
it('should handle two step repeater negotiation', function () {
|
|
send_ver('000.000', client);
|
|
send_ver('003.008', client);
|
|
expect(client._rfb_version).to.equal(3.8);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Security', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'Security';
|
|
});
|
|
|
|
it('should simply receive the auth scheme when for versions < 3.7', function () {
|
|
client._rfb_version = 3.6;
|
|
const auth_scheme_raw = [1, 2, 3, 4];
|
|
const auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) +
|
|
(auth_scheme_raw[2] << 8) + auth_scheme_raw[3];
|
|
client._sock._websocket._receive_data(new Uint8Array(auth_scheme_raw));
|
|
expect(client._rfb_auth_scheme).to.equal(auth_scheme);
|
|
});
|
|
|
|
it('should prefer no authentication is possible', function () {
|
|
client._rfb_version = 3.7;
|
|
const auth_schemes = [2, 1, 3];
|
|
client._sock._websocket._receive_data(new Uint8Array(auth_schemes));
|
|
expect(client._rfb_auth_scheme).to.equal(1);
|
|
expect(client._sock).to.have.sent(new Uint8Array([1, 1]));
|
|
});
|
|
|
|
it('should choose for the most prefered scheme possible for versions >= 3.7', function () {
|
|
client._rfb_version = 3.7;
|
|
const auth_schemes = [2, 22, 16];
|
|
client._sock._websocket._receive_data(new Uint8Array(auth_schemes));
|
|
expect(client._rfb_auth_scheme).to.equal(22);
|
|
expect(client._sock).to.have.sent(new Uint8Array([22]));
|
|
});
|
|
|
|
it('should fail if there are no supported schemes for versions >= 3.7', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfb_version = 3.7;
|
|
const auth_schemes = [1, 32];
|
|
client._sock._websocket._receive_data(new Uint8Array(auth_schemes));
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () {
|
|
client._rfb_version = 3.7;
|
|
const failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
|
|
sinon.spy(client, '_fail');
|
|
client._sock._websocket._receive_data(new Uint8Array(failure_data));
|
|
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
expect(client._fail).to.have.been.calledWith(
|
|
'Security negotiation failed on no security types (reason: whoops)');
|
|
});
|
|
|
|
it('should transition to the Authentication state and continue on successful negotiation', function () {
|
|
client._rfb_version = 3.7;
|
|
const auth_schemes = [1, 1];
|
|
client._negotiate_authentication = sinon.spy();
|
|
client._sock._websocket._receive_data(new Uint8Array(auth_schemes));
|
|
expect(client._rfb_init_state).to.equal('Authentication');
|
|
expect(client._negotiate_authentication).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('Authentication', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'Security';
|
|
});
|
|
|
|
function send_security(type, cl) {
|
|
cl._sock._websocket._receive_data(new Uint8Array([1, type]));
|
|
}
|
|
|
|
it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
|
|
client._rfb_version = 3.6;
|
|
const err_msg = "Whoopsies";
|
|
const data = [0, 0, 0, 0];
|
|
const err_len = err_msg.length;
|
|
push32(data, err_len);
|
|
for (let i = 0; i < err_len; i++) {
|
|
data.push(err_msg.charCodeAt(i));
|
|
}
|
|
|
|
sinon.spy(client, '_fail');
|
|
client._sock._websocket._receive_data(new Uint8Array(data));
|
|
expect(client._fail).to.have.been.calledWith(
|
|
'Security negotiation failed on authentication scheme (reason: Whoopsies)');
|
|
});
|
|
|
|
it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
|
|
client._rfb_version = 3.8;
|
|
send_security(1, client);
|
|
expect(client._rfb_init_state).to.equal('SecurityResult');
|
|
});
|
|
|
|
it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () {
|
|
client._rfb_version = 3.7;
|
|
send_security(1, client);
|
|
expect(client._rfb_init_state).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should fail on an unknown auth scheme', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfb_version = 3.8;
|
|
send_security(57, client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
describe('VNC Authentication (type 2) Handler', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'Security';
|
|
client._rfb_version = 3.8;
|
|
});
|
|
|
|
it('should fire the credentialsrequired event if missing a password', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
send_security(2, client);
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receive_data(new Uint8Array(challenge));
|
|
|
|
expect(client._rfb_credentials).to.be.empty;
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.types).to.have.members(["password"]);
|
|
});
|
|
|
|
it('should encrypt the password with DES and then send it back', function () {
|
|
client._rfb_credentials = { password: 'passwd' };
|
|
send_security(2, client);
|
|
client._sock._websocket._get_sent_data(); // skip the choice of auth reply
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receive_data(new Uint8Array(challenge));
|
|
|
|
const des_pass = RFB.genDES('passwd', challenge);
|
|
expect(client._sock).to.have.sent(new Uint8Array(des_pass));
|
|
});
|
|
|
|
it('should transition to SecurityResult immediately after sending the password', function () {
|
|
client._rfb_credentials = { password: 'passwd' };
|
|
send_security(2, client);
|
|
|
|
const challenge = [];
|
|
for (let i = 0; i < 16; i++) { challenge[i] = i; }
|
|
client._sock._websocket._receive_data(new Uint8Array(challenge));
|
|
|
|
expect(client._rfb_init_state).to.equal('SecurityResult');
|
|
});
|
|
});
|
|
|
|
describe('XVP Authentication (type 22) Handler', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'Security';
|
|
client._rfb_version = 3.8;
|
|
});
|
|
|
|
it('should fall through to standard VNC authentication upon completion', function () {
|
|
client._rfb_credentials = { username: 'user',
|
|
target: 'target',
|
|
password: 'password' };
|
|
client._negotiate_std_vnc_auth = sinon.spy();
|
|
send_security(22, client);
|
|
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should fire the credentialsrequired event if all credentials are missing', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("credentialsrequired", spy);
|
|
client._rfb_credentials = {};
|
|
send_security(22, client);
|
|
|
|
expect(client._rfb_credentials).to.be.empty;
|
|
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._rfb_credentials = { username: 'user',
|
|
target: 'target' };
|
|
send_security(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._rfb_credentials = { username: 'user',
|
|
target: 'target',
|
|
password: 'password' };
|
|
client._negotiate_std_vnc_auth = sinon.spy();
|
|
|
|
send_security(22, client);
|
|
|
|
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 () {
|
|
client._rfb_init_state = 'Security';
|
|
client._rfb_version = 3.8;
|
|
send_security(16, client);
|
|
client._sock._websocket._get_sent_data(); // skip the security reply
|
|
});
|
|
|
|
function send_num_str_pairs(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._receive_data(new Uint8Array(data));
|
|
}
|
|
|
|
it('should skip tunnel negotiation if no tunnels are requested', function () {
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfb_tightvnc).to.be.true;
|
|
});
|
|
|
|
it('should fail if no supported tunnels are listed', function () {
|
|
sinon.spy(client, "_fail");
|
|
send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should choose the notunnel tunnel type', function () {
|
|
send_num_str_pairs([[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 () {
|
|
send_num_str_pairs([[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 () {
|
|
send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client);
|
|
client._sock._websocket._get_sent_data(); // skip the tunnel choice here
|
|
send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._rfb_init_state).to.equal('SecurityResult');
|
|
});
|
|
|
|
/*it('should attempt to use VNC auth over no auth when possible', function () {
|
|
client._rfb_tightvnc = true;
|
|
client._negotiate_std_vnc_auth = sinon.spy();
|
|
send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client);
|
|
expect(client._sock).to.have.sent([0, 0, 0, 1]);
|
|
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
|
|
expect(client._rfb_auth_scheme).to.equal(2);
|
|
});*/ // while this would make sense, the original code doesn't actually do this
|
|
|
|
it('should accept the "no auth" auth type and transition to SecurityResult', function () {
|
|
client._rfb_tightvnc = true;
|
|
send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._rfb_init_state).to.equal('SecurityResult');
|
|
});
|
|
|
|
it('should accept VNC authentication and transition to that', function () {
|
|
client._rfb_tightvnc = true;
|
|
client._negotiate_std_vnc_auth = sinon.spy();
|
|
send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client);
|
|
expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2]));
|
|
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
|
|
expect(client._rfb_auth_scheme).to.equal(2);
|
|
});
|
|
|
|
it('should fail if there are no supported auth types', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfb_tightvnc = true;
|
|
send_num_str_pairs([[23, 'stdv', 'badval__']], client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('SecurityResult', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'SecurityResult';
|
|
});
|
|
|
|
it('should fall through to ServerInitialisation on a response code of 0', function () {
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfb_init_state).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should fail on an error code of 1 with the given message for versions >= 3.8', function () {
|
|
client._rfb_version = 3.8;
|
|
sinon.spy(client, '_fail');
|
|
const failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
|
|
client._sock._websocket._receive_data(new Uint8Array(failure_data));
|
|
expect(client._fail).to.have.been.calledWith(
|
|
'Security negotiation failed on security result (reason: whoops)');
|
|
});
|
|
|
|
it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
|
|
sinon.spy(client, '_fail');
|
|
client._rfb_version = 3.7;
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1]));
|
|
expect(client._fail).to.have.been.calledWith(
|
|
'Security handshake failed');
|
|
});
|
|
|
|
it('should result in securityfailure event when receiving a non zero status', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.status).to.equal(2);
|
|
});
|
|
|
|
it('should include reason when provided in securityfailure event', function () {
|
|
client._rfb_version = 3.8;
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
const failure_data = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,
|
|
32, 102, 97, 105, 108, 117, 114, 101];
|
|
client._sock._websocket._receive_data(new Uint8Array(failure_data));
|
|
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 () {
|
|
client._rfb_version = 3.9;
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
const failure_data = [0, 0, 0, 1, 0, 0, 0, 0];
|
|
client._sock._websocket._receive_data(new Uint8Array(failure_data));
|
|
expect(spy.args[0][0].detail.status).to.equal(1);
|
|
expect('reason' in spy.args[0][0].detail).to.be.false;
|
|
});
|
|
|
|
it('should not include reason in securityfailure event for version < 3.8', function () {
|
|
client._rfb_version = 3.6;
|
|
const spy = sinon.spy();
|
|
client.addEventListener("securityfailure", spy);
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2]));
|
|
expect(spy.args[0][0].detail.status).to.equal(2);
|
|
expect('reason' in spy.args[0][0].detail).to.be.false;
|
|
});
|
|
});
|
|
|
|
describe('ClientInitialisation', function () {
|
|
it('should transition to the ServerInitialisation state', function () {
|
|
const client = make_rfb();
|
|
client._rfb_connection_state = 'connecting';
|
|
client._rfb_init_state = 'SecurityResult';
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._rfb_init_state).to.equal('ServerInitialisation');
|
|
});
|
|
|
|
it('should send 1 if we are in shared mode', function () {
|
|
const client = make_rfb('wss://host:8675', { shared: true });
|
|
client._rfb_connection_state = 'connecting';
|
|
client._rfb_init_state = 'SecurityResult';
|
|
client._sock._websocket._receive_data(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 = make_rfb('wss://host:8675', { shared: false });
|
|
client._rfb_connection_state = 'connecting';
|
|
client._rfb_init_state = 'SecurityResult';
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0]));
|
|
expect(client._sock).to.have.sent(new Uint8Array([0]));
|
|
});
|
|
});
|
|
|
|
describe('ServerInitialisation', function () {
|
|
beforeEach(function () {
|
|
client._rfb_init_state = 'ServerInitialisation';
|
|
});
|
|
|
|
function send_server_init(opts, client) {
|
|
const full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0,
|
|
true_color: 1, red_max: 255, green_max: 255, blue_max: 255,
|
|
red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' };
|
|
for (let opt in opts) {
|
|
full_opts[opt] = opts[opt];
|
|
}
|
|
const data = [];
|
|
|
|
push16(data, full_opts.width);
|
|
push16(data, full_opts.height);
|
|
|
|
data.push(full_opts.bpp);
|
|
data.push(full_opts.depth);
|
|
data.push(full_opts.big_endian);
|
|
data.push(full_opts.true_color);
|
|
|
|
push16(data, full_opts.red_max);
|
|
push16(data, full_opts.green_max);
|
|
push16(data, full_opts.blue_max);
|
|
push8(data, full_opts.red_shift);
|
|
push8(data, full_opts.green_shift);
|
|
push8(data, full_opts.blue_shift);
|
|
|
|
// padding
|
|
push8(data, 0);
|
|
push8(data, 0);
|
|
push8(data, 0);
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(data));
|
|
|
|
const name_data = [];
|
|
let name_len = [];
|
|
pushString(name_data, full_opts.name);
|
|
push32(name_len, name_data.length);
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(name_len));
|
|
client._sock._websocket._receive_data(new Uint8Array(name_data));
|
|
}
|
|
|
|
it('should set the framebuffer width and height', function () {
|
|
send_server_init({ width: 32, height: 84 }, client);
|
|
expect(client._fb_width).to.equal(32);
|
|
expect(client._fb_height).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);
|
|
send_server_init({ name: 'som€ nam€' }, client);
|
|
|
|
expect(client._fb_name).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._rfb_tightvnc = true;
|
|
send_server_init({}, client);
|
|
|
|
const tight_data = [];
|
|
push16(tight_data, 1);
|
|
push16(tight_data, 2);
|
|
push16(tight_data, 3);
|
|
push16(tight_data, 0);
|
|
for (let i = 0; i < 16 + 32 + 48; i++) {
|
|
tight_data.push(i);
|
|
}
|
|
client._sock._websocket._receive_data(new Uint8Array(tight_data));
|
|
|
|
expect(client._rfb_connection_state).to.equal('connected');
|
|
});
|
|
|
|
it('should resize the display', function () {
|
|
sinon.spy(client._display, 'resize');
|
|
send_server_init({ 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 mouse and keyboard', function () {
|
|
sinon.spy(client._keyboard, 'grab');
|
|
sinon.spy(client._mouse, 'grab');
|
|
send_server_init({}, client);
|
|
expect(client._keyboard.grab).to.have.been.calledOnce;
|
|
expect(client._mouse.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 () {
|
|
send_server_init({ 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);
|
|
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 () {
|
|
send_server_init({ 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 transition to the "connected" state', function () {
|
|
send_server_init({}, client);
|
|
expect(client._rfb_connection_state).to.equal('connected');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Protocol Message Processing After Completing Initialization', function () {
|
|
let client;
|
|
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
client._fb_name = 'some device';
|
|
client._fb_width = 640;
|
|
client._fb_height = 20;
|
|
});
|
|
|
|
describe('Framebuffer Update Handling', function () {
|
|
const target_data_arr = [
|
|
0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
|
|
0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
|
|
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255,
|
|
0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255
|
|
];
|
|
let target_data;
|
|
|
|
const target_data_check_arr = [
|
|
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
|
|
0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255,
|
|
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255,
|
|
0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255
|
|
];
|
|
let target_data_check;
|
|
|
|
before(function () {
|
|
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray
|
|
target_data = new Uint8Array(target_data_arr);
|
|
target_data_check = new Uint8Array(target_data_check_arr);
|
|
});
|
|
|
|
function send_fbu_msg(rect_info, rect_data, client, rect_cnt) {
|
|
let data = [];
|
|
|
|
if (!rect_cnt || rect_cnt > -1) {
|
|
// header
|
|
data.push(0); // msg type
|
|
data.push(0); // padding
|
|
push16(data, rect_cnt || rect_data.length);
|
|
}
|
|
|
|
for (let i = 0; i < rect_data.length; i++) {
|
|
if (rect_info[i]) {
|
|
push16(data, rect_info[i].x);
|
|
push16(data, rect_info[i].y);
|
|
push16(data, rect_info[i].width);
|
|
push16(data, rect_info[i].height);
|
|
push32(data, rect_info[i].encoding);
|
|
}
|
|
data = data.concat(rect_data[i]);
|
|
}
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(data));
|
|
}
|
|
|
|
it('should send an update request if there is sufficient data', function () {
|
|
const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20);
|
|
|
|
client._framebufferUpdate = () => true;
|
|
client._sock._websocket._receive_data(new Uint8Array([0]));
|
|
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
});
|
|
|
|
it('should not send an update request if we need more data', function () {
|
|
client._sock._websocket._receive_data(new Uint8Array([0]));
|
|
expect(client._sock._websocket._get_sent_data()).to.have.length(0);
|
|
});
|
|
|
|
it('should resume receiving an update if we previously did not have enough data', function () {
|
|
const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20);
|
|
|
|
// just enough to set FBU.rects
|
|
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3]));
|
|
expect(client._sock._websocket._get_sent_data()).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._receive_data(new Uint8Array([247]));
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
});
|
|
|
|
it('should not send a request in continuous updates mode', function () {
|
|
client._enabledContinuousUpdates = true;
|
|
client._framebufferUpdate = () => true;
|
|
client._sock._websocket._receive_data(new Uint8Array([0]));
|
|
|
|
expect(client._sock._websocket._get_sent_data()).to.have.length(0);
|
|
});
|
|
|
|
it('should fail on an unsupported encoding', function () {
|
|
sinon.spy(client, "_fail");
|
|
const rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 };
|
|
send_fbu_msg([rect_info], [[]], client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should be able to pause and resume receiving rects if not enought data', function () {
|
|
// seed some initial data to copy
|
|
client._fb_width = 4;
|
|
client._fb_height = 4;
|
|
client._display.resize(4, 4);
|
|
client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0);
|
|
|
|
const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
|
|
{ x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
|
|
// data says [{ old_x: 2, old_y: 0 }, { old_x: 0, old_y: 0 }]
|
|
const rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
|
|
send_fbu_msg([info[0]], [rects[0]], client, 2);
|
|
send_fbu_msg([info[1]], [rects[1]], client, -1);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
describe('Message Encoding Handlers', function () {
|
|
beforeEach(function () {
|
|
// a really small frame
|
|
client._fb_width = 4;
|
|
client._fb_height = 4;
|
|
client._fb_depth = 24;
|
|
client._display.resize(4, 4);
|
|
});
|
|
|
|
it('should handle the RAW encoding', function () {
|
|
const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 },
|
|
{ x: 2, y: 0, width: 2, height: 2, encoding: 0x00 },
|
|
{ x: 0, y: 2, width: 4, height: 1, encoding: 0x00 },
|
|
{ x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }];
|
|
// data is in bgrx
|
|
const rects = [
|
|
[0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0],
|
|
[0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0],
|
|
[0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0],
|
|
[0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]];
|
|
send_fbu_msg(info, rects, client);
|
|
expect(client._display).to.have.displayed(target_data);
|
|
});
|
|
|
|
it('should handle the RAW encoding in low colour mode', function () {
|
|
const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 },
|
|
{ x: 2, y: 0, width: 2, height: 2, encoding: 0x00 },
|
|
{ x: 0, y: 2, width: 4, height: 1, encoding: 0x00 },
|
|
{ x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }];
|
|
const rects = [
|
|
[0x03, 0x03, 0x03, 0x03],
|
|
[0x0c, 0x0c, 0x0c, 0x0c],
|
|
[0x0c, 0x0c, 0x03, 0x03],
|
|
[0x0c, 0x0c, 0x03, 0x03]];
|
|
client._fb_depth = 8;
|
|
send_fbu_msg(info, rects, client);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
it('should handle the COPYRECT encoding', function () {
|
|
// seed some initial data to copy
|
|
client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0);
|
|
|
|
const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01},
|
|
{ x: 2, y: 2, width: 2, height: 2, encoding: 0x01}];
|
|
// data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }]
|
|
const rects = [[0, 2, 0, 0], [0, 0, 0, 0]];
|
|
send_fbu_msg(info, rects, client);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
// TODO(directxman12): for encodings with subrects, test resuming on partial send?
|
|
// TODO(directxman12): test rre_chunk_sz (related to above about subrects)?
|
|
|
|
it('should handle the RRE encoding', function () {
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }];
|
|
const rect = [];
|
|
push32(rect, 2); // 2 subrects
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
push16(rect, 0); // x: 0
|
|
push16(rect, 0); // y: 0
|
|
push16(rect, 2); // width: 2
|
|
push16(rect, 2); // height: 2
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
push16(rect, 2); // x: 2
|
|
push16(rect, 2); // y: 2
|
|
push16(rect, 2); // width: 2
|
|
push16(rect, 2); // height: 2
|
|
|
|
send_fbu_msg(info, [rect], client);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
describe('the HEXTILE encoding handler', function () {
|
|
it('should handle a tile with fg, bg specified, normal subrects', function () {
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
|
|
const rect = [];
|
|
rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
rect.push(2); // 2 subrects
|
|
rect.push(0); // x: 0, y: 0
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
rect.push(2 | (2 << 4)); // x: 2, y: 2
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
send_fbu_msg(info, [rect], client);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
it('should handle a raw tile', function () {
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
|
|
const rect = [];
|
|
rect.push(0x01); // raw
|
|
for (let i = 0; i < target_data.length; i += 4) {
|
|
rect.push(target_data[i + 2]);
|
|
rect.push(target_data[i + 1]);
|
|
rect.push(target_data[i]);
|
|
rect.push(target_data[i + 3]);
|
|
}
|
|
send_fbu_msg(info, [rect], client);
|
|
expect(client._display).to.have.displayed(target_data);
|
|
});
|
|
|
|
it('should handle a tile with only bg specified (solid bg)', function () {
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
|
|
const rect = [];
|
|
rect.push(0x02);
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
send_fbu_msg(info, [rect], client);
|
|
|
|
const expected = [];
|
|
for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); }
|
|
expect(client._display).to.have.displayed(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should handle a tile with only bg specified and an empty frame afterwards', function () {
|
|
// set the width so we can have two tiles
|
|
client._fb_width = 8;
|
|
client._display.resize(8, 4);
|
|
|
|
const info = [{ x: 0, y: 0, width: 32, height: 4, encoding: 0x05 }];
|
|
|
|
const rect = [];
|
|
|
|
// send a bg frame
|
|
rect.push(0x02);
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
|
|
// send an empty frame
|
|
rect.push(0x00);
|
|
|
|
send_fbu_msg(info, [rect], client);
|
|
|
|
const expected = [];
|
|
for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid
|
|
for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color
|
|
expect(client._display).to.have.displayed(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should handle a tile with bg and coloured subrects', function () {
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
|
|
const rect = [];
|
|
rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
rect.push(2); // 2 subrects
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
rect.push(0); // x: 0, y: 0
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
rect.push(2 | (2 << 4)); // x: 2, y: 2
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
send_fbu_msg(info, [rect], client);
|
|
expect(client._display).to.have.displayed(target_data_check);
|
|
});
|
|
|
|
it('should carry over fg and bg colors from the previous tile if not specified', function () {
|
|
client._fb_width = 4;
|
|
client._fb_height = 17;
|
|
client._display.resize(4, 17);
|
|
|
|
const info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}];
|
|
const rect = [];
|
|
rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects
|
|
push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color
|
|
rect.push(0xff); // becomes ff0000ff --> #0000FF fg color
|
|
rect.push(0x00);
|
|
rect.push(0x00);
|
|
rect.push(0xff);
|
|
rect.push(8); // 8 subrects
|
|
for (let i = 0; i < 4; i++) {
|
|
rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
}
|
|
rect.push(0x08); // anysubrects
|
|
rect.push(1); // 1 subrect
|
|
rect.push(0); // x: 0, y: 0
|
|
rect.push(1 | (1 << 4)); // width: 2, height: 2
|
|
send_fbu_msg(info, [rect], client);
|
|
|
|
let expected = [];
|
|
for (let i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); }
|
|
expected = expected.concat(target_data_check_arr.slice(0, 16));
|
|
expect(client._display).to.have.displayed(new Uint8Array(expected));
|
|
});
|
|
|
|
it('should fail on an invalid subencoding', function () {
|
|
sinon.spy(client, "_fail");
|
|
const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }];
|
|
const rects = [[45]]; // an invalid subencoding
|
|
send_fbu_msg(info, rects, client);
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
it.skip('should handle the TIGHT encoding', function () {
|
|
// TODO(directxman12): test this
|
|
});
|
|
|
|
it.skip('should handle the TIGHT_PNG encoding', function () {
|
|
// TODO(directxman12): test this
|
|
});
|
|
|
|
it('should handle the DesktopSize pseduo-encoding', function () {
|
|
sinon.spy(client._display, 'resize');
|
|
send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client);
|
|
|
|
expect(client._fb_width).to.equal(20);
|
|
expect(client._fb_height).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._fb_width = 4;
|
|
client._fb_height = 4;
|
|
client._display.resize(4, 4);
|
|
sinon.spy(client._display, 'resize');
|
|
});
|
|
|
|
function make_screen_data(nr_of_screens) {
|
|
const data = [];
|
|
push8(data, nr_of_screens); // number-of-screens
|
|
push8(data, 0); // padding
|
|
push16(data, 0); // padding
|
|
for (let i=0; i<nr_of_screens; 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 reason_for_change = 1; // requested by this client
|
|
const status_code = 0; // No error
|
|
|
|
send_fbu_msg([{ x: reason_for_change, y: status_code,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
make_screen_data(1), client);
|
|
|
|
expect(client._fb_width).to.equal(20);
|
|
expect(client._fb_height).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 reason_for_change = 2; // requested by another client
|
|
const status_code = 0; // No error
|
|
|
|
send_fbu_msg([{ x: reason_for_change, y: status_code,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
make_screen_data(1), client);
|
|
|
|
expect(client._fb_width).to.equal(20);
|
|
expect(client._fb_height).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 reason_for_change = 2; // requested by another client
|
|
const status_code = 0; // No error
|
|
|
|
send_fbu_msg([{ x: reason_for_change, y: status_code,
|
|
width: 60, height: 50, encoding: -308 }],
|
|
make_screen_data(3), client);
|
|
|
|
expect(client._fb_width).to.equal(60);
|
|
expect(client._fb_height).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 reason_for_change = 1; // requested by this client
|
|
const status_code = 1; // Resize is administratively prohibited
|
|
|
|
send_fbu_msg([{ x: reason_for_change, y: status_code,
|
|
width: 20, height: 50, encoding: -308 }],
|
|
make_screen_data(1), client);
|
|
|
|
expect(client._fb_width).to.equal(4);
|
|
expect(client._fb_height).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);
|
|
|
|
send_fbu_msg([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 = [];
|
|
|
|
send_fbu_msg([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);
|
|
|
|
send_fbu_msg([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.reset();
|
|
});
|
|
|
|
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);
|
|
|
|
send_fbu_msg([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;
|
|
|
|
send_fbu_msg([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);
|
|
|
|
send_fbu_msg([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]);
|
|
}
|
|
|
|
send_fbu_msg([{ 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]);
|
|
}
|
|
|
|
send_fbu_msg([{ 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 and_mask =
|
|
[0xff, 0xff, 0xff, 0xff, //Transparent
|
|
0xff, 0xff, 0xff, 0xff, //Transparent
|
|
0x00, 0x00, 0x00, 0x00, //Opaque
|
|
0xff, 0xff, 0xff, 0xff]; //Inverted
|
|
|
|
let xor_mask =
|
|
[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 < and_mask.length; i++) {
|
|
push8(rect, and_mask[i]);
|
|
}
|
|
//XOR-mask
|
|
for (let i = 0; i < xor_mask.length; i++) {
|
|
push8(rect, xor_mask[i]);
|
|
}
|
|
|
|
let expected_rgba = [0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x33, 0x22, 0x11, 0xff,
|
|
0x00, 0x00, 0x00, 0xff];
|
|
|
|
send_fbu_msg([{ 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(expected_rgba,
|
|
hotx, hoty,
|
|
w, h);
|
|
});
|
|
|
|
it('should update the cursor when type is alpha', function () {
|
|
let data = [0xee, 0x55, 0xff, 0x00, // bgra
|
|
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 expected_rgba = [0xff, 0x55, 0xee, 0x00,
|
|
0x00, 0xff, 0x00, 0xff,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0x00, 0xff, 0x00, 0x22,
|
|
0xff, 0x00, 0x00, 0xee];
|
|
|
|
send_fbu_msg([{ 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(expected_rgba,
|
|
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();
|
|
send_fbu_msg([{ 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 () {
|
|
send_fbu_msg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100);
|
|
expect(client._FBU.rects).to.equal(0);
|
|
});
|
|
|
|
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);
|
|
|
|
send_fbu_msg([{ x: 0, y: 0, width: 0, height: 0, encoding: -307 }], [data], client);
|
|
|
|
expect(client._fb_name).to.equal('som€ nam€');
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.name).to.equal('som€ nam€');
|
|
});
|
|
});
|
|
});
|
|
|
|
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._receive_data(new Uint8Array([250, 0, 10, 1]));
|
|
expect(client._rfb_xvp_ver).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 () {
|
|
sinon.spy(client, "_fail");
|
|
client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237]));
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
it('should fire the clipboard callback with the retrieved text on ServerCutText', function () {
|
|
const expected_str = 'cheese!';
|
|
const data = [3, 0, 0, 0];
|
|
push32(data, expected_str.length);
|
|
for (let i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); }
|
|
const spy = sinon.spy();
|
|
client.addEventListener("clipboard", spy);
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array(data));
|
|
expect(spy).to.have.been.calledOnce;
|
|
expect(spy.args[0][0].detail.text).to.equal(expected_str);
|
|
});
|
|
|
|
it('should fire the bell callback on Bell', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("bell", spy);
|
|
client._sock._websocket._receive_data(new Uint8Array([2]));
|
|
expect(spy).to.have.been.calledOnce;
|
|
});
|
|
|
|
it('should respond correctly to ServerFence', function () {
|
|
const expected_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}};
|
|
const incoming_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}};
|
|
|
|
const payload = "foo\x00ab9";
|
|
|
|
// ClientFence and ServerFence are identical in structure
|
|
RFB.messages.clientFence(expected_msg, (1<<0) | (1<<1), payload);
|
|
RFB.messages.clientFence(incoming_msg, 0xffffffff, payload);
|
|
|
|
client._sock._websocket._receive_data(incoming_msg._sQ);
|
|
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
|
|
expected_msg._sQlen = 0;
|
|
incoming_msg._sQlen = 0;
|
|
|
|
RFB.messages.clientFence(expected_msg, (1<<0), payload);
|
|
RFB.messages.clientFence(incoming_msg, (1<<0) | (1<<31), payload);
|
|
|
|
client._sock._websocket._receive_data(incoming_msg._sQ);
|
|
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
});
|
|
|
|
it('should enable continuous updates on first EndOfContinousUpdates', function () {
|
|
const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}};
|
|
|
|
RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 640, 20);
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.false;
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array([150]));
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.true;
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
});
|
|
|
|
it('should disable continuous updates on subsequent EndOfContinousUpdates', function () {
|
|
client._enabledContinuousUpdates = true;
|
|
client._supportsContinuousUpdates = true;
|
|
|
|
client._sock._websocket._receive_data(new Uint8Array([150]));
|
|
|
|
expect(client._enabledContinuousUpdates).to.be.false;
|
|
});
|
|
|
|
it('should update continuous updates on resize', function () {
|
|
const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 90, 700);
|
|
|
|
client._resize(450, 160);
|
|
|
|
expect(client._sock._websocket._get_sent_data()).to.have.length(0);
|
|
|
|
client._enabledContinuousUpdates = true;
|
|
|
|
client._resize(90, 700);
|
|
|
|
expect(client._sock).to.have.sent(expected_msg._sQ);
|
|
});
|
|
|
|
it('should fail on an unknown message type', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._sock._websocket._receive_data(new Uint8Array([87]));
|
|
expect(client._fail).to.have.been.calledOnce;
|
|
});
|
|
});
|
|
|
|
describe('Asynchronous Events', function () {
|
|
let client;
|
|
beforeEach(function () {
|
|
client = make_rfb();
|
|
});
|
|
|
|
describe('Mouse event handlers', function () {
|
|
it('should not send button messages in view-only mode', function () {
|
|
client._viewOnly = true;
|
|
sinon.spy(client._sock, 'flush');
|
|
client._handleMouseButton(0, 0, 1, 0x001);
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
|
|
it('should not send movement messages in view-only mode', function () {
|
|
client._viewOnly = true;
|
|
sinon.spy(client._sock, 'flush');
|
|
client._handleMouseMove(0, 0);
|
|
expect(client._sock.flush).to.not.have.been.called;
|
|
});
|
|
|
|
it('should send a pointer event on mouse button presses', function () {
|
|
client._handleMouseButton(10, 12, 1, 0x001);
|
|
const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001);
|
|
expect(client._sock).to.have.sent(pointer_msg._sQ);
|
|
});
|
|
|
|
it('should send a mask of 1 on mousedown', function () {
|
|
client._handleMouseButton(10, 12, 1, 0x001);
|
|
const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001);
|
|
expect(client._sock).to.have.sent(pointer_msg._sQ);
|
|
});
|
|
|
|
it('should send a mask of 0 on mouseup', function () {
|
|
client._mouse_buttonMask = 0x001;
|
|
client._handleMouseButton(10, 12, 0, 0x001);
|
|
const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000);
|
|
expect(client._sock).to.have.sent(pointer_msg._sQ);
|
|
});
|
|
|
|
it('should send a pointer event on mouse movement', function () {
|
|
client._handleMouseMove(10, 12);
|
|
const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000);
|
|
expect(client._sock).to.have.sent(pointer_msg._sQ);
|
|
});
|
|
|
|
it('should set the button mask so that future mouse movements use it', function () {
|
|
client._handleMouseButton(10, 12, 1, 0x010);
|
|
client._handleMouseMove(13, 9);
|
|
const pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010);
|
|
RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010);
|
|
expect(client._sock).to.have.sent(pointer_msg._sQ);
|
|
});
|
|
});
|
|
|
|
describe('Keyboard Event Handlers', function () {
|
|
it('should send a key message on a key press', function () {
|
|
client._handleKeyEvent(0x41, 'KeyA', true);
|
|
const key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}};
|
|
RFB.messages.keyEvent(key_msg, 0x41, 1);
|
|
expect(client._sock).to.have.sent(key_msg._sQ);
|
|
});
|
|
|
|
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('WebSocket event handlers', function () {
|
|
// message events
|
|
it('should do nothing if we receive an empty message and have nothing in the queue', function () {
|
|
client._normal_msg = sinon.spy();
|
|
client._sock._websocket._receive_data(new Uint8Array([]));
|
|
expect(client._normal_msg).to.not.have.been.called;
|
|
});
|
|
|
|
it('should handle a message in the connected state as a normal message', function () {
|
|
client._normal_msg = sinon.spy();
|
|
client._sock._websocket._receive_data(new Uint8Array([1, 2, 3]));
|
|
expect(client._normal_msg).to.have.been.called;
|
|
});
|
|
|
|
it('should handle a message in any non-disconnected/failed state like an init message', function () {
|
|
client._rfb_connection_state = 'connecting';
|
|
client._rfb_init_state = 'ProtocolVersion';
|
|
client._init_msg = sinon.spy();
|
|
client._sock._websocket._receive_data(new Uint8Array([1, 2, 3]));
|
|
expect(client._init_msg).to.have.been.called;
|
|
});
|
|
|
|
it('should process all normal messages directly', function () {
|
|
const spy = sinon.spy();
|
|
client.addEventListener("bell", spy);
|
|
client._sock._websocket._receive_data(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._rfb_init_state).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._rfb_connection_state = '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._rfb_connection_state).to.equal('disconnecting');
|
|
client._sock._websocket.close = real;
|
|
client._sock._websocket.close();
|
|
expect(client._rfb_connection_state).to.equal('disconnected');
|
|
});
|
|
|
|
it('should fail if we get a close event while connecting', function () {
|
|
sinon.spy(client, "_fail");
|
|
client._rfb_connection_state = '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
|
|
});
|
|
});
|
|
});
|