const expect = chai.expect; import RFB from '../core/rfb.js'; import Websock from '../core/websock.js'; import ZStream from "../vendor/pako/lib/zlib/zstream.js"; import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js"; import { encodings } from '../core/encodings.js'; import { toUnsigned32bit } from '../core/util/int.js'; import { encodeUTF8 } from '../core/util/strings.js'; import 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, 5); /* 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 () { 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._rfbConnectionState = "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._rfbConnectionState = "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 () { 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._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, // 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 0x00, 0x00, 0x00, 0x00, // screen id = 0 0x00, 0x00, // screen x = 0 0x00, 0x00, // screen y = 0 0x00, 0x04, // screen width = 4 0x00, 0x04, // screen height = 4 0x00, 0x00, 0x00, 0x00]; // screen flags // This property is indirectly used as a marker for the first update client._supportsSetDesktopSize = false; // 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, 0, 0); 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, 0, 0); }); 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, 0, 0); // 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, 0, 0); }); 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, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00 ]; // 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.firstCall.args[1]).to.equal(120); expect(RFB.messages.setDesktopSize.firstCall.args[2]).to.equal(130); }); }); 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 () { sinon.spy(client, "_fail"); sendVer('002.000', client); expect(client._fail).to.have.been.calledOnce; }); }); 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 () { sinon.spy(client, "_fail"); const authSchemes = [1, 32]; client._sock._websocket._receiveData(new Uint8Array(authSchemes)); expect(client._fail).to.have.been.calledOnce; }); it('should fail with the appropriate message if no types are sent', function () { const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; sinon.spy(client, '_fail'); client._sock._websocket._receiveData(new Uint8Array(failureData)); expect(client._fail).to.have.been.calledOnce; expect(client._fail).to.have.been.calledWith( 'Security negotiation failed on no security types (reason: whoops)'); }); 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(); sinon.spy(client, '_fail'); client._sock._websocket._receiveData(new Uint8Array(data)); expect(client._fail).to.have.been.calledWith( 'Security negotiation failed on authentication scheme (reason: Whoopsies)'); }); 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'); }); }); 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 () { sinon.spy(client, "_fail"); sendSecurity(57, client); expect(client._fail).to.have.been.calledOnce; }); 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 receiveData = new Uint8Array([ // server public key 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42, 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6, 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f, 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08, 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a, 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d, 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40, 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd, 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33, 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07, 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7, 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11, 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7, 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff, 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca, 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39, 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda, 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a, 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07, 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a, 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a, 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62, 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f, 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39, 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3, 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7, 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d, 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98, 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71, 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31, 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f, 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01, 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, // server random 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb, 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe, 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31, 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b, 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe, 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0, 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79, 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6, 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66, 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a, 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4, 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5, 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11, 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35, 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32, 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29, 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4, 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6, 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab, 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48, 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15, 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5, 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a, 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36, 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11, 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11, 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59, 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d, 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95, 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14, 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, 0xdf, 0xcb, // server hash 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a, 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47, 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01, 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e, 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13, // subtype 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c, 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7, 0x94, 0xd0, 0x19, ]); const sendData = new Uint8Array([ // client public key 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9, 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8, 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f, 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f, 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62, 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4, 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17, 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12, 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7, 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce, 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6, 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d, 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4, 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06, 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20, 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74, 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea, 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64, 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43, 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12, 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c, 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a, 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde, 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39, 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49, 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3, 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65, 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc, 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd, 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0, 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28, 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80, 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, // client random 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6, 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30, 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c, 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68, 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4, 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c, 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b, 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d, 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e, 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a, 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a, 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5, 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62, 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19, 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23, 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c, 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed, 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb, 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b, 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27, 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde, 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce, 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2, 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b, 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd, 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad, 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e, 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b, 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83, 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4, 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, 0xed, 0x84, // client hash 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e, 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea, 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f, 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84, 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe, // credentials 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f, 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82, 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29, 0x91, 0x38, ]); it('should fire the serververification event', async function () { let verification = new Promise((resolve, reject) => { client.addEventListener("serververification", (e) => { resolve(e.detail.publickey); }); }); client._sock._websocket._receiveData(receiveData); expect(await verification).to.deep.equal(receiveData.slice(0, 516)); }); it('should handle approveServer and fire the credentialsrequired event', 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(receiveData); await verification; client.approveServer(); 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(receiveData); await verification; client.approveServer(); await credentials; client.sendCredentials({ "password": "123456" }); clock.tick(); // FIXME: We don't have a good way to know when // the async stuff is done, so we hook in // to this internal function that is // called at the end await new Promise((resolve, reject) => { sinon.stub(client._sock._websocket, "send") .callsFake((data) => { FakeWebSocket.prototype.send.call(client._sock._websocket, data); resolve(); }); }); expect(client._sock).to.have.sent(sendData); }); }); 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 () { sinon.spy(client, "_fail"); sendNumStrPairs([[123, 'OTHR', 'SOMETHNG']], client); expect(client._fail).to.have.been.calledOnce; }); 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 () { sinon.spy(client, "_fail"); sendNumStrPairs([[0, 'TGHT', 'NOTUNNEL']], client); client._sock._websocket._getSentData(); // skip the tunnel choice here sendNumStrPairs([[23, 'stdv', 'badval__']], client); expect(client._fail).to.have.been.calledOnce; }); }); 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 () { sinon.spy(client, "_fail"); client._sock._websocket._receiveData(new Uint8Array([0, 1])); expect(client._fail).to.have.been.calledOnce; }); 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 sinon.spy(client, "_fail"); client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4])); expect(client._fail).to.have.been.calledOnce; }); 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)); sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); it('should support Plain authentication with an empty password', function () { 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)); sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); it('should support Plain authentication with a very long username and password', function () { 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)); sinon.spy(client, "_initMsg"); client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 0])); expect(client._initMsg).to.have.been.called; }); }); }); describe('Legacy SecurityResult', function () { beforeEach(function () { sendVer('003.007\n', client); client._sock._websocket._getSentData(); sendSecurity(1, client); client._sock._websocket._getSentData(); }); it('should not include reason in securityfailure event', function () { const spy = sinon.spy(); client.addEventListener("securityfailure", spy); 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 () { const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; RFB.messages.fbUpdateRequest(expectedMsg, true, 0, 0, 640, 20); client._framebufferUpdate = () => true; client._sock._websocket._receiveData(new Uint8Array([0])); expect(client._sock).to.have.sent(expectedMsg._sQ); }); it('should not send an update request if we need more data', function () { client._sock._websocket._receiveData(new Uint8Array([0])); expect(client._sock._websocket._getSentData()).to.have.length(0); }); it('should resume receiving an update if we previously did not have enough data', function () { const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; RFB.messages.fbUpdateRequest(expectedMsg, true, 0, 0, 640, 20); // 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(expectedMsg._sQ); }); 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._websocket._getSentData()).to.have.length(0); }); it('should fail on an unsupported encoding', function () { sinon.spy(client, "_fail"); const rectInfo = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; sendFbuMsg([rectInfo], [[]], client); expect(client._fail).to.have.been.calledOnce; }); 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 {}}; const incomingMsg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; const payload = "foo\x00ab9"; // ClientFence and ServerFence are identical in structure RFB.messages.clientFence(expectedMsg, (1<<0) | (1<<1), payload); RFB.messages.clientFence(incomingMsg, 0xffffffff, payload); client._sock._websocket._receiveData(incomingMsg._sQ); expect(client._sock).to.have.sent(expectedMsg._sQ); expectedMsg._sQlen = 0; incomingMsg._sQlen = 0; RFB.messages.clientFence(expectedMsg, (1<<0), payload); RFB.messages.clientFence(incomingMsg, (1<<0) | (1<<31), payload); client._sock._websocket._receiveData(incomingMsg._sQ); expect(client._sock).to.have.sent(expectedMsg._sQ); }); it('should enable continuous updates on first EndOfContinousUpdates', function () { const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; RFB.messages.enableContinuousUpdates(expectedMsg, true, 0, 0, 640, 20); expect(client._enabledContinuousUpdates).to.be.false; client._sock._websocket._receiveData(new Uint8Array([150])); expect(client._enabledContinuousUpdates).to.be.true; expect(client._sock).to.have.sent(expectedMsg._sQ); }); 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 () { const expectedMsg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; RFB.messages.enableContinuousUpdates(expectedMsg, true, 0, 0, 90, 700); client._resize(450, 160); expect(client._sock._websocket._getSentData()).to.have.length(0); client._enabledContinuousUpdates = true; client._resize(90, 700); expect(client._sock).to.have.sent(expectedMsg._sQ); }); it('should fail on an unknown message type', function () { sinon.spy(client, "_fail"); client._sock._websocket._receiveData(new Uint8Array([87])); expect(client._fail).to.have.been.calledOnce; }); }); 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 () { client._handleKeyEvent(0x41, 'KeyA', true); const keyMsg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; RFB.messages.keyEvent(keyMsg, 0x41, 1); expect(client._sock).to.have.sent(keyMsg._sQ); }); 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; before(function () { FakeWebSocket.replace(); sock = new Websock(); sock.open(); }); after(function () { FakeWebSocket.restore(); }); describe('Extended Clipboard Handling Send', function () { beforeEach(function () { sinon.spy(RFB.messages, 'clientCutText'); }); afterEach(function () { RFB.messages.clientCutText.restore(); }); it('should call clientCutText with correct Caps data', function () { let formats = { 0: 2, 2: 4121 }; let expectedData = new Uint8Array([0x1F, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x10, 0x19]); let actions = [ 1 << 24, // Caps 1 << 25, // Request 1 << 26, // Peek 1 << 27, // Notify 1 << 28 // Provide ]; RFB.messages.extendedClipboardCaps(sock, actions, formats); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); }); it('should call clientCutText with correct Request data', function () { let formats = new Uint8Array([0x01]); let expectedData = new Uint8Array([0x02, 0x00, 0x00, 0x01]); RFB.messages.extendedClipboardRequest(sock, formats); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); }); it('should call clientCutText with correct Notify data', function () { let formats = new Uint8Array([0x01]); let expectedData = new Uint8Array([0x08, 0x00, 0x00, 0x01]); RFB.messages.extendedClipboardNotify(sock, formats); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData); }); it('should call clientCutText with correct Provide data', function () { let testText = "Test string"; let expectedText = encodeUTF8(testText + "\0"); let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data let expectedData = new Uint8Array(4 + deflatedData.length); expectedData[0] = 0x10; // The client capabilities expectedData[1] = 0x00; // Reserved flags expectedData[2] = 0x00; // Reserved flags expectedData[3] = 0x01; // The formats client supports expectedData.set(deflatedData, 4); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); }); describe('End of line characters', function () { it('Carriage return', function () { let testText = "Hello\rworld\r\r!"; let expectedText = encodeUTF8("Hello\r\nworld\r\n\r\n!\0"); let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data let expectedData = new Uint8Array(4 + deflatedData.length); expectedData[0] = 0x10; // The client capabilities expectedData[1] = 0x00; // Reserved flags expectedData[2] = 0x00; // Reserved flags expectedData[3] = 0x01; // The formats client supports expectedData.set(deflatedData, 4); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); }); it('Carriage return Line feed', function () { let testText = "Hello\r\n\r\nworld\r\n!"; let expectedText = encodeUTF8(testText + "\0"); let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data let expectedData = new Uint8Array(4 + deflatedData.length); expectedData[0] = 0x10; // The client capabilities expectedData[1] = 0x00; // Reserved flags expectedData[2] = 0x00; // Reserved flags expectedData[3] = 0x01; // The formats client supports expectedData.set(deflatedData, 4); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); }); it('Line feed', function () { let testText = "Hello\n\n\nworld\n!"; let expectedText = encodeUTF8("Hello\r\n\r\n\r\nworld\r\n!\0"); let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data let expectedData = new Uint8Array(4 + deflatedData.length); expectedData[0] = 0x10; // The client capabilities expectedData[1] = 0x00; // Reserved flags expectedData[2] = 0x00; // Reserved flags expectedData[3] = 0x01; // The formats client supports expectedData.set(deflatedData, 4); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); }); it('Carriage return and Line feed mixed', function () { let testText = "\rHello\r\n\rworld\n\n!"; let expectedText = encodeUTF8("\r\nHello\r\n\r\nworld\r\n\r\n!\0"); let deflatedData = deflateWithSize(expectedText); // Build Expected with flags and deflated data let expectedData = new Uint8Array(4 + deflatedData.length); expectedData[0] = 0x10; // The client capabilities expectedData[1] = 0x00; // Reserved flags expectedData[2] = 0x00; // Reserved flags expectedData[3] = 0x01; // The formats client supports expectedData.set(deflatedData, 4); RFB.messages.extendedClipboardProvide(sock, [0x01], [testText]); expect(RFB.messages.clientCutText).to.have.been.calledOnce; expect(RFB.messages.clientCutText).to.have.been.calledWith(sock, expectedData, true); }); }); }); });