diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..6846bb11 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,43 @@ +# KasmVNC Client Tests + +The page `/tests/vnc_playback.html` can be used to playback KasmVNC session recordings. The playbacks can be ran in realtime or as fast as possible for performance testing. + +## Creating new recordings + +In order to create a new recording, you will need to disable KasmVNC's built-in web server, enable the legacy VNC TCP port, and disable authentication. + +```bash +sudo apt-get install websockify +vncserver -noWebsocket -disableBasicAuth +websockify --web /usr/share/kasmvnc/www --record=/home/ubuntu/record.bin 8444 localhost:5901 +``` + +Websockify automatically adds a number to the end of the filename, so the above example might be record.bin.8. After you are finished recording, Ctrl+C the running websockify process and mv the file to the noVNC www directory. + +```bash +sudo mkdir /usr/share/kasmvnc/www/recordings +mv /home/ubuntu/record.bin.8 /usr/share/kasmvnc/www/recordings +``` + +## Playing Back Recordings + +Place recordings on the KasmVNC server in the /usr/share/kasmvnc/www/recordings directory, you may need to create this directory. Then navigate to https://server-ip:8444/tests/vnc_playback.html?data=record.bin.8 where record.bin.8 is the name of the playback file you placed in the recordings directory. + +## Kasm Provided Recordings + +The following recordings are used by Kasm Technologies to provide repeatable performance statisitics using different rendering settings. + +| Name | Description | URL| +|------|-------|----| +| newyork.1 | Default 'Static' preset mode. | https://kasm-static-content.s3.amazonaws.com/kasmvnc/playbacktests/newyork.1 | +| losangeles.1 | Default static preset mode with webp disabled | https://kasm-static-content.s3.amazonaws.com/kasmvnc/playbacktests/losangeles.1 | + + +## Historical Statistics + +This table keeps track of performance of pre-defined recordings, defined in the previous section, on static hardware that can be replicated over time to track performance improvements. + +| File | Hardware | OS | Browser | Webpacked | Result Avg | +|------|---------|----|---------|-------|---------| +| newyork.1 | Macbook M1 Pro, 32GB RAM | macOS 12.2 | Chrome 106 | False | 2446ms | +| losangeles.1 | Macbook M1 Pro, 32GB RAM | macOS 12.2 | Chrome 106 | False | 2272ms | \ No newline at end of file diff --git a/tests/playback.js b/tests/playback.js index 962307c0..e1e326e5 100644 --- a/tests/playback.js +++ b/tests/playback.js @@ -42,6 +42,24 @@ if (window.setImmediate === undefined) { }); } +class FakeWebSocket { + constructor() { + this.binaryType = "arraybuffer"; + this.protocol = ""; + this.readyState = "open"; + + this.onerror = () => {}; + this.onmessage = () => {}; + this.onopen = () => {}; + } + + send() { + } + + close() { + } +} + export default class RecordingPlayer { constructor(frames, disconnected) { this._frames = frames; @@ -63,13 +81,13 @@ export default class RecordingPlayer { run(realtime, trafficManagement) { // initialize a new RFB - this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test'); + this._ws = new FakeWebSocket(); + this._rfb = new RFB(document.getElementById('VNC_screen'), document.getElementById('noVNC_keyboardinput'), this._ws); this._rfb.viewOnly = true; this._rfb.addEventListener("disconnect", this._handleDisconnect.bind(this)); this._rfb.addEventListener("credentialsrequired", this._handleCredentials.bind(this)); - this._enablePlaybackMode(); // reset the frame index and timer this._frameIndex = 0; @@ -79,19 +97,7 @@ export default class RecordingPlayer { this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement; this._running = true; - } - - // _enablePlaybackMode mocks out things not required for running playback - _enablePlaybackMode() { - const self = this; - this._rfb._sock.send = () => {}; - this._rfb._sock.close = () => {}; - this._rfb._sock.flush = () => {}; - this._rfb._sock.open = function () { - this.init(); - this._eventHandlers.open(); - self._queueNextPacket(); - }; + this._queueNextPacket(); } _queueNextPacket() { @@ -136,7 +142,7 @@ export default class RecordingPlayer { const frame = this._frames[this._frameIndex]; - this._rfb._sock._recvMessage({'data': frame.data}); + this._ws.onmessage({'data': frame.data}); this._frameIndex++; this._queueNextPacket(); @@ -153,7 +159,7 @@ export default class RecordingPlayer { this._rfb._display.flush(); } else { this._running = false; - this._rfb._sock._eventHandlers.close({code: 1000, reason: ""}); + this._ws.onclose({code: 1000, reason: ""}); delete this._rfb; this.onfinish((new Date()).getTime() - this._startTime); } diff --git a/tests/test.inflator.js b/tests/test.inflator.js new file mode 100644 index 00000000..533bcd86 --- /dev/null +++ b/tests/test.inflator.js @@ -0,0 +1,113 @@ +/* eslint-disable no-console */ +const expect = chai.expect; + +import { deflateInit, deflate, Z_FULL_FLUSH } from "../vendor/pako/lib/zlib/deflate.js"; +import ZStream from "../vendor/pako/lib/zlib/zstream.js"; +import Inflator from "../core/inflator.js"; + +function _deflator(data) { + let strm = new ZStream(); + + deflateInit(strm, 5); + + /* eslint-disable camelcase */ + strm.input = data; + strm.avail_in = strm.input.length; + strm.next_in = 0; + /* eslint-enable camelcase */ + + let chunks = []; + let totalLen = 0; + while (strm.avail_in > 0) { + /* eslint-disable camelcase */ + strm.output = new Uint8Array(1024 * 10 * 10); + strm.avail_out = strm.output.length; + strm.next_out = 0; + /* eslint-enable camelcase */ + + let ret = deflate(strm, Z_FULL_FLUSH); + + // Check that return code is not an error + expect(ret).to.be.greaterThan(-1); + + let chunk = new Uint8Array(strm.output.buffer, 0, strm.next_out); + totalLen += chunk.length; + chunks.push(chunk); + } + + // Combine chunks into a single data + + let outData = new Uint8Array(totalLen); + let offset = 0; + + for (let i = 0; i < chunks.length; i++) { + outData.set(chunks[i], offset); + offset += chunks[i].length; + } + + return outData; +} + +describe('Inflate data', function () { + + it('should be able to inflate messages', function () { + let inflator = new Inflator(); + + let text = "123asdf"; + let preText = new Uint8Array(text.length); + for (let i = 0; i < preText.length; i++) { + preText[i] = text.charCodeAt(i); + } + + let compText = _deflator(preText); + + inflator.setInput(compText); + let inflatedText = inflator.inflate(preText.length); + + expect(inflatedText).to.array.equal(preText); + + }); + + it('should be able to inflate large messages', function () { + let inflator = new Inflator(); + + /* Generate a big string with random characters. Used because + repetition of letters might be deflated more effectively than + random ones. */ + let text = ""; + let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 300000; i++) { + text += characters.charAt(Math.floor(Math.random() * characters.length)); + } + + let preText = new Uint8Array(text.length); + for (let i = 0; i < preText.length; i++) { + preText[i] = text.charCodeAt(i); + } + + let compText = _deflator(preText); + + //Check that the compressed size is expected size + expect(compText.length).to.be.greaterThan((1024 * 10 * 10) * 2); + + inflator.setInput(compText); + let inflatedText = inflator.inflate(preText.length); + + expect(inflatedText).to.array.equal(preText); + }); + + it('should throw an error on insufficient data', function () { + let inflator = new Inflator(); + + let text = "123asdf"; + let preText = new Uint8Array(text.length); + for (let i = 0; i < preText.length; i++) { + preText[i] = text.charCodeAt(i); + } + + let compText = _deflator(preText); + + inflator.setInput(compText); + expect(() => inflator.inflate(preText.length * 2)).to.throw(); + }); +}); diff --git a/tests/test.jpeg.js b/tests/test.jpeg.js new file mode 100644 index 00000000..6834f03d --- /dev/null +++ b/tests/test.jpeg.js @@ -0,0 +1,288 @@ +const expect = chai.expect; + +import Websock from '../core/websock.js'; +import Display from '../core/display.js'; + +import JPEGDecoder from '../core/decoders/jpeg.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function testDecodeRect(decoder, x, y, width, height, data, display, depth) { + let sock; + + sock = new Websock; + sock.open("ws://example.com"); + + sock.on('message', () => { + decoder.decodeRect(x, y, width, height, sock, display, depth); + }); + + // Empty messages are filtered at multiple layers, so we need to + // do a direct call + if (data.length === 0) { + decoder.decodeRect(x, y, width, height, sock, display, depth); + } else { + sock._websocket._receiveData(new Uint8Array(data)); + } + + display.flip(); +} + +describe('JPEG Decoder', function () { + let decoder; + let display; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + beforeEach(function () { + decoder = new JPEGDecoder(); + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should handle JPEG rects', function (done) { + let data = [ + // JPEG data + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, + 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c, + 0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, + 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, + 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, + 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d, + 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10, + 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, + 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04, + 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, + 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0, + 0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03, + 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, + 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, + 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, + 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, + 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, + 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, + 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, + 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, + 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, + 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, + 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, + 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, + 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, + 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, + 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, + 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, + 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, + 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, + 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, + 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, + 0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8, + 0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d, + 0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a, + 0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd, + 0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00, + 0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, + 0xff, 0xd9, + ]; + + testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24); + + let targetData = new Uint8Array([ + 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255 + ]); + + // Browsers have rounding errors, so we need an approximate + // comparing function + function almost(a, b) { + let diff = Math.abs(a - b); + return diff < 5; + } + + display.onflush = () => { + expect(display).to.have.displayed(targetData, almost); + done(); + }; + display.flush(); + }); + + it('should handle JPEG rects without Huffman and quantification tables', function (done) { + let data1 = [ + // JPEG data + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, + 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c, + 0x01, 0x2c, 0x00, 0x42, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, + 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05, + 0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, + 0x07, 0x06, 0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, + 0x0a, 0x0b, 0x0b, 0x0d, 0x0e, 0x12, 0x10, 0x0d, + 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10, 0x16, 0x10, + 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f, + 0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, + 0x14, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x03, 0x04, + 0x04, 0x05, 0x04, 0x05, 0x09, 0x05, 0x05, 0x09, + 0x14, 0x0d, 0x0b, 0x0d, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, + 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0xff, 0xc0, + 0x00, 0x11, 0x08, 0x00, 0x04, 0x00, 0x04, 0x03, + 0x01, 0x11, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, + 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, + 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, + 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, + 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, + 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, + 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, + 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, + 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, + 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, + 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, + 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, + 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, + 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, + 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, + 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, + 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, + 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, + 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, + 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, + 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, + 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, + 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, + 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, + 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, + 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, + 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, + 0xf7, 0xfb, 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8, + 0x3f, 0xf0, 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d, + 0x7e, 0x6f, 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a, + 0x8f, 0xfe, 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd, + 0xa7, 0xff, 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00, + 0x43, 0xec, 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, + 0xff, 0xd9, + ]; + + testDecodeRect(decoder, 0, 0, 4, 4, data1, display, 24); + + let data2 = [ + // JPEG data + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, + 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x01, 0x2c, + 0x01, 0x2c, 0x00, 0x73, 0xff, 0xc0, 0x00, 0x11, + 0x08, 0x00, 0x04, 0x00, 0x04, 0x03, 0x01, 0x11, + 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, + 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, + 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xf7, 0xfb, + 0x67, 0x56, 0xff, 0x00, 0x9f, 0xf8, 0x3f, 0xf0, + 0x51, 0xa7, 0xff, 0x00, 0xf2, 0x3d, 0x7e, 0x6f, + 0xfd, 0xab, 0x94, 0x7f, 0xd0, 0x9a, 0x8f, 0xfe, + 0x0d, 0xc7, 0x7f, 0xf3, 0x61, 0xfd, 0xa7, 0xff, + 0x00, 0x10, 0x77, 0x0d, 0xff, 0x00, 0x43, 0xec, + 0xcf, 0xff, 0x00, 0x0b, 0xab, 0x1f, 0xff, 0xd9, + ]; + + testDecodeRect(decoder, 0, 0, 4, 4, data2, display, 24); + + let targetData = new Uint8Array([ + 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255, 0xff, 0x00, 0x00, 255 + ]); + + // Browsers have rounding errors, so we need an approximate + // comparing function + function almost(a, b) { + let diff = Math.abs(a - b); + return diff < 5; + } + + display.onflush = () => { + expect(display).to.have.displayed(targetData, almost); + done(); + }; + display.flush(); + }); +}); diff --git a/tests/test.ra2.js b/tests/test.ra2.js new file mode 100644 index 00000000..cc505b1f --- /dev/null +++ b/tests/test.ra2.js @@ -0,0 +1,357 @@ +const expect = chai.expect; + +import RFB from '../core/rfb.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function fakeGetRandomValues(arr) { + if (arr.length === 16) { + arr.set(new Uint8Array([ + 0x1c, 0x08, 0xfe, 0x21, 0x78, 0xef, 0x4e, 0xf9, + 0x3f, 0x05, 0xec, 0xea, 0xd4, 0x6b, 0xa5, 0xd5, + ])); + } else { + arr.set(new Uint8Array([ + 0xee, 0xe2, 0xf1, 0x5a, 0x3c, 0xa7, 0xbe, 0x95, + 0x6f, 0x2a, 0x75, 0xfd, 0x62, 0x01, 0xcb, 0xbf, + 0x43, 0x74, 0xca, 0x47, 0x4d, 0xfb, 0x0f, 0xcf, + 0x3a, 0x6d, 0x55, 0x6b, 0x59, 0x3a, 0xf6, 0x87, + 0xcb, 0x03, 0xb7, 0x28, 0x35, 0x7b, 0x15, 0x8e, + 0xb6, 0xc8, 0x8f, 0x2d, 0x5e, 0x7b, 0x1c, 0x9a, + 0x32, 0x55, 0xe7, 0x64, 0x36, 0x25, 0x7b, 0xa3, + 0xe9, 0x4f, 0x6f, 0x97, 0xdc, 0xa4, 0xd4, 0x62, + 0x6d, 0x7f, 0xab, 0x02, 0x6b, 0x13, 0x56, 0x69, + 0xfb, 0xd0, 0xd4, 0x13, 0x76, 0xcd, 0x0d, 0xd0, + 0x1f, 0xd1, 0x0c, 0x63, 0x3a, 0x34, 0x20, 0x6c, + 0xbb, 0x60, 0x45, 0x82, 0x23, 0xfd, 0x7c, 0x77, + 0x6d, 0xcc, 0x5e, 0xaa, 0xc3, 0x0c, 0x43, 0xb7, + 0x8d, 0xc0, 0x27, 0x6e, 0xeb, 0x1d, 0x6c, 0x5f, + 0xd8, 0x1c, 0x3c, 0x1c, 0x60, 0x2e, 0x82, 0x15, + 0xfd, 0x2e, 0x5f, 0x3a, 0x15, 0x53, 0x14, 0x70, + 0x4f, 0xe1, 0x65, 0x68, 0x35, 0x6d, 0xc7, 0x64, + 0xdb, 0xdd, 0x09, 0x31, 0x4f, 0x7b, 0x6d, 0x6c, + 0x77, 0x59, 0x5e, 0x1e, 0xfa, 0x4b, 0x06, 0x14, + 0xbe, 0xdc, 0x9c, 0x3d, 0x7b, 0xed, 0xf3, 0x2b, + 0x19, 0x26, 0x11, 0x8e, 0x3f, 0xab, 0x73, 0x9a, + 0x0a, 0x3a, 0xaa, 0x85, 0x06, 0xd5, 0xca, 0x3f, + 0xc3, 0xe2, 0x33, 0x7f, 0x97, 0x74, 0x98, 0x8f, + 0x2f, 0xa5, 0xfc, 0x7e, 0xb1, 0x77, 0x71, 0x58, + 0xf0, 0xbc, 0x04, 0x59, 0xbb, 0xb4, 0xc6, 0xcc, + 0x0f, 0x06, 0xcd, 0xa2, 0xd5, 0x01, 0x2f, 0xb2, + 0x22, 0x0b, 0xfc, 0x1e, 0x59, 0x9f, 0xd3, 0x4f, + 0x30, 0x95, 0xc6, 0x80, 0x0f, 0x69, 0xf3, 0x4a, + 0xd4, 0x36, 0xb6, 0x5a, 0x0b, 0x16, 0x0d, 0x81, + 0x31, 0xb0, 0x69, 0xd4, 0x4e, + ])); + } +} + +async function fakeGeneratekey() { + let key = JSON.parse('{"alg":"RSA-OAEP-256","d":"B7QR2yI8sXjo8vQhJpX9odqqR\ +6wIuPrTM1B1JJEKVeSrr7OYcc1FRJ52Vap9LIAU-ezigs9QDvWMxknB8motLnG69Wck37nt9_z4s8l\ +FQp0nROA-oaR92HW34KNL1b2fEVWGI0N86h730MvTJC5O2cmKeMezIG-oNqbbfFyP8AW-WLdDlgZm1\ +1-FjzhbVpb0Bc7nRSgBPSV-EY6Sl-LuglxDx4LaTdQW7QE_WXoRUt-GYGfTseuFQQK5WeoyX3yBtQy\ +dpauW6rrgyWdtP4hDFIoZsX6w1i-UMWMMwlIB5FdnUSi26igVGADGpV_vGMP36bv-EHp0bY-Qp0gpI\ +fLfgQ","dp":"Z1v5UceFfV2bhmbG19eGYb30jFxqoRBq36PKNY7IunMs1keYy0FpLbyGhtgMZ1Ymm\ +c8wEzGYsCPEP-ykcun_rlyu7YxmcnyC9YQqTqLyqvO-7rUqDvk9TMfdqWFP6heADRhKZmEbmcau6_m\ +2MwwK9kOkMKWvpqp8_TpJMnAH7zE","dq":"OBacRE15aY3NtCR4cvP5os3sT70JbDdDLHT3IHZM6r\ +E35CYNpLDia2chm_wnMcYvKFW9zC2ajRZ15i9c_VXQzS7ZlTaQYBFyMt7kVhxMEMFsPv1crD6t3uEI\ +j0LNuNYyy0jkon_LPZKQFK654CiL-L2YaNXOH4HbHP02dWeVQIE","e":"AQAB","ext":true,"ke\ +y_ops":["decrypt"],"kty":"RSA","n":"m1c92ZFk9ZI6l_O4YFiNxbv0Ng94SB3yThy1P_mcqr\ +GDQkRiGVdcTxAk38T9PgLztmspF-6U5TAHO-gSmmW88AC9m6f1Mspps6r7zl-M_OG-TwvGzf3BDz8z\ +Eg1FPbZV7whO1M4TCAZ0PqwG7qCc6nK1WiAhaKrSpzuPdL1igfNBsX7qu5wgw4ZTTGSLbVC_LfULQ5\ +FADgFTRXUSaxm1F8C_Lwy6a2e4nTcXilmtN2IHUjHegzm-Tq2HizmR3ARdWJpESYIW5-AXoiqj29tD\ +rqCmu2WPkB2psVp83IzZfaQNQzjNfvA8GpimkcDCkP5VMRrtKCcG4ZAFnO-A3NBX_Q","p":"2Q_lN\ +L7vCOBzAppYzCZo3WSh0hX-MOZyPUznks5U2TjmfdNZoL6_FJRiGyyLvwSiZFdEAAvpAyESFfFigng\ +AqMLSf448nPg15VUGj533CotsEM0WpoEr1JCgqdUbgDAfJQIBcwOmegBqd7lWm7uzEnRCvouB70ybk\ +JfpdprhkVE","q":"tzTt-F3g2u_3Ctj26Ho9iN_wC_W0lXGzslLt5nLmss8JqdLoDDrijjU-gjeRh\ +7lgiuHdUc3dorfFKbaMNOjoW3QKqt9oZ1JM0HKeRw0X2PnWW_0WK6DK5ASWDTXbMq2sUZqJvYEyL74\ +H2Zrt0RPAux7XQLEVgND6ROdXnMJ70O0","qi":"qfl4cXQkz4BNqa2De0-PfdU-8d1w3onnaGqx1D\ +s2fHzD_SJ4cNghn2TksoT9Qo64b3pUjH9igi2pyEjomk6D12N6FG0e10u7vFKv3W5YqUOgTpYdbcWH\ +dZ2qZWJU0XQZIrF8jLGTOO4GYP6_9sJ5R7Wk_0MdqQy8qvixWD4zLcY"}'); + key = await window.crypto.subtle.importKey("jwk", key, { + name: "RSA-OAEP", + hash: {name: "SHA-256"} + }, true, ["decrypt"]); + return {privateKey: key}; +} + +const receiveData = new Uint8Array([ + // server public key + 0x00, 0x00, 0x08, 0x00, 0xac, 0x1a, 0xbc, 0x42, + 0x8a, 0x2a, 0x69, 0x65, 0x54, 0xf8, 0x9a, 0xe6, + 0x43, 0xaa, 0xf7, 0x27, 0xf6, 0x2a, 0xf8, 0x8f, + 0x36, 0xd4, 0xae, 0x54, 0x0f, 0x16, 0x28, 0x08, + 0xc2, 0x5b, 0xca, 0x23, 0xdc, 0x27, 0x88, 0x1a, + 0x12, 0x82, 0xa8, 0x54, 0xea, 0x00, 0x99, 0x8d, + 0x02, 0x1d, 0x77, 0x4a, 0xeb, 0xd0, 0x93, 0x40, + 0x79, 0x86, 0xcb, 0x37, 0xd4, 0xb2, 0xc7, 0xcd, + 0x93, 0xe1, 0x00, 0x4d, 0x86, 0xff, 0x97, 0x33, + 0x0c, 0xad, 0x51, 0x47, 0x45, 0x85, 0x56, 0x07, + 0x65, 0x21, 0x7c, 0x57, 0x6d, 0x68, 0x7d, 0xd7, + 0x00, 0x43, 0x0c, 0x9d, 0x3b, 0xa1, 0x5a, 0x11, + 0xed, 0x51, 0x77, 0xf9, 0xd1, 0x5b, 0x33, 0xd7, + 0x1a, 0xeb, 0x65, 0x57, 0xc0, 0x01, 0x51, 0xff, + 0x9b, 0x82, 0xb3, 0xeb, 0x82, 0xc2, 0x1f, 0xca, + 0x47, 0xc0, 0x6a, 0x09, 0xe0, 0xf7, 0xda, 0x39, + 0x85, 0x12, 0xe7, 0x45, 0x8d, 0xb4, 0x1a, 0xda, + 0xcb, 0x86, 0x58, 0x52, 0x37, 0x66, 0x9d, 0x8a, + 0xce, 0xf2, 0x18, 0x78, 0x7d, 0x7f, 0xf0, 0x07, + 0x94, 0x8e, 0x6b, 0x17, 0xd9, 0x00, 0x2a, 0x3a, + 0xb9, 0xd4, 0x77, 0xde, 0x70, 0x85, 0xc4, 0x3a, + 0x62, 0x10, 0x02, 0xee, 0xba, 0xd8, 0xc0, 0x62, + 0xd0, 0x8e, 0xc1, 0x98, 0x19, 0x8e, 0x39, 0x0f, + 0x3e, 0x1d, 0x61, 0xb1, 0x93, 0x13, 0x59, 0x39, + 0xcb, 0x96, 0xf2, 0x17, 0xc9, 0xe1, 0x41, 0xd3, + 0x20, 0xdd, 0x62, 0x5e, 0x7d, 0x53, 0xd6, 0xb7, + 0x1d, 0xfe, 0x02, 0x18, 0x1f, 0xe0, 0xef, 0x3d, + 0x94, 0xe3, 0x0a, 0x9c, 0x59, 0x54, 0xd8, 0x98, + 0x16, 0x9c, 0x31, 0xda, 0x41, 0x0f, 0x2e, 0x71, + 0x68, 0xe0, 0xa2, 0x62, 0x3e, 0xe5, 0x25, 0x31, + 0xcf, 0xfc, 0x67, 0x63, 0xc3, 0xb0, 0xda, 0x3f, + 0x7b, 0x59, 0xbe, 0x7e, 0x9e, 0xa8, 0xd0, 0x01, + 0x4f, 0x43, 0x7f, 0x8d, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // server random + 0x01, 0x00, 0x5b, 0x58, 0x2a, 0x96, 0x2d, 0xbb, + 0x88, 0xec, 0xc3, 0x54, 0x00, 0xf3, 0xbb, 0xbe, + 0x17, 0xa3, 0x84, 0xd3, 0xef, 0xd8, 0x4a, 0x31, + 0x09, 0x20, 0xdd, 0xbc, 0x16, 0x9d, 0xc9, 0x5b, + 0x99, 0x62, 0x86, 0xfe, 0x0b, 0x28, 0x4b, 0xfe, + 0x5b, 0x56, 0x2d, 0xcb, 0x6e, 0x6f, 0xec, 0xf0, + 0x53, 0x0c, 0x33, 0x84, 0x93, 0xc9, 0xbf, 0x79, + 0xde, 0xb3, 0xb9, 0x29, 0x60, 0x78, 0xde, 0xe6, + 0x1d, 0xa7, 0x89, 0x48, 0x3f, 0xd1, 0x58, 0x66, + 0x27, 0x9c, 0xd4, 0x6e, 0x72, 0x9c, 0x6e, 0x4a, + 0xc0, 0x69, 0x79, 0x6f, 0x79, 0x0f, 0x13, 0xc4, + 0x20, 0xcf, 0xa6, 0xbb, 0xce, 0x18, 0x6d, 0xd5, + 0x9e, 0xd9, 0x67, 0xbe, 0x61, 0x43, 0x67, 0x11, + 0x76, 0x2f, 0xfd, 0x78, 0x75, 0x2b, 0x89, 0x35, + 0xdd, 0x0f, 0x13, 0x7f, 0xee, 0x78, 0xad, 0x32, + 0x56, 0x21, 0x81, 0x08, 0x1f, 0xcf, 0x4c, 0x29, + 0xa3, 0xeb, 0x89, 0x2d, 0xbe, 0xba, 0x8d, 0xe4, + 0x69, 0x28, 0xba, 0x53, 0x82, 0xce, 0x5c, 0xf6, + 0x5e, 0x5e, 0xa5, 0xb3, 0x88, 0xd8, 0x3d, 0xab, + 0xf4, 0x24, 0x9e, 0x3f, 0x04, 0xaf, 0xdc, 0x48, + 0x90, 0x53, 0x37, 0xe6, 0x82, 0x1d, 0xe0, 0x15, + 0x91, 0xa1, 0xc6, 0xa9, 0x54, 0xe5, 0x2a, 0xb5, + 0x64, 0x2d, 0x93, 0xc0, 0xc0, 0xe1, 0x0f, 0x6a, + 0x4b, 0xdb, 0x77, 0xf8, 0x4a, 0x0f, 0x83, 0x36, + 0xdd, 0x5e, 0x1e, 0xdd, 0x39, 0x65, 0xa2, 0x11, + 0xc2, 0xcf, 0x56, 0x1e, 0xa1, 0x29, 0xae, 0x11, + 0x9f, 0x3a, 0x82, 0xc7, 0xbd, 0x89, 0x6e, 0x59, + 0xb8, 0x59, 0x17, 0xcb, 0x65, 0xa0, 0x4b, 0x4d, + 0xbe, 0x33, 0x32, 0x85, 0x9c, 0xca, 0x5e, 0x95, + 0xc2, 0x5a, 0xd0, 0xc9, 0x8b, 0xf1, 0xf5, 0x14, + 0xcf, 0x76, 0x80, 0xc2, 0x24, 0x0a, 0x39, 0x7e, + 0x60, 0x64, 0xce, 0xd9, 0xb8, 0xad, 0x24, 0xa8, + 0xdf, 0xcb, + // server hash + 0x00, 0x14, 0x39, 0x30, 0x66, 0xb5, 0x66, 0x8a, + 0xcd, 0xb9, 0xda, 0xe0, 0xde, 0xcb, 0xf6, 0x47, + 0x5f, 0x54, 0x66, 0xe0, 0xbc, 0x49, 0x37, 0x01, + 0xf2, 0x9e, 0xef, 0xcc, 0xcd, 0x4d, 0x6c, 0x0e, + 0xc6, 0xab, 0x28, 0xd4, 0x7b, 0x13, + // subtype + 0x00, 0x01, 0x30, 0x2a, 0xc3, 0x0b, 0xc2, 0x1c, + 0xeb, 0x02, 0x44, 0x92, 0x5d, 0xfd, 0xf9, 0xa7, + 0x94, 0xd0, 0x19, +]); + +const sendData = new Uint8Array([ + // client public key + 0x00, 0x00, 0x08, 0x00, 0x9b, 0x57, 0x3d, 0xd9, + 0x91, 0x64, 0xf5, 0x92, 0x3a, 0x97, 0xf3, 0xb8, + 0x60, 0x58, 0x8d, 0xc5, 0xbb, 0xf4, 0x36, 0x0f, + 0x78, 0x48, 0x1d, 0xf2, 0x4e, 0x1c, 0xb5, 0x3f, + 0xf9, 0x9c, 0xaa, 0xb1, 0x83, 0x42, 0x44, 0x62, + 0x19, 0x57, 0x5c, 0x4f, 0x10, 0x24, 0xdf, 0xc4, + 0xfd, 0x3e, 0x02, 0xf3, 0xb6, 0x6b, 0x29, 0x17, + 0xee, 0x94, 0xe5, 0x30, 0x07, 0x3b, 0xe8, 0x12, + 0x9a, 0x65, 0xbc, 0xf0, 0x00, 0xbd, 0x9b, 0xa7, + 0xf5, 0x32, 0xca, 0x69, 0xb3, 0xaa, 0xfb, 0xce, + 0x5f, 0x8c, 0xfc, 0xe1, 0xbe, 0x4f, 0x0b, 0xc6, + 0xcd, 0xfd, 0xc1, 0x0f, 0x3f, 0x33, 0x12, 0x0d, + 0x45, 0x3d, 0xb6, 0x55, 0xef, 0x08, 0x4e, 0xd4, + 0xce, 0x13, 0x08, 0x06, 0x74, 0x3e, 0xac, 0x06, + 0xee, 0xa0, 0x9c, 0xea, 0x72, 0xb5, 0x5a, 0x20, + 0x21, 0x68, 0xaa, 0xd2, 0xa7, 0x3b, 0x8f, 0x74, + 0xbd, 0x62, 0x81, 0xf3, 0x41, 0xb1, 0x7e, 0xea, + 0xbb, 0x9c, 0x20, 0xc3, 0x86, 0x53, 0x4c, 0x64, + 0x8b, 0x6d, 0x50, 0xbf, 0x2d, 0xf5, 0x0b, 0x43, + 0x91, 0x40, 0x0e, 0x01, 0x53, 0x45, 0x75, 0x12, + 0x6b, 0x19, 0xb5, 0x17, 0xc0, 0xbf, 0x2f, 0x0c, + 0xba, 0x6b, 0x67, 0xb8, 0x9d, 0x37, 0x17, 0x8a, + 0x59, 0xad, 0x37, 0x62, 0x07, 0x52, 0x31, 0xde, + 0x83, 0x39, 0xbe, 0x4e, 0xad, 0x87, 0x8b, 0x39, + 0x91, 0xdc, 0x04, 0x5d, 0x58, 0x9a, 0x44, 0x49, + 0x82, 0x16, 0xe7, 0xe0, 0x17, 0xa2, 0x2a, 0xa3, + 0xdb, 0xdb, 0x43, 0xae, 0xa0, 0xa6, 0xbb, 0x65, + 0x8f, 0x90, 0x1d, 0xa9, 0xb1, 0x5a, 0x7c, 0xdc, + 0x8c, 0xd9, 0x7d, 0xa4, 0x0d, 0x43, 0x38, 0xcd, + 0x7e, 0xf0, 0x3c, 0x1a, 0x98, 0xa6, 0x91, 0xc0, + 0xc2, 0x90, 0xfe, 0x55, 0x31, 0x1a, 0xed, 0x28, + 0x27, 0x06, 0xe1, 0x90, 0x05, 0x9c, 0xef, 0x80, + 0xdc, 0xd0, 0x57, 0xfd, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x01, + // client random + 0x01, 0x00, 0x84, 0x7f, 0x26, 0x54, 0x74, 0xf6, + 0x47, 0xaf, 0x33, 0x64, 0x0d, 0xa6, 0xe5, 0x30, + 0xba, 0xe6, 0xe4, 0x8e, 0x50, 0x40, 0x71, 0x1c, + 0x0e, 0x06, 0x63, 0xf5, 0x07, 0x2a, 0x26, 0x68, + 0xd6, 0xcf, 0xa6, 0x80, 0x84, 0x5e, 0x64, 0xd4, + 0x5e, 0x62, 0x31, 0xfe, 0x44, 0x51, 0x0b, 0x7c, + 0x4d, 0x55, 0xc5, 0x4a, 0x7e, 0x0d, 0x4d, 0x9b, + 0x84, 0xb4, 0x32, 0x2b, 0x4d, 0x8a, 0x34, 0x8d, + 0xc8, 0xcf, 0x19, 0x3b, 0x64, 0x82, 0x27, 0x9e, + 0xa7, 0x70, 0x2a, 0xc1, 0xb8, 0xf3, 0x6a, 0x3a, + 0xf2, 0x75, 0x6e, 0x1d, 0xeb, 0xb6, 0x70, 0x7a, + 0x15, 0x18, 0x38, 0x00, 0xb4, 0x4f, 0x55, 0xb5, + 0xd8, 0x03, 0x4e, 0xb8, 0x53, 0xff, 0x80, 0x62, + 0xf1, 0x9d, 0x27, 0xe8, 0x2a, 0x3d, 0x98, 0x19, + 0x32, 0x09, 0x7e, 0x9a, 0xb0, 0xc7, 0x46, 0x23, + 0x10, 0x85, 0x35, 0x00, 0x96, 0xce, 0xb3, 0x2c, + 0x84, 0x8d, 0xf4, 0x9e, 0xa8, 0x42, 0x67, 0xed, + 0x09, 0xa6, 0x09, 0x97, 0xb3, 0x64, 0x26, 0xfb, + 0x71, 0x11, 0x9b, 0x3f, 0xbb, 0x57, 0xb8, 0x5b, + 0x2e, 0xc5, 0x2d, 0x8c, 0x5c, 0xf7, 0xef, 0x27, + 0x25, 0x88, 0x42, 0x45, 0x43, 0xa4, 0xe7, 0xde, + 0xea, 0xf9, 0x15, 0x7b, 0x5d, 0x66, 0x24, 0xce, + 0xf7, 0xc8, 0x2f, 0xc5, 0xc0, 0x3d, 0xcd, 0xf2, + 0x62, 0xfc, 0x1a, 0x5e, 0xec, 0xff, 0xf1, 0x1b, + 0xc8, 0xdb, 0xc1, 0x0f, 0x54, 0x66, 0x9e, 0xfd, + 0x99, 0x9b, 0x23, 0x70, 0x62, 0x37, 0x80, 0xad, + 0x91, 0x6b, 0x84, 0x85, 0x6a, 0x4c, 0x80, 0x9e, + 0x60, 0x8a, 0x93, 0xa3, 0xc8, 0x8e, 0xc4, 0x4b, + 0x4d, 0xb4, 0x8e, 0x3e, 0xaf, 0xce, 0xcd, 0x83, + 0xe5, 0x21, 0x90, 0x95, 0x20, 0x3c, 0x82, 0xb4, + 0x7c, 0xab, 0x63, 0x9c, 0xae, 0xc3, 0xc9, 0x71, + 0x1a, 0xec, 0x34, 0x18, 0x47, 0xec, 0x5c, 0x4d, + 0xed, 0x84, + // client hash + 0x00, 0x14, 0x9c, 0x91, 0x9e, 0x76, 0xcf, 0x1e, + 0x66, 0x87, 0x5e, 0x29, 0xf1, 0x13, 0x80, 0xea, + 0x7d, 0xec, 0xae, 0xf9, 0x60, 0x01, 0xd3, 0x6f, + 0xb7, 0x9e, 0xb2, 0xcd, 0x2d, 0xc8, 0xf8, 0x84, + 0xb2, 0x9f, 0xc3, 0x7e, 0xb4, 0xbe, + // credentials + 0x00, 0x08, 0x9d, 0xc8, 0x3a, 0xb8, 0x80, 0x4f, + 0xe3, 0x52, 0xdb, 0x62, 0x9e, 0x97, 0x64, 0x82, + 0xa8, 0xa1, 0x6b, 0x7e, 0x4d, 0x68, 0x8c, 0x29, + 0x91, 0x38, +]); + +describe('RA2 handshake', function () { + let sock; + let rfb; + let sentData; + + before(() => { + FakeWebSocket.replace(); + sinon.stub(window.crypto, "getRandomValues").callsFake(fakeGetRandomValues); + sinon.stub(window.crypto.subtle, "generateKey").callsFake(fakeGeneratekey); + }); + after(() => { + FakeWebSocket.restore(); + window.crypto.getRandomValues.restore(); + window.crypto.subtle.generateKey.restore(); + }); + + it('should fire the serververification event', function (done) { + sentData = new Uint8Array(); + rfb = new RFB(document.createElement('div'), "ws://example.com"); + sock = rfb._sock; + sock.send = (data) => { + let res = new Uint8Array(sentData.length + data.length); + res.set(sentData); + res.set(data, sentData.length); + sentData = res; + }; + rfb._rfbInitState = "Security"; + rfb._rfbVersion = 3.8; + sock._websocket._receiveData(new Uint8Array([1, 6])); + rfb.addEventListener("serververification", (e) => { + expect(e.detail.publickey).to.eql(receiveData.slice(0, 516)); + done(); + }); + sock._websocket._receiveData(receiveData); + }); + + it('should handle approveServer and fire the credentialsrequired event', function (done) { + rfb.addEventListener("credentialsrequired", (e) => { + expect(e.detail.types).to.eql(["password"]); + done(); + }); + rfb.approveServer(); + }); + + it('should match sendData after sending credentials', function (done) { + rfb.addEventListener("securityresult", (event) => { + expect(sentData.slice(1)).to.eql(sendData); + done(); + }); + rfb.sendCredentials({ "password": "123456" }); + }); +}); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 672d8c98..75d1e118 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -71,6 +71,21 @@ function deflateWithSize(data) { 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); @@ -80,6 +95,9 @@ describe('Remote Frame Buffer Protocol Client', function () { // 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(); @@ -100,6 +118,7 @@ describe('Remote Frame Buffer Protocol Client', function () { delete Websock.prototype.toString; this.clock.restore(); window.requestAnimationFrame = raf; + window.ResizeObserver = realObserver; }); let container; @@ -140,38 +159,112 @@ describe('Remote Frame Buffer Protocol Client', function () { } describe('Connecting/Disconnecting', function () { - describe('#RFB', function () { - it('should set the current state to "connecting"', function () { - const client = new RFB(document.createElement('div'), 'wss://host:8675'); - client._rfbConnectionState = ''; - this.clock.tick(); - expect(client._rfbConnectionState).to.equal('connecting'); + 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 () { - const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); - sinon.spy(client._sock, 'open'); - this.clock.tick(); - expect(client._sock.open).to.have.been.calledOnce; - expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); + 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 go to state "disconnecting" before "disconnected"', function () { - sinon.spy(client, '_updateConnectionState'); + it('should start closing WebSocket', function () { + let callback = sinon.spy(); + client.addEventListener('disconnect', callback); client.disconnect(); - expect(client._updateConnectionState).to.have.been.calledTwice; - expect(client._updateConnectionState.getCall(0).args[0]) - .to.equal('disconnecting'); - expect(client._updateConnectionState.getCall(1).args[0]) - .to.equal('disconnected'); - expect(client._rfbConnectionState).to.equal('disconnected'); + 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 () { @@ -302,6 +395,13 @@ describe('Remote Frame Buffer Protocol Client', function () { client.focus(); expect(client._canvas.focus).to.have.been.calledOnce; }); + + it('should include focus options', function () { + client._canvas.focus = sinon.spy(); + 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 () { @@ -396,6 +496,7 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('Clipping', function () { let client; + beforeEach(function () { client = makeRFB(); container.style.width = '70px'; @@ -421,9 +522,8 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + fakeResizeObserver.fire(); + clock.tick(1000); expect(client._display.viewportChangeSize).to.have.been.calledOnce; expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50); @@ -440,6 +540,10 @@ describe('Remote Frame Buffer Protocol Client', function () { 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. @@ -453,9 +557,8 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + fakeResizeObserver.fire(); + clock.tick(1000); expect(client._display.viewportChangeSize).to.not.have.been.called; }); @@ -466,13 +569,38 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + 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; @@ -618,9 +746,8 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + fakeResizeObserver.fire(); + clock.tick(1000); expect(client._display.autoscale).to.have.been.calledOnce; expect(client._display.autoscale).to.have.been.calledWith(40, 50); @@ -637,6 +764,10 @@ describe('Remote Frame Buffer Protocol Client', function () { 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); @@ -649,9 +780,8 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + fakeResizeObserver.fire(); + clock.tick(1000); expect(client._display.autoscale).to.not.have.been.called; }); @@ -681,20 +811,39 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should request a resize when initially connecting', function () { // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00 ]; + 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._supportsSetDesktopSize = false; - 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); + expect(RFB.messages.setDesktopSize).to.have.been.calledWith( + sinon.match.object, 70, 80, 0, 0); RFB.messages.setDesktopSize.resetHistory(); @@ -708,27 +857,53 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should request a resize when the container resizes', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); + 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'; - const event1 = new UIEvent('resize'); - window.dispatchEvent(event1); + fakeResizeObserver.fire(); clock.tick(400); expect(RFB.messages.setDesktopSize).to.not.have.been.called; container.style.width = '40px'; container.style.height = '50px'; - const event2 = new UIEvent('resize'); - window.dispatchEvent(event2); + fakeResizeObserver.fire(); clock.tick(400); expect(RFB.messages.setDesktopSize).to.not.have.been.called; @@ -744,8 +919,7 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); + fakeResizeObserver.fire(); clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; @@ -756,8 +930,7 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); + fakeResizeObserver.fire(); clock.tick(1000); expect(RFB.messages.setDesktopSize).to.not.have.been.called; @@ -768,84 +941,44 @@ describe('Remote Frame Buffer Protocol Client', function () { container.style.width = '40px'; container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); + 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 + // Simple ExtendedDesktopSize FBU message, new size: 100x100 const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, + 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('#_updateConnectionState', function () { - let client; - beforeEach(function () { - client = makeRFB(); - }); - - it('should clear the disconnect timer if the state is not "disconnecting"', function () { - const spy = sinon.spy(); - client._disconnTimer = setTimeout(spy, 50); - client._rfbConnectionState = 'connecting'; - client._updateConnectionState('connected'); - this.clock.tick(51); - expect(spy).to.not.have.been.called; - expect(client._disconnTimer).to.be.null; - }); - - it('should set the rfbConnectionState', function () { - client._rfbConnectionState = 'connecting'; - client._updateConnectionState('connected'); - expect(client._rfbConnectionState).to.equal('connected'); - }); - - it('should not change the state when we are disconnected', function () { - client.disconnect(); - expect(client._rfbConnectionState).to.equal('disconnected'); - client._updateConnectionState('connecting'); - expect(client._rfbConnectionState).to.not.equal('connecting'); - }); - - it('should ignore state changes to the same state', function () { - const connectSpy = sinon.spy(); - client.addEventListener("connect", connectSpy); - - expect(client._rfbConnectionState).to.equal('connected'); - client._updateConnectionState('connected'); - expect(connectSpy).to.not.have.been.called; - - client.disconnect(); - - const disconnectSpy = sinon.spy(); - client.addEventListener("disconnect", disconnectSpy); - - expect(client._rfbConnectionState).to.equal('disconnected'); - client._updateConnectionState('disconnected'); - expect(disconnectSpy).to.not.have.been.called; - }); - - it('should ignore illegal state changes', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._updateConnectionState('disconnected'); - expect(client._rfbConnectionState).to.not.equal('disconnected'); - expect(spy).to.not.have.been.called; - }); - }); - describe('#_fail', function () { let client; beforeEach(function () { @@ -886,106 +1019,6 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - describe('Connection States', function () { - describe('connecting', function () { - it('should open the websocket connection', function () { - const client = new RFB(document.createElement('div'), - 'ws://HOST:8675/PATH'); - sinon.spy(client._sock, 'open'); - this.clock.tick(); - expect(client._sock.open).to.have.been.calledOnce; - }); - }); - - describe('connected', function () { - let client; - beforeEach(function () { - client = makeRFB(); - }); - - it('should result in a connect event if state becomes connected', function () { - const spy = sinon.spy(); - client.addEventListener("connect", spy); - client._rfbConnectionState = 'connecting'; - client._updateConnectionState('connected'); - expect(spy).to.have.been.calledOnce; - }); - - it('should not result in a connect event if the state is not "connected"', function () { - const spy = sinon.spy(); - client.addEventListener("connect", spy); - client._sock._websocket.open = () => {}; // explicitly don't call onopen - client._updateConnectionState('connecting'); - expect(spy).to.not.have.been.called; - }); - }); - - describe('disconnecting', function () { - let client; - beforeEach(function () { - client = makeRFB(); - }); - - it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () { - sinon.spy(client, '_updateConnectionState'); - client._sock._websocket.close = () => {}; // explicitly don't call onclose - client._updateConnectionState('disconnecting'); - this.clock.tick(3 * 1000); - expect(client._updateConnectionState).to.have.been.calledTwice; - expect(client._rfbDisconnectReason).to.not.equal(""); - expect(client._rfbConnectionState).to.equal("disconnected"); - }); - - it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { - client._updateConnectionState('disconnecting'); - this.clock.tick(3 * 1000 / 2); - client._sock._websocket.close(); - this.clock.tick(3 * 1000 / 2 + 1); - expect(client._rfbConnectionState).to.equal('disconnected'); - }); - - it('should close the WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('disconnecting'); - expect(client._sock.close).to.have.been.calledOnce; - }); - - it('should not result in a disconnect event', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._sock._websocket.close = () => {}; // explicitly don't call onclose - client._updateConnectionState('disconnecting'); - expect(spy).to.not.have.been.called; - }); - }); - - describe('disconnected', function () { - let client; - beforeEach(function () { - client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); - }); - - it('should result in a disconnect event if state becomes "disconnected"', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._rfbConnectionState = 'disconnecting'; - client._updateConnectionState('disconnected'); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.clean).to.be.true; - }); - - it('should result in a disconnect event without msg when no reason given', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._rfbConnectionState = 'disconnecting'; - client._rfbDisconnectReason = ""; - client._updateConnectionState('disconnected'); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0].length).to.equal(1); - }); - }); - }); - describe('Protocol Initialization States', function () { let client; beforeEach(function () { @@ -993,17 +1026,21 @@ describe('Remote Frame Buffer Protocol Client', function () { client._rfbConnectionState = 'connecting'; }); - describe('ProtocolVersion', function () { - 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 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); @@ -1015,9 +1052,9 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._rfbVersion).to.equal(3.3); }); - it('should interpret version 003.889 as version 3.3', function () { + it('should interpret version 003.889 as version 3.8', function () { sendVer('003.889', client); - expect(client._rfbVersion).to.equal(3.3); + expect(client._rfbVersion).to.equal(3.8); }); it('should interpret version 003.007 as version 3.7', function () { @@ -1094,44 +1131,24 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('Security', function () { beforeEach(function () { - client._rfbInitState = 'Security'; + sendVer('003.008\n', client); + client._sock._websocket._getSentData(); }); - it('should simply receive the auth scheme when for versions < 3.7', function () { - client._rfbVersion = 3.6; - const authSchemeRaw = [1, 2, 3, 4]; - const authScheme = (authSchemeRaw[0] << 24) + (authSchemeRaw[1] << 16) + - (authSchemeRaw[2] << 8) + authSchemeRaw[3]; - client._sock._websocket._receiveData(new Uint8Array(authSchemeRaw)); - expect(client._rfbAuthScheme).to.equal(authScheme); - }); - - it('should prefer no authentication is possible', function () { - client._rfbVersion = 3.7; - const authSchemes = [2, 1, 3]; + 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._rfbAuthScheme).to.equal(1); - expect(client._sock).to.have.sent(new Uint8Array([1, 1])); + expect(client._sock).to.have.sent(new Uint8Array([30])); }); - it('should choose for the most prefered scheme possible for versions >= 3.7', function () { - client._rfbVersion = 3.7; - const authSchemes = [2, 22, 16]; - client._sock._websocket._receiveData(new Uint8Array(authSchemes)); - expect(client._rfbAuthScheme).to.equal(22); - expect(client._sock).to.have.sent(new Uint8Array([22])); - }); - - it('should fail if there are no supported schemes for versions >= 3.7', function () { + it('should fail if there are no supported schemes', function () { sinon.spy(client, "_fail"); - client._rfbVersion = 3.7; 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 for versions >= 3.7', function () { - client._rfbVersion = 3.7; + 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)); @@ -1142,7 +1159,6 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should transition to the Authentication state and continue on successful negotiation', function () { - client._rfbVersion = 3.7; const authSchemes = [1, 1]; client._negotiateAuthentication = sinon.spy(); client._sock._websocket._receiveData(new Uint8Array(authSchemes)); @@ -1151,17 +1167,8 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - describe('Authentication', function () { - beforeEach(function () { - client._rfbInitState = 'Security'; - }); - - function sendSecurity(type, cl) { - cl._sock._websocket._receiveData(new Uint8Array([1, type])); - } - + describe('Legacy Authentication', function () { it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { - client._rfbVersion = 3.6; const errMsg = "Whoopsies"; const data = [0, 0, 0, 0]; const errLen = errMsg.length; @@ -1170,37 +1177,42 @@ describe('Remote Frame Buffer Protocol Client', function () { 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 SecurityResult on "no auth" (1) for versions >= 3.8', function () { - client._rfbVersion = 3.8; + 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 transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { - client._rfbVersion = 3.7; - sendSecurity(1, client); - expect(client._rfbInitState).to.equal('ServerInitialisation'); - }); - it('should fail on an unknown auth scheme', function () { sinon.spy(client, "_fail"); - client._rfbVersion = 3.8; sendSecurity(57, client); expect(client._fail).to.have.been.calledOnce; }); describe('VNC Authentication (type 2) Handler', function () { - beforeEach(function () { - client._rfbInitState = 'Security'; - client._rfbVersion = 3.8; - }); - it('should fire the credentialsrequired event if missing a password', function () { const spy = sinon.spy(); client.addEventListener("credentialsrequired", spy); @@ -1240,12 +1252,74 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - describe('XVP Authentication (type 22) Handler', function () { - beforeEach(function () { - client._rfbInitState = 'Security'; - client._rfbVersion = 3.8; + describe('ARD Authentication (type 30) Handler', function () { + it('should fire the credentialsrequired event if all credentials are missing', function () { + const spy = sinon.spy(); + client.addEventListener("credentialsrequired", spy); + client._rfbCredentials = {}; + sendSecurity(30, client); + + expect(client._rfbCredentials).to.be.empty; + 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._rfbCredentials = { 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._rfbCredentials = { username: 'user', + password: 'password' }; + sendSecurity(30, client); + + expect(client._sock).to.have.sent([30]); + + function byteArray(length) { + return Array.from(new Uint8Array(length).keys()); + } + + let generator = [127, 255]; + let prime = byteArray(128); + let serverPrivateKey = byteArray(128); + let serverPublicKey = client._modPow(generator, serverPrivateKey, prime); + + let clientPrivateKey = byteArray(128); + let clientPublicKey = client._modPow(generator, clientPrivateKey, prime); + + let padding = Array.from(byteArray(64), byte => String.fromCharCode(65+byte%26)).join(''); + + await client._negotiateARDAuthAsync(generator, 128, prime, serverPublicKey, clientPrivateKey, padding); + + client._negotiateARDAuth(); + + expect(client._rfbInitState).to.equal('SecurityResult'); + + let expectEncrypted = new Uint8Array([ + 232, 234, 159, 162, 170, 180, 138, 104, 164, 49, 53, 96, 20, 36, 21, 15, + 217, 219, 107, 173, 196, 60, 96, 142, 215, 71, 13, 185, 185, 47, 5, 175, + 151, 30, 194, 55, 173, 214, 141, 161, 36, 138, 146, 3, 178, 89, 43, 248, + 131, 134, 205, 174, 9, 150, 171, 74, 222, 201, 20, 2, 30, 168, 162, 123, + 46, 86, 81, 221, 44, 211, 180, 247, 221, 61, 95, 155, 157, 241, 76, 76, + 49, 217, 234, 75, 147, 237, 199, 159, 93, 140, 191, 174, 52, 90, 133, 58, + 243, 81, 112, 182, 64, 62, 149, 7, 151, 28, 36, 161, 247, 247, 36, 96, + 230, 95, 58, 207, 46, 183, 100, 139, 143, 155, 224, 43, 219, 3, 71, 139]); + + let output = new Uint8Array(256); + output.set(expectEncrypted, 0); + output.set(clientPublicKey, 128); + + expect(client._sock).to.have.sent(output); + }); + }); + + describe('XVP Authentication (type 22) Handler', function () { it('should fall through to standard VNC authentication upon completion', function () { client._rfbCredentials = { username: 'user', target: 'target', @@ -1294,8 +1368,6 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('TightVNC Authentication (type 16) Handler', function () { beforeEach(function () { - client._rfbInitState = 'Security'; - client._rfbVersion = 3.8; sendSecurity(16, client); client._sock._websocket._getSentData(); // skip the security reply }); @@ -1381,8 +1453,6 @@ describe('Remote Frame Buffer Protocol Client', function () { describe('VeNCrypt Authentication (type 19) Handler', function () { beforeEach(function () { - client._rfbInitState = 'Security'; - client._rfbVersion = 3.8; sendSecurity(19, client); expect(client._sock).to.have.sent(new Uint8Array([19])); }); @@ -1393,18 +1463,70 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._fail).to.have.been.calledOnce; }); - it('should fail if the Plain authentication is not present', function () { + 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, only list subtype 1. + // Subtype list sinon.spy(client, "_fail"); - client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 0, 1])); + 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)); + }); + it('should support Plain authentication', function () { client._rfbCredentials = { username: 'username', password: 'password' }; // VeNCrypt version @@ -1476,9 +1598,30 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); + 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 () { - client._rfbInitState = 'SecurityResult'; + 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 () { @@ -1486,60 +1629,26 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._rfbInitState).to.equal('ServerInitialisation'); }); - it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { - client._rfbVersion = 3.8; - sinon.spy(client, '_fail'); - const failureData = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; - client._sock._websocket._receiveData(new Uint8Array(failureData)); - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on security result (reason: whoops)'); - }); - - it('should fail on an error code of 1 with a standard message for version < 3.8', function () { - sinon.spy(client, '_fail'); - client._rfbVersion = 3.7; - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1])); - expect(client._fail).to.have.been.calledWith( - 'Security handshake failed'); - }); - - it('should result in securityfailure event when receiving a non zero status', function () { - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2])); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.status).to.equal(2); - }); - it('should include reason when provided in securityfailure event', function () { - client._rfbVersion = 3.8; 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 () { - client._rfbVersion = 3.9; 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; }); - - it('should not include reason in securityfailure event for version < 3.8', function () { - client._rfbVersion = 3.6; - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2])); - expect(spy.args[0][0].detail.status).to.equal(2); - expect('reason' in spy.args[0][0].detail).to.be.false; - }); }); describe('ClientInitialisation', function () { @@ -1686,6 +1795,10 @@ describe('Remote Frame Buffer Protocol Client', function () { 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); @@ -1706,9 +1819,11 @@ describe('Remote Frame Buffer Protocol Client', function () { }); }); - it('should transition to the "connected" state', function () { + it('should send the "connect" event', function () { + let spy = sinon.spy(); + client.addEventListener('connect', spy); sendServerInit({}, client); - expect(client._rfbConnectionState).to.equal('connected'); + expect(spy).to.have.been.calledOnce; }); }); }); @@ -2610,27 +2725,6 @@ describe('Remote Frame Buffer Protocol Client', function () { client._canvas.dispatchEvent(ev); } - function supportsSendMouseMovementEvent() { - // Some browsers (like Safari) support the movementX / - // movementY properties of MouseEvent, but do not allow creation - // of non-trusted events with those properties. - let ev; - - ev = new MouseEvent('mousemove', - { 'movementX': 100, - 'movementY': 100 }); - return ev.movementX === 100 && ev.movementY === 100; - } - - function sendMouseMovementEvent(dx, dy) { - let ev; - - ev = new MouseEvent('mousemove', - { 'movementX': dx, - 'movementY': dy }); - client._canvas.dispatchEvent(ev); - } - function sendMouseButtonEvent(x, y, down, button) { let pos = elementToClient(x, y); let ev; @@ -2744,62 +2838,6 @@ describe('Remote Frame Buffer Protocol Client', function () { 50, 70, 0x0); }); - it('should ignore remote cursor position updates', function () { - if (!supportsSendMouseMovementEvent()) { - this.skip(); - return; - } - // Simple VMware Cursor Position FBU message with pointer coordinates - // (50, 50). - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32, - 0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ]; - client._resize(100, 100); - - const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition'); - client._sock._websocket._receiveData(new Uint8Array(incoming)); - expect(cursorSpy).to.have.been.calledOnceWith(); - cursorSpy.restore(); - - expect(client._mousePos).to.deep.equal({ }); - sendMouseMoveEvent(10, 10); - clock.tick(100); - expect(pointerEvent).to.have.been.calledOnceWith(client._sock, - 10, 10, 0x0); - }); - - it('should handle remote mouse position updates in pointer lock mode', function () { - if (!supportsSendMouseMovementEvent()) { - this.skip(); - return; - } - // Simple VMware Cursor Position FBU message with pointer coordinates - // (50, 50). - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x32, 0x00, 0x32, - 0x00, 0x00, 0x00, 0x00, 0x57, 0x4d, 0x56, 0x66 ]; - client._resize(100, 100); - - const spy = sinon.spy(); - client.addEventListener("inputlock", spy); - let stub = sinon.stub(document, 'pointerLockElement'); - stub.get(function () { return client._canvas; }); - client._handlePointerLockChange(); - stub.restore(); - client._sock._websocket._receiveData(new Uint8Array([0x02, 0x02])); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.pointer).to.be.true; - - const cursorSpy = sinon.spy(client, '_handleVMwareCursorPosition'); - client._sock._websocket._receiveData(new Uint8Array(incoming)); - expect(cursorSpy).to.have.been.calledOnceWith(); - cursorSpy.restore(); - - expect(client._mousePos).to.deep.equal({ x: 50, y: 50 }); - sendMouseMovementEvent(10, 10); - clock.tick(100); - expect(pointerEvent).to.have.been.calledOnceWith(client._sock, - 60, 60, 0x0); - }); - describe('Event Aggregation', function () { it('should send a single pointer event on mouse movement', function () { sendMouseMoveEvent(50, 70); diff --git a/tests/test.websock.js b/tests/test.websock.js index 6f35ec35..857fdca8 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -270,6 +270,14 @@ describe('Websock', function () { // it('should initialize the event handlers')? }); + describe('attaching', function () { + it('should attach to an existing websocket', function () { + let ws = new FakeWebSocket('ws://localhost:8675'); + sock.attach(ws); + expect(WebSocket).to.not.have.been.called; + }); + }); + describe('closing', function () { beforeEach(function () { sock.open('ws://localhost'); @@ -342,6 +350,93 @@ describe('Websock', function () { }); }); + describe('ready state', function () { + it('should be "unused" after construction', function () { + let sock = new Websock(); + expect(sock.readyState).to.equal('unused'); + }); + + it('should be "connecting" if WebSocket is connecting', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = WebSocket.CONNECTING; + sock.attach(ws); + expect(sock.readyState).to.equal('connecting'); + }); + + it('should be "open" if WebSocket is open', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = WebSocket.OPEN; + sock.attach(ws); + expect(sock.readyState).to.equal('open'); + }); + + it('should be "closing" if WebSocket is closing', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = WebSocket.CLOSING; + sock.attach(ws); + expect(sock.readyState).to.equal('closing'); + }); + + it('should be "closed" if WebSocket is closed', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = WebSocket.CLOSED; + sock.attach(ws); + expect(sock.readyState).to.equal('closed'); + }); + + it('should be "unknown" if WebSocket state is unknown', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 666; + sock.attach(ws); + expect(sock.readyState).to.equal('unknown'); + }); + + it('should be "connecting" if RTCDataChannel is connecting', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 'connecting'; + sock.attach(ws); + expect(sock.readyState).to.equal('connecting'); + }); + + it('should be "open" if RTCDataChannel is open', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 'open'; + sock.attach(ws); + expect(sock.readyState).to.equal('open'); + }); + + it('should be "closing" if RTCDataChannel is closing', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 'closing'; + sock.attach(ws); + expect(sock.readyState).to.equal('closing'); + }); + + it('should be "closed" if RTCDataChannel is closed', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 'closed'; + sock.attach(ws); + expect(sock.readyState).to.equal('closed'); + }); + + it('should be "unknown" if RTCDataChannel state is unknown', function () { + let sock = new Websock(); + let ws = new FakeWebSocket(); + ws.readyState = 'foobar'; + sock.attach(ws); + expect(sock.readyState).to.equal('unknown'); + }); + }); + after(function () { // eslint-disable-next-line no-global-assign WebSocket = oldWS; diff --git a/tests/test.webutil.js b/tests/test.webutil.js index 82a9cc6d..6681b3c7 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -7,6 +7,48 @@ import * as WebUtil from '../app/webutil.js'; describe('WebUtil', function () { "use strict"; + describe('config variables', function () { + it('should parse query string variables', function () { + // history.pushState() will not cause the browser to attempt loading + // the URL, this is exactly what we want here for the tests. + history.pushState({}, '', "test?myvar=myval"); + expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval"); + }); + it('should return default value when no query match', function () { + history.pushState({}, '', "test?myvar=myval"); + expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def"); + }); + it('should handle no query match and no default value', function () { + history.pushState({}, '', "test?myvar=myval"); + expect(WebUtil.getConfigVar("other")).to.be.equal(null); + }); + it('should parse fragment variables', function () { + history.pushState({}, '', "test#myvar=myval"); + expect(WebUtil.getConfigVar("myvar")).to.be.equal("myval"); + }); + it('should return default value when no fragment match', function () { + history.pushState({}, '', "test#myvar=myval"); + expect(WebUtil.getConfigVar("other", "def")).to.be.equal("def"); + }); + it('should handle no fragment match and no default value', function () { + history.pushState({}, '', "test#myvar=myval"); + expect(WebUtil.getConfigVar("other")).to.be.equal(null); + }); + it('should handle both query and fragment', function () { + history.pushState({}, '', "test?myquery=1#myhash=2"); + expect(WebUtil.getConfigVar("myquery")).to.be.equal("1"); + expect(WebUtil.getConfigVar("myhash")).to.be.equal("2"); + }); + it('should prioritize fragment if both provide same var', function () { + history.pushState({}, '', "test?myvar=1#myvar=2"); + expect(WebUtil.getConfigVar("myvar")).to.be.equal("2"); + }); + }); + + describe('cookies', function () { + // TODO + }); + describe('settings', function () { describe('localStorage', function () { diff --git a/tests/test.zrle.js b/tests/test.zrle.js new file mode 100644 index 00000000..e09d208d --- /dev/null +++ b/tests/test.zrle.js @@ -0,0 +1,124 @@ +const expect = chai.expect; + +import Websock from '../core/websock.js'; +import Display from '../core/display.js'; + +import ZRLEDecoder from '../core/decoders/zrle.js'; + +import FakeWebSocket from './fake.websocket.js'; + +function testDecodeRect(decoder, x, y, width, height, data, display, depth) { + let sock; + + sock = new Websock; + sock.open("ws://example.com"); + + sock.on('message', () => { + decoder.decodeRect(x, y, width, height, sock, display, depth); + }); + + // Empty messages are filtered at multiple layers, so we need to + // do a direct call + if (data.length === 0) { + decoder.decodeRect(x, y, width, height, sock, display, depth); + } else { + sock._websocket._receiveData(new Uint8Array(data)); + } + + display.flip(); +} + +describe('ZRLE Decoder', function () { + let decoder; + let display; + + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + beforeEach(function () { + decoder = new ZRLEDecoder(); + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should handle the Raw subencoding', function () { + testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0e, 0x78, 0x5e, 0x62, 0x60, 0x60, 0xf8, 0x4f, 0x12, 0x02, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should handle the Solid subencoding', function () { + testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x62, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff + ]); + + expect(display).to.have.displayed(targetData); + }); + + + it('should handle the Palette Tile subencoding', function () { + testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x12, 0x78, 0x5E, 0x62, 0x62, 0x60, 248, 0xff, 0x9F, 0x01, 0x08, 0x3E, 0x7C, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, + 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should handle the RLE Tile subencoding', function () { + testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x0d, 0x78, 0x5e, 0x6a, 0x60, 0x60, 0xf8, 0x2f, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should handle the RLE Palette Tile subencoding', function () { + testDecodeRect(decoder, 0, 0, 4, 4, + [0x00, 0x00, 0x00, 0x11, 0x78, 0x5e, 0x6a, 0x62, 0x60, 0xf8, 0xff, 0x9f, 0x81, 0xa1, 0x81, 0x1f, 0x00, 0x00, 0x00, 0xff, 0xff], + display, 24); + + let targetData = new Uint8Array([ + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff + ]); + + expect(display).to.have.displayed(targetData); + }); + + it('should fail on an invalid subencoding', function () { + let data = [0x00, 0x00, 0x00, 0x0c, 0x78, 0x5e, 0x6a, 0x64, 0x60, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0xff, 0xff]; + expect(() => testDecodeRect(decoder, 0, 0, 4, 4, data, display, 24)).to.throw(); + }); +}); diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index ffa69906..148e292d 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -19,6 +19,16 @@

+ +
+ + +
+
Loading