From 7d714b15f58af1c3e3dfd6b3d98855da79a46752 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 14:56:57 +0200 Subject: [PATCH 01/15] Remove special password state We already have a callback mechanism for this, so let's use that. Adds an optional parameter 'msg' to the callback. Fixes vnc_auto.html (#646) which was broken after 4e0c36dda708628836dc6f5d68fc40d05c7716d9 --- app/ui.js | 33 +++++++++++++++++++++++---------- core/rfb.js | 15 +++++---------- tests/test.rfb.js | 29 ++++++++++++++++++++--------- vnc_auto.html | 19 +++++++++++-------- 4 files changed, 59 insertions(+), 37 deletions(-) diff --git a/app/ui.js b/app/ui.js index ee0b6dbb..601abc28 100644 --- a/app/ui.js +++ b/app/ui.js @@ -339,6 +339,7 @@ var UI; try { UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), 'onUpdateState': UI.updateState, + 'onPasswordRequired': UI.passwordRequired, 'onXvpInit': UI.updateXvpButton, 'onClipboard': UI.clipboardReceive, 'onFBUComplete': UI.initialResize, @@ -373,15 +374,6 @@ var UI; case 'loaded': UI.showStatus(msg, 'normal'); break; - case 'password': - document.getElementById('noVNC_password_dlg') - .classList.add('noVNC_open'); - setTimeout(function () { - document.getElementById(('noVNC_password_input').focus()); - }, 100); - - UI.showStatus(msg, 'warn'); - break; default: UI.showStatus(msg, 'warn'); break; @@ -977,6 +969,27 @@ var UI; // Don't display the connection settings until we're actually disconnected }, +/* ------^------- + * /CONNECTION + * ============== + * PASSWORD + * ------v------*/ + + passwordRequired: function(rfb, msg) { + + document.getElementById('noVNC_password_dlg') + .classList.add('noVNC_open'); + + setTimeout(function () { + document.getElementById('noVNC_password_input').focus(); + }, 100); + + if (typeof msg === 'undefined') { + msg = "Password is required"; + } + UI.updateState(null, "warning", null, msg); + }, + setPassword: function() { UI.rfb.sendPassword(document.getElementById('noVNC_password_input').value); document.getElementById('noVNC_password_dlg') @@ -985,7 +998,7 @@ var UI; }, /* ------^------- - * /CONNECTION + * /PASSWORD * ============== * FULLSCREEN * ------v------*/ diff --git a/core/rfb.js b/core/rfb.js index 0718c89d..ea49c0cf 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -158,7 +158,7 @@ // Callback functions 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change - 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required + 'onPasswordRequired': function () { }, // onPasswordRequired(rfb, msg): VNC password is required 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received 'onBell': function () { }, // onBell(rfb): RFB Bell message received 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed @@ -269,7 +269,6 @@ sendPassword: function (passwd) { this._rfb_password = passwd; - this._rfb_state = 'Authentication'; setTimeout(this._init_msg.bind(this), 0); }, @@ -433,7 +432,6 @@ * ProtocolVersion * Security * Authentication - * password - waiting for password, not part of RFB * SecurityResult * ClientInitialization - not triggered by server message * ServerInitialization (to normal) @@ -737,9 +735,9 @@ var xvp_sep = this._xvp_password_sep; var xvp_auth = this._rfb_password.split(xvp_sep); if (xvp_auth.length < 3) { - this._updateState('password', 'XVP credentials required (user' + xvp_sep + - 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); - this._onPasswordRequired(this); + var msg = 'XVP credentials required (user' + xvp_sep + + 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password; + this._onPasswordRequired(this, msg); return false; } @@ -755,9 +753,6 @@ _negotiate_std_vnc_auth: function () { if (this._rfb_password.length === 0) { - // Notify via both callbacks since it's kind of - // an RFB state change and a UI interface issue - this._updateState('password', "Password Required"); this._onPasswordRequired(this); return false; } @@ -1326,7 +1321,7 @@ // Callback functions ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change - ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required + ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb, msg): VNC password is required ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 9ceda664..d5ceede5 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -126,10 +126,9 @@ describe('Remote Frame Buffer Protocol Client', function() { beforeEach(function () { this.clock = sinon.useFakeTimers(); }); afterEach(function () { this.clock.restore(); }); - it('should set the state to "Authentication"', function () { - client._rfb_state = "blah"; + it('should set the rfb password properly"', function () { client.sendPassword('pass'); - expect(client._rfb_state).to.equal('Authentication'); + expect(client._rfb_password).to.equal('pass'); }); it('should call init_msg "soon"', function () { @@ -728,9 +727,13 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_version = 3.8; }); - it('should transition to the "password" state if missing a password', function () { + it('should call the passwordRequired callback if missing a password', function () { + client.set_onPasswordRequired(sinon.spy()); send_security(2, client); - expect(client._rfb_state).to.equal('password'); + + var spy = client.get_onPasswordRequired(); + expect(client._rfb_password.length).to.equal(0); + expect(spy).to.have.been.calledOnce; }); it('should encrypt the password with DES and then send it back', function () { @@ -777,15 +780,23 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; }); - it('should transition to the "password" state if the passwords is missing', function() { + it('should call the passwordRequired callback if the password is missing', function() { + client.set_onPasswordRequired(sinon.spy()); + client._rfb_password = ''; send_security(22, client); - expect(client._rfb_state).to.equal('password'); + + var spy = client.get_onPasswordRequired(); + expect(client._rfb_password.length).to.equal(0); + expect(spy).to.have.been.calledOnce; }); - it('should transition to the "password" state if the passwords is improperly formatted', function() { + it('should call the passwordRequired callback if the password is improperly formatted', function() { + client.set_onPasswordRequired(sinon.spy()); client._rfb_password = 'user@target'; send_security(22, client); - expect(client._rfb_state).to.equal('password'); + + var spy = client.get_onPasswordRequired(); + expect(spy).to.have.been.calledOnce; }); it('should split the password, send the first two parts, and pass on the last part', function () { diff --git a/vnc_auto.html b/vnc_auto.html index a4cbb236..1d9cd9e8 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -102,15 +102,18 @@ UIresize(); rfb.set_onFBUComplete(function() { }); } - function passwordRequired(rfb) { - var msg; - msg = '
Date: Mon, 29 Aug 2016 14:57:51 +0200 Subject: [PATCH 02/15] Don't change state to same state The comment already stated as much, but the code was broken. --- core/rfb.js | 1 + tests/test.rfb.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/rfb.js b/core/rfb.js index ea49c0cf..f441dab4 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -442,6 +442,7 @@ if (state === oldstate) { // Already here, ignore Util.Debug("Already in state '" + state + "', ignoring"); + return; } /* diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d5ceede5..e1a11f0b 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -343,7 +343,10 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('Page States', function () { describe('loaded', function () { var client; - beforeEach(function () { client = make_rfb(); }); + beforeEach(function () { + client = make_rfb(); + client._rfb_state = 'disconnected'; + }); it('should close any open WebSocket connection', function () { sinon.spy(client._sock, 'close'); From a7127fee73a5fb8941f3076eef9eab4eaaabc998 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 14:59:28 +0200 Subject: [PATCH 03/15] Don't abuse state change function for messages This doesn't even work anymore since we fixed it to ignore changes to the current state. Add a separate callback for notifications instead. --- app/ui.js | 5 +++++ core/rfb.js | 35 +++++++++++++++++++++++++++++++++-- tests/test.rfb.js | 31 +++++++++++++++++++++++++++---- vnc_auto.html | 4 ++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/app/ui.js b/app/ui.js index 601abc28..f65e1985 100644 --- a/app/ui.js +++ b/app/ui.js @@ -338,6 +338,7 @@ var UI; initRFB: function() { try { UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), + 'onNotification': UI.notification, 'onUpdateState': UI.updateState, 'onPasswordRequired': UI.passwordRequired, 'onXvpInit': UI.updateXvpButton, @@ -486,6 +487,10 @@ var UI; document.getElementById('noVNC_status').classList.remove("noVNC_open"); }, + notification: function (rfb, msg, level, options) { + UI.showStatus(msg, level); + }, + activateControlbar: function(event) { clearTimeout(UI.idleControlbarTimeout); // We manipulate the anchor instead of the actual control diff --git a/core/rfb.js b/core/rfb.js index f441dab4..8a5d8178 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -158,6 +158,7 @@ // Callback functions 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI 'onPasswordRequired': function () { }, // onPasswordRequired(rfb, msg): VNC password is required 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received 'onBell': function () { }, // onBell(rfb): RFB Bell message received @@ -535,6 +536,33 @@ return false; }, + /* + * Send a notification to the UI. Valid levels are: + * 'normal'|'warn'|'error' + * + * NOTE: Options could be added in the future. + * NOTE: If this function is called multiple times, remember that the + * interface could be only showing the latest notification. + */ + _notification: function(msg, level, options) { + switch (level) { + case 'normal': + case 'warn': + case 'error': + Util.Debug("Notification[" + level + "]:" + msg); + break; + default: + Util.Error("Invalid notification level: " + level); + return; + } + + if (options) { + this._onNotification(this, msg, level, options); + } else { + this._onNotification(this, msg, level); + } + }, + _handle_message: function () { if (this._sock.rQlen() === 0) { Util.Warn("handle_message called on an empty receive queue"); @@ -1130,7 +1158,8 @@ switch (xvp_msg) { case 0: // XVP_FAIL - this._updateState(this._rfb_state, "Operation Failed"); + Util.Error("Operation Failed"); + this._notification("XVP Operation Failed", 'error'); break; case 1: // XVP_INIT this._rfb_xvp_ver = xvp_ver; @@ -1322,6 +1351,7 @@ // Callback functions ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb, msg): VNC password is required ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received @@ -2275,7 +2305,8 @@ msg = "Unknown reason"; break; } - Util.Info("Server did not accept the resize request: " + msg); + this._notification("Server did not accept the resize request: " + + msg, 'normal'); return true; } diff --git a/tests/test.rfb.js b/tests/test.rfb.js index e1a11f0b..d6f1a1fa 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -338,6 +338,28 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._disconnTimer).to.be.null; }); }); + + describe('#_notification', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should call the notification callback', function () { + client.set_onNotification(sinon.spy()); + client._notification('notify!', 'warn'); + var spy = client.get_onNotification(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal('notify!'); + expect(spy.args[0][2]).to.equal('warn'); + }); + + it('should not call the notification callback when level is invalid', function () { + client.set_onNotification(sinon.spy()); + client._notification('notify!', 'invalid'); + var spy = client.get_onNotification(); + expect(spy).to.not.have.been.called; + }); + }); + }); describe('Page States', function () { @@ -1730,11 +1752,12 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_height = 32; }); - it('should call updateState with a message on XVP_FAIL, but keep the same state', function () { - client._updateState = sinon.spy(); + it('should send a notification on XVP_FAIL', function () { + client.set_onNotification(sinon.spy()); client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 0])); - expect(client._updateState).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledWith('normal', 'Operation Failed'); + var spy = client.get_onNotification(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal('XVP Operation Failed'); }); it('should set the XVP version and fire the callback with the version on XVP_INIT', function () { diff --git a/vnc_auto.html b/vnc_auto.html index 1d9cd9e8..4362080d 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -161,6 +161,9 @@ s.innerHTML = msg; } } + function notification(rfb, msg, level, options) { + $D('noVNC_status').innerHTML = msg; + } window.onresize = function () { // When the window has been resized, wait until the size remains @@ -236,6 +239,7 @@ 'local_cursor': WebUtil.getConfigVar('cursor', true), 'shared': WebUtil.getConfigVar('shared', true), 'view_only': WebUtil.getConfigVar('view_only', false), + 'onNotification: notification, 'onUpdateState': updateState, 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired, From d5bbe50eb5990b6ba744e894de8fce84e487588a Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 15:00:24 +0200 Subject: [PATCH 04/15] Properly filter list of auth schemes We do _not_ support scheme 1 through 16, only 1, 2, and 16 (and 22). --- core/rfb.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 8a5d8178..7b865790 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -739,8 +739,17 @@ var types = this._sock.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); for (var i = 0; i < types.length; i++) { - if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { - this._rfb_auth_scheme = types[i]; + switch (types[i]) { + case 1: // None + case 2: // VNC Authentication + case 16: // Tight + case 22: // XVP + if (types[i] > this._rfb_auth_scheme) { + this._rfb_auth_scheme = types[i]; + } + break; + default: + break; } } From 159c50c0096908fedb8e05d21098acd3679ef550 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 15:04:11 +0200 Subject: [PATCH 05/15] Clean up special state handling for 'failed' --- core/rfb.js | 25 +++++++++++-------------- tests/test.rfb.js | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 7b865790..e9cfc325 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -446,6 +446,8 @@ return; } + this._rfb_state = state; + /* * These are disconnected states. A previous connect may * asynchronously cause a connection so make sure we are closed. @@ -467,14 +469,7 @@ Util.Warn(cmsg); } - if (oldstate === 'failed' && state === 'disconnected') { - // do disconnect action, but stay in failed state - this._rfb_state = 'failed'; - } else { - this._rfb_state = state; - } - - if (this._disconnTimer && this._rfb_state !== 'disconnect') { + if (this._disconnTimer && state !== 'disconnect') { Util.Debug("Clearing disconnect timer"); clearTimeout(this._disconnTimer); this._disconnTimer = null; @@ -512,12 +507,6 @@ } else if (oldstate === 'init') { Util.Error("Error while initializing."); } - - // Make sure we transition to disconnected - setTimeout(function () { - this._updateState('disconnected'); - }.bind(this), 50); - break; default: @@ -525,10 +514,18 @@ } if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state and + // keep the previous status message + this._rfb_state = 'failed'; this._onUpdateState(this, state, oldstate); } else { this._onUpdateState(this, state, oldstate, statusMsg); } + + // Make sure we transition to disconnected + if (state === 'failed') { + this._updateState('disconnected'); + } }, _fail: function (msg) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d6f1a1fa..34edd26c 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -677,7 +677,7 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); client._sock._websocket._receive_data(failure_data); - expect(client._fail).to.have.been.calledTwice; + expect(client._fail).to.have.been.calledOnce; expect(client._fail).to.have.been.calledWith('Security failure: whoops'); }); From c00ee156b9a612bb18ed2b6a4bc5d115f3e56914 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 26 Aug 2016 15:14:27 +0200 Subject: [PATCH 06/15] Separate out init state from page state This makes the state machine simpler as we don't have to confuse protocol states with visual states. --- core/rfb.js | 116 +++++++++++++++------------- tests/test.rfb.js | 187 +++++++++++++++++++++++----------------------- 2 files changed, 158 insertions(+), 145 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index e9cfc325..87442e60 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -35,7 +35,8 @@ this._rfb_password = ''; this._rfb_path = ''; - this._rfb_state = 'disconnected'; + this._rfb_connection_state = 'disconnected'; + this._rfb_init_state = ''; this._rfb_version = 0; this._rfb_max_version = 3.8; this._rfb_auth_scheme = ''; @@ -204,8 +205,10 @@ this._sock = new Websock(); this._sock.on('message', this._handle_message.bind(this)); this._sock.on('open', function () { - if (this._rfb_state === 'connect') { - this._updateState('ProtocolVersion', "Starting VNC handshake"); + if ((this._rfb_connection_state === 'connect') && + (this._rfb_init_state === '')) { + this._rfb_init_state = 'ProtocolVersion'; + Util.Debug("Starting VNC handshake"); } else { this._fail("Got unexpected WebSocket connection"); } @@ -220,14 +223,20 @@ } msg += ")"; } - if (this._rfb_state === 'disconnect') { - this._updateState('disconnected', 'VNC disconnected' + msg); - } else if (this._rfb_state === 'ProtocolVersion') { - this._fail('Failed to connect to server' + msg); - } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { - Util.Error("Received onclose while disconnected" + msg); - } else { - this._fail("Server disconnected" + msg); + switch (this._rfb_connection_state) { + case 'disconnect': + this._updateConnectionState('disconnected', 'VNC disconnected' + msg); + break; + case 'connect': + this._fail('Failed to connect to server' + msg); + break; + case 'failed': + case 'disconnected': + Util.Error("Received onclose while disconnected" + msg); + break; + default: + this._fail("Server disconnected" + msg); + break; } this._sock.off('close'); }.bind(this)); @@ -239,7 +248,7 @@ var rmode = this._display.get_render_mode(); Util.Info("Using native WebSockets"); - this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); + this._updateConnectionState('loaded', 'noVNC ready: native WebSockets, ' + rmode); Util.Debug("<< RFB.constructor"); }; @@ -257,12 +266,13 @@ return this._fail("Must set host and port"); } - this._updateState('connect'); + this._rfb_init_state = ''; + this._updateConnectionState('connect'); return true; }, disconnect: function () { - this._updateState('disconnect', 'Disconnecting'); + this._updateConnectionState('disconnect', 'Disconnecting'); this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); @@ -274,7 +284,7 @@ }, sendCtrlAltDel: function () { - if (this._rfb_state !== 'normal' || this._view_only) { return false; } + if (this._rfb_connection_state !== 'normal' || this._view_only) { return false; } Util.Info("Sending Ctrl-Alt-Del"); RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); @@ -308,7 +318,7 @@ // Send a key press. If 'down' is not specified then send a down key // followed by an up key. sendKey: function (code, down) { - if (this._rfb_state !== "normal" || this._view_only) { return false; } + if (this._rfb_connection_state !== "normal" || this._view_only) { return false; } if (typeof down !== 'undefined') { Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); @@ -321,14 +331,14 @@ }, clipboardPasteFrom: function (text) { - if (this._rfb_state !== 'normal') { return; } + if (this._rfb_connection_state !== 'normal') { return; } RFB.messages.clientCutText(this._sock, text); }, // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message requestDesktopSize: function (width, height) { - if (this._rfb_state !== "normal") { return; } + if (this._rfb_connection_state !== "normal") { return; } if (this._supportsSetDesktopSize) { RFB.messages.setDesktopSize(this._sock, width, height, @@ -420,25 +430,17 @@ }, /* - * Page states: + * Connection states: * loaded - page load, equivalent to disconnected * disconnected - idle state - * connect - starting to connect (to ProtocolVersion) + * connect - starting to connect * normal - connected * disconnect - starting to disconnect * failed - abnormal disconnect * fatal - failed to load page, or fatal error - * - * RFB protocol initialization states: - * ProtocolVersion - * Security - * Authentication - * SecurityResult - * ClientInitialization - not triggered by server message - * ServerInitialization (to normal) */ - _updateState: function (state, statusMsg) { - var oldstate = this._rfb_state; + _updateConnectionState: function (state, statusMsg) { + var oldstate = this._rfb_connection_state; if (state === oldstate) { // Already here, ignore @@ -446,7 +448,7 @@ return; } - this._rfb_state = state; + this._rfb_connection_state = state; /* * These are disconnected states. A previous connect may @@ -516,7 +518,7 @@ if (oldstate === 'failed' && state === 'disconnected') { // do disconnect action, but stay in failed state and // keep the previous status message - this._rfb_state = 'failed'; + this._rfb_connection_state = 'failed'; this._onUpdateState(this, state, oldstate); } else { this._onUpdateState(this, state, oldstate, statusMsg); @@ -524,12 +526,12 @@ // Make sure we transition to disconnected if (state === 'failed') { - this._updateState('disconnected'); + this._updateConnectionState('disconnected'); } }, _fail: function (msg) { - this._updateState('failed', msg); + this._updateConnectionState('failed', msg); return false; }, @@ -566,7 +568,7 @@ return; } - switch (this._rfb_state) { + switch (this._rfb_connection_state) { case 'disconnected': case 'failed': Util.Error("Got data while disconnected"); @@ -638,7 +640,7 @@ if (this._view_only) { return; } // View only, skip mouse events - if (this._rfb_state !== "normal") { return; } + if (this._rfb_connection_state !== "normal") { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, @@ -665,7 +667,7 @@ if (this._view_only) { return; } // View only, skip mouse events - if (this._rfb_state !== "normal") { return; } + if (this._rfb_connection_state !== "normal") { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, @@ -717,7 +719,9 @@ var cversion = "00" + parseInt(this._rfb_version, 10) + ".00" + ((this._rfb_version * 10) % 10); this._sock.send_string("RFB " + cversion + "\n"); - this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); + Util.Debug('Sent ProtocolVersion: ' + cversion); + + this._rfb_init_state = 'Security'; }, _negotiate_security: function () { @@ -761,7 +765,9 @@ this._rfb_auth_scheme = this._sock.rQshift32(); } - this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); + this._rfb_init_state = 'Authentication'; + Util.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); + return this._init_msg(); // jump to authentication }, @@ -798,7 +804,7 @@ var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); var response = RFB.genDES(this._rfb_password, challenge); this._sock.send(response); - this._updateState("SecurityResult"); + this._rfb_init_state = "SecurityResult"; return true; }, @@ -871,7 +877,7 @@ switch (authType) { case 'STDVNOAUTH__': // no auth - this._updateState('SecurityResult'); + this._rfb_init_state = 'SecurityResult'; return true; case 'STDVVNCAUTH_': // VNC auth this._rfb_auth_scheme = 2; @@ -895,10 +901,10 @@ case 1: // no auth if (this._rfb_version >= 3.8) { - this._updateState('SecurityResult'); + this._rfb_init_state = 'SecurityResult'; return true; } - this._updateState('ClientInitialisation', "No auth required"); + this._rfb_init_state = 'ClientInitialisation'; return this._init_msg(); case 22: // XVP auth @@ -919,7 +925,8 @@ if (this._sock.rQwait('VNC auth response ', 4)) { return false; } switch (this._sock.rQshift32()) { case 0: // OK - this._updateState('ClientInitialisation', 'Authentication OK'); + this._rfb_init_state = 'ClientInitialisation'; + Util.Debug('Authentication OK'); return this._init_msg(); case 1: // failed if (this._rfb_version >= 3.8) { @@ -1047,15 +1054,23 @@ this._timing.pixels = 0; if (this._encrypt) { - this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); + this._updateConnectionState('normal', 'Connected (encrypted) to: ' + this._fb_name); } else { - this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + this._updateConnectionState('normal', 'Connected (unencrypted) to: ' + this._fb_name); } return true; }, + /* RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization + */ _init_msg: function () { - switch (this._rfb_state) { + switch (this._rfb_init_state) { case 'ProtocolVersion': return this._negotiate_protocol_version(); @@ -1070,14 +1085,15 @@ case 'ClientInitialisation': this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation - this._updateState('ServerInitialisation', "Authentication OK"); + this._rfb_init_state = 'ServerInitialisation'; return true; case 'ServerInitialisation': return this._negotiate_server_init(); default: - return this._fail("Unknown state: " + this._rfb_state); + return this._fail("Unknown init state: " + + this._rfb_init_state); } }, @@ -1255,7 +1271,7 @@ } while (this._FBU.rects > 0) { - if (this._rfb_state !== "normal") { return false; } + if (this._rfb_connection_state !== "normal") { return false; } if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } if (this._FBU.bytes === 0) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 34edd26c..88a78c72 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -65,12 +65,12 @@ describe('Remote Frame Buffer Protocol Client', function() { }); describe('#connect', function () { - beforeEach(function () { client._updateState = sinon.spy(); }); + beforeEach(function () { client._updateConnectionState = sinon.spy(); }); it('should set the current state to "connect"', function () { client.connect('host', 8675); - expect(client._updateState).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledWith('connect'); + expect(client._updateConnectionState).to.have.been.calledOnce; + expect(client._updateConnectionState).to.have.been.calledWith('connect'); }); it('should fail if we are missing a host', function () { @@ -89,18 +89,18 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); client.connect('abc'); expect(client._fail).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledWith('failed'); + expect(client._updateConnectionState).to.have.been.calledOnce; + expect(client._updateConnectionState).to.have.been.calledWith('failed'); }); }); describe('#disconnect', function () { - beforeEach(function () { client._updateState = sinon.spy(); }); + beforeEach(function () { client._updateConnectionState = sinon.spy(); }); it('should set the current state to "disconnect"', function () { client.disconnect(); - expect(client._updateState).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledWith('disconnect'); + expect(client._updateConnectionState).to.have.been.calledOnce; + expect(client._updateConnectionState).to.have.been.calledWith('disconnect'); }); it('should unregister error event handler', function () { @@ -145,7 +145,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = "normal"; + client._rfb_connection_state = "normal"; client._view_only = false; }); @@ -163,7 +163,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should not send the keys if we are not in a normal state', function () { - client._rfb_state = "broken"; + client._rfb_connection_state = "broken"; client.sendCtrlAltDel(); expect(client._sock.flush).to.not.have.been.called; }); @@ -181,7 +181,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = "normal"; + client._rfb_connection_state = "normal"; client._view_only = false; }); @@ -201,7 +201,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should not send the key if we are not in a normal state', function () { - client._rfb_state = "broken"; + client._rfb_connection_state = "broken"; client.sendKey(123); expect(client._sock.flush).to.not.have.been.called; }); @@ -219,7 +219,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = "normal"; + client._rfb_connection_state = "normal"; client._view_only = false; }); @@ -231,7 +231,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should not send the text if we are not in a normal state', function () { - client._rfb_state = "broken"; + client._rfb_connection_state = "broken"; client.clipboardPasteFrom('abc'); expect(client._sock.flush).to.not.have.been.called; }); @@ -243,7 +243,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = "normal"; + client._rfb_connection_state = "normal"; client._view_only = false; client._supportsSetDesktopSize = true; }); @@ -273,7 +273,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should not send the request if we are not in a normal state', function () { - client._rfb_state = "broken"; + client._rfb_connection_state = "broken"; client.requestDesktopSize(1,2); expect(client._sock.flush).to.not.have.been.called; }); @@ -285,7 +285,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = "normal"; + client._rfb_connection_state = "normal"; client._view_only = false; client._rfb_xvp_ver = 1; }); @@ -318,7 +318,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); describe('Misc Internals', function () { - describe('#_updateState', function () { + describe('#_updateConnectionState', function () { var client; beforeEach(function () { this.clock = sinon.useFakeTimers(); @@ -332,7 +332,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should clear the disconnect timer if the state is not disconnect', function () { var spy = sinon.spy(); client._disconnTimer = setTimeout(spy, 50); - client._updateState('normal'); + client._updateConnectionState('normal'); this.clock.tick(51); expect(spy).to.not.have.been.called; expect(client._disconnTimer).to.be.null; @@ -362,17 +362,17 @@ describe('Remote Frame Buffer Protocol Client', function() { }); - describe('Page States', function () { + describe('Connection States', function () { describe('loaded', function () { var client; beforeEach(function () { client = make_rfb(); - client._rfb_state = 'disconnected'; + client._rfb_connection_state = 'disconnected'; }); it('should close any open WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateState('loaded'); + client._updateConnectionState('loaded'); expect(client._sock.close).to.have.been.calledOnce; }); }); @@ -383,7 +383,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should close any open WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateState('disconnected'); + client._updateConnectionState('disconnected'); expect(client._sock.close).to.have.been.calledOnce; }); }); @@ -394,27 +394,27 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should reset the variable states', function () { sinon.spy(client, '_init_vars'); - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._init_vars).to.have.been.calledOnce; }); it('should actually connect to the websocket', function () { sinon.spy(client._sock, 'open'); - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._sock.open).to.have.been.calledOnce; }); it('should use wss:// to connect if encryption is enabled', function () { sinon.spy(client._sock, 'open'); client.set_encrypt(true); - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._sock.open.args[0][0]).to.contain('wss://'); }); it('should use ws:// to connect if encryption is not enabled', function () { sinon.spy(client._sock, 'open'); client.set_encrypt(true); - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._sock.open.args[0][0]).to.contain('wss://'); }); @@ -424,13 +424,13 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_host = 'HOST'; client._rfb_port = 8675; client._rfb_path = 'PATH'; - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); }); it('should attempt to close the websocket before we open an new one', function () { sinon.spy(client._sock, 'close'); - client._updateState('connect'); + client._updateConnectionState('connect'); expect(client._sock.close).to.have.been.calledOnce; }); }); @@ -449,22 +449,22 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail if we do not call Websock.onclose within the disconnection timeout', function () { client._sock._websocket.close = function () {}; // explicitly don't call onclose - client._updateState('disconnect'); + client._updateConnectionState('disconnect'); this.clock.tick(client.get_disconnectTimeout() * 1000); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { - client._updateState('disconnect'); + client._updateConnectionState('disconnect'); this.clock.tick(client.get_disconnectTimeout() * 500); client._sock._websocket.close(); this.clock.tick(client.get_disconnectTimeout() * 500 + 1); - expect(client._rfb_state).to.equal('disconnected'); + expect(client._rfb_connection_state).to.equal('disconnected'); }); it('should close the WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateState('disconnect'); + client._updateConnectionState('disconnect'); expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect }); }); @@ -483,15 +483,15 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should close the WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateState('failed'); + client._updateConnectionState('failed'); expect(client._sock.close).to.have.been.called; }); it('should transition to disconnected but stay in failed state', function () { client.set_onUpdateState(sinon.spy()); - client._updateState('failed'); + client._updateConnectionState('failed'); this.clock.tick(50); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); var onUpdateState = client.get_onUpdateState(); expect(onUpdateState).to.have.been.called; @@ -508,12 +508,12 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should close any open WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateState('fatal'); + client._updateConnectionState('fatal'); expect(client._sock.close).to.have.been.calledOnce; }); }); - // NB(directxman12): Normal does *nothing* in updateState + // NB(directxman12): Normal does *nothing* in updateConnectionState }); describe('Protocol Initialization States', function () { @@ -595,7 +595,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail on an invalid version', function () { send_ver('002.000', client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); @@ -633,7 +633,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should transition to the Security state on successful negotiation', function () { send_ver('003.008', client); - expect(client._rfb_state).to.equal('Security'); + expect(client._rfb_init_state).to.equal('Security'); }); }); @@ -644,7 +644,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'Security'; + client._rfb_init_state = 'Security'; }); it('should simply receive the auth scheme when for versions < 3.7', function () { @@ -668,7 +668,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_version = 3.7; var auth_schemes = [1, 32]; client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { @@ -686,7 +686,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var auth_schemes = [1, 1]; client._negotiate_authentication = sinon.spy(); client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_state).to.equal('Authentication'); + expect(client._rfb_init_state).to.equal('Authentication'); expect(client._negotiate_authentication).to.have.been.calledOnce; }); }); @@ -698,7 +698,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'Security'; + client._rfb_init_state = 'Security'; }); function send_security(type, cl) { @@ -717,28 +717,26 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); client._sock._websocket._receive_data(new Uint8Array(data)); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies'); }); it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { client._rfb_version = 3.8; send_security(1, client); - expect(client._rfb_state).to.equal('SecurityResult'); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); - it('should transition straight to ClientInitialisation on "no auth" for versions < 3.8', function () { + it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { client._rfb_version = 3.7; - sinon.spy(client, '_updateState'); send_security(1, client); - expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); - expect(client._rfb_state).to.equal('ServerInitialisation'); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); }); it('should fail on an unknown auth scheme', function () { client._rfb_version = 3.8; send_security(57, client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); describe('VNC Authentication (type 2) Handler', function () { @@ -748,7 +746,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'Security'; + client._rfb_init_state = 'Security'; client._rfb_version = 3.8; }); @@ -782,7 +780,7 @@ describe('Remote Frame Buffer Protocol Client', function() { for (var i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receive_data(new Uint8Array(challenge)); - expect(client._rfb_state).to.equal('SecurityResult'); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); }); @@ -793,7 +791,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'Security'; + client._rfb_init_state = 'Security'; client._rfb_version = 3.8; }); @@ -847,7 +845,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'Security'; + client._rfb_init_state = 'Security'; client._rfb_version = 3.8; send_security(16, client); client._sock._websocket._get_sent_data(); // skip the security reply @@ -879,7 +877,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail if no supported tunnels are listed', function () { send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); it('should choose the notunnel tunnel type', function () { @@ -892,7 +890,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._get_sent_data(); // skip the tunnel choice here send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_state).to.equal('SecurityResult'); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); /*it('should attempt to use VNC auth over no auth when possible', function () { @@ -908,7 +906,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_tightvnc = true; send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_state).to.equal('SecurityResult'); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); it('should accept VNC authentication and transition to that', function () { @@ -923,7 +921,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail if there are no supported auth types', function () { client._rfb_tightvnc = true; send_num_str_pairs([[23, 'stdv', 'badval__']], client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); }); @@ -935,14 +933,13 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'SecurityResult'; + client._rfb_init_state = 'SecurityResult'; }); - it('should fall through to ClientInitialisation on a response code of 0', function () { - client._updateState = sinon.spy(); + it('should fall through to ServerInitialisation on a response code of 0', function () { + client._updateConnectionState = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._updateState).to.have.been.calledOnce; - expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); }); it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { @@ -950,14 +947,14 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; client._sock._websocket._receive_data(new Uint8Array(failure_data)); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); expect(client._fail).to.have.been.calledWith('whoops'); }); it('should fail on an error code of 1 with a standard message for version < 3.8', function () { client._rfb_version = 3.7; client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); @@ -968,12 +965,12 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'SecurityResult'; + client._rfb_init_state = 'SecurityResult'; }); it('should transition to the ServerInitialisation state', function () { client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._rfb_state).to.equal('ServerInitialisation'); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); }); it('should send 1 if we are in shared mode', function () { @@ -996,7 +993,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'ServerInitialisation'; + client._rfb_init_state = 'ServerInitialisation'; }); function send_server_init(opts, client) { @@ -1072,7 +1069,7 @@ describe('Remote Frame Buffer Protocol Client', function() { } client._sock._websocket._receive_data(tight_data); - expect(client._rfb_state).to.equal('normal'); + expect(client._rfb_connection_state).to.equal('normal'); }); it('should set the true color mode on the display to the configuration variable', function () { @@ -1136,7 +1133,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should transition to the "normal" state', function () { send_server_init({}, client); - expect(client._rfb_state).to.equal('normal'); + expect(client._rfb_connection_state).to.equal('normal'); }); }); }); @@ -1148,7 +1145,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; @@ -1161,7 +1158,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; @@ -1322,7 +1319,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client.set_onFBUReceive(sinon.spy()); var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; send_fbu_msg([rect_info], [[]], client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); it('should be able to pause and resume receiving rects if not enought data', function () { @@ -1351,7 +1348,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; // a really small frame client._fb_width = 4; @@ -1428,7 +1425,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; // a really small frame client._fb_width = 4; @@ -1569,7 +1566,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rects = [[45]]; // an invalid subencoding send_fbu_msg(info, rects, client); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); @@ -1604,7 +1601,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; client._supportsSetDesktopSize = false; // a really small frame @@ -1746,7 +1743,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._fb_name = 'some device'; client._fb_width = 27; client._fb_height = 32; @@ -1770,7 +1767,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail on unknown XVP message types', function () { client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237])); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); @@ -1863,7 +1860,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail on an unknown message type', function () { client._sock._websocket._receive_data(new Uint8Array([87])); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); }); @@ -1876,7 +1873,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; }); it('should not send button messages in view-only mode', function () { @@ -2017,7 +2014,7 @@ describe('Remote Frame Buffer Protocol Client', function() { // message events it ('should do nothing if we receive an empty message and have nothing in the queue', function () { client.connect('host', 8675); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._normal_msg = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([])); expect(client._normal_msg).to.not.have.been.called; @@ -2025,7 +2022,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should handle a message in the normal state as a normal message', function () { client.connect('host', 8675); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._normal_msg = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); expect(client._normal_msg).to.have.been.calledOnce; @@ -2033,7 +2030,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should handle a message in any non-disconnected/failed state like an init message', function () { client.connect('host', 8675); - client._rfb_state = 'ProtocolVersion'; + client._rfb_init_state = 'ProtocolVersion'; client._init_msg = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); expect(client._init_msg).to.have.been.calledOnce; @@ -2042,7 +2039,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should split up the handling of muplitle normal messages across 10ms intervals', function () { client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client.set_onBell(sinon.spy()); client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); expect(client.get_onBell()).to.have.been.calledOnce; @@ -2054,35 +2051,35 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should update the state to ProtocolVersion on open (if the state is "connect")', function () { client.connect('host', 8675); client._sock._websocket._open(); - expect(client._rfb_state).to.equal('ProtocolVersion'); + expect(client._rfb_init_state).to.equal('ProtocolVersion'); }); it('should fail if we are not currently ready to connect and we get an "open" event', function () { client.connect('host', 8675); - client._rfb_state = 'some_other_state'; + client._rfb_connection_state = 'some_other_state'; client._sock._websocket._open(); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); // close events it('should transition to "disconnected" from "disconnect" on a close event', function () { client.connect('host', 8675); - client._rfb_state = 'disconnect'; + client._rfb_connection_state = 'disconnect'; client._sock._websocket.close(); - expect(client._rfb_state).to.equal('disconnected'); + expect(client._rfb_connection_state).to.equal('disconnected'); }); it('should transition to failed if we get a close event from any non-"disconnection" state', function () { client.connect('host', 8675); - client._rfb_state = 'normal'; + client._rfb_connection_state = 'normal'; client._sock._websocket.close(); - expect(client._rfb_state).to.equal('failed'); + expect(client._rfb_connection_state).to.equal('failed'); }); it('should unregister close event handler', function () { sinon.spy(client._sock, 'off'); client.connect('host', 8675); - client._rfb_state = 'disconnect'; + client._rfb_connection_state = 'disconnect'; client._sock._websocket.close(); expect(client._sock.off).to.have.been.calledWith('close'); }); From 52e9cc48b0118cbe3f9e6ab73a90325fd658ea9b Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 2 Sep 2016 12:03:16 +0200 Subject: [PATCH 07/15] Clean up log output on state changes --- core/rfb.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index 87442e60..1a15831f 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -463,14 +463,18 @@ Util.Error('Fatal error, cannot continue'); } - var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; - var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; - if (state === 'failed' || state === 'fatal') { - Util.Error(cmsg); - } else { - Util.Warn(cmsg); + if (typeof(statusMsg) !== 'undefined') { + var cmsg = " Msg: " + statusMsg; + if (state === 'failed' || state === 'fatal') { + Util.Error(cmsg); + } else { + Util.Warn(cmsg); + } } + var smsg = "New state '" + state + "', was '" + oldstate + "'."; + Util.Debug(smsg); + if (this._disconnTimer && state !== 'disconnect') { Util.Debug("Clearing disconnect timer"); clearTimeout(this._disconnTimer); From c2a4d3ef86f966580c661272d772a708f0da003d Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 16 Sep 2016 12:12:10 +0200 Subject: [PATCH 08/15] Improve the connection state names Names such as 'disconnect' and 'disconnected' are inconsistent in the way that one describes an action and the other a state. The state that was called 'normal' didn't fit in with the others because the other names describe a connection state. The new names are: 'disconnecting', 'connecting' and 'connected' --- app/ui.js | 10 +++--- core/rfb.js | 60 ++++++++++++++++----------------- tests/test.rfb.js | 84 +++++++++++++++++++++++------------------------ vnc_auto.html | 4 +-- 4 files changed, 79 insertions(+), 79 deletions(-) diff --git a/app/ui.js b/app/ui.js index f65e1985..7251a4c6 100644 --- a/app/ui.js +++ b/app/ui.js @@ -369,7 +369,7 @@ var UI; // zero means no timeout UI.showStatus(msg, 'error', 0); break; - case 'normal': + case 'connected': /* falls through */ case 'disconnected': case 'loaded': @@ -386,7 +386,7 @@ var UI; // Disable/enable controls depending on connection state updateVisualState: function() { - var connected = UI.rfb && UI.rfb_state === 'normal'; + var connected = UI.rfb && UI.rfb_state === 'connected'; //Util.Debug(">> updateVisualState"); document.getElementById('noVNC_setting_encrypt').disabled = connected; @@ -1062,7 +1062,7 @@ var UI; var screen = UI.screenSize(); - if (screen && UI.rfb_state === 'normal' && UI.rfb.get_display()) { + if (screen && UI.rfb_state === 'connected' && UI.rfb.get_display()) { var display = UI.rfb.get_display(); var resizeMode = UI.getSetting('resize'); @@ -1187,7 +1187,7 @@ var UI; // Handle special cases where clipping is forced on/off or locked enableDisableViewClip: function() { var resizeSetting = document.getElementById('noVNC_setting_resize'); - var connected = UI.rfb && UI.rfb_state === 'normal'; + var connected = UI.rfb && UI.rfb_state === 'connected'; if (UI.isSafari) { // Safari auto-hides the scrollbars which makes them @@ -1243,7 +1243,7 @@ var UI; updateViewDrag: function() { var clipping = false; - if (UI.rfb_state !== 'normal') return; + if (UI.rfb_state !== 'connected') return; // Check if viewport drag is possible. It is only possible // if the remote display is clipping the client display. diff --git a/core/rfb.js b/core/rfb.js index 1a15831f..d0341931 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -205,7 +205,7 @@ this._sock = new Websock(); this._sock.on('message', this._handle_message.bind(this)); this._sock.on('open', function () { - if ((this._rfb_connection_state === 'connect') && + if ((this._rfb_connection_state === 'connecting') && (this._rfb_init_state === '')) { this._rfb_init_state = 'ProtocolVersion'; Util.Debug("Starting VNC handshake"); @@ -224,10 +224,10 @@ msg += ")"; } switch (this._rfb_connection_state) { - case 'disconnect': + case 'disconnecting': this._updateConnectionState('disconnected', 'VNC disconnected' + msg); break; - case 'connect': + case 'connecting': this._fail('Failed to connect to server' + msg); break; case 'failed': @@ -267,12 +267,12 @@ } this._rfb_init_state = ''; - this._updateConnectionState('connect'); + this._updateConnectionState('connecting'); return true; }, disconnect: function () { - this._updateConnectionState('disconnect', 'Disconnecting'); + this._updateConnectionState('disconnecting', 'Disconnecting'); this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); @@ -284,7 +284,7 @@ }, sendCtrlAltDel: function () { - if (this._rfb_connection_state !== 'normal' || this._view_only) { return false; } + if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } Util.Info("Sending Ctrl-Alt-Del"); RFB.messages.keyEvent(this._sock, KeyTable.XK_Control_L, 1); @@ -318,7 +318,7 @@ // Send a key press. If 'down' is not specified then send a down key // followed by an up key. sendKey: function (code, down) { - if (this._rfb_connection_state !== "normal" || this._view_only) { return false; } + if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } if (typeof down !== 'undefined') { Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); @@ -331,14 +331,14 @@ }, clipboardPasteFrom: function (text) { - if (this._rfb_connection_state !== 'normal') { return; } + if (this._rfb_connection_state !== 'connected') { return; } RFB.messages.clientCutText(this._sock, text); }, // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message requestDesktopSize: function (width, height) { - if (this._rfb_connection_state !== "normal") { return; } + if (this._rfb_connection_state !== 'connected') { return; } if (this._supportsSetDesktopSize) { RFB.messages.setDesktopSize(this._sock, width, height, @@ -416,7 +416,7 @@ if (this._display && this._display.get_context()) { this._keyboard.ungrab(); this._mouse.ungrab(); - if (state !== 'connect' && state !== 'loaded') { + if (state !== 'connecting' && state !== 'loaded') { this._display.defaultCursor(); } if (Util.get_logging() !== 'debug' || state === 'loaded') { @@ -431,13 +431,13 @@ /* * Connection states: - * loaded - page load, equivalent to disconnected + * loaded - page load, equivalent to disconnected * disconnected - idle state - * connect - starting to connect - * normal - connected - * disconnect - starting to disconnect - * failed - abnormal disconnect - * fatal - failed to load page, or fatal error + * connecting + * connected + * disconnecting + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error */ _updateConnectionState: function (state, statusMsg) { var oldstate = this._rfb_connection_state; @@ -454,8 +454,8 @@ * These are disconnected states. A previous connect may * asynchronously cause a connection so make sure we are closed. */ - if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, - 'disconnect': 1, 'failed': 1, 'fatal': 1}) { + if (state in {'disconnected': 1, 'loaded': 1, 'connecting': 1, + 'disconnecting': 1, 'failed': 1, 'fatal': 1}) { this._cleanupSocket(state); } @@ -475,7 +475,7 @@ var smsg = "New state '" + state + "', was '" + oldstate + "'."; Util.Debug(smsg); - if (this._disconnTimer && state !== 'disconnect') { + if (this._disconnTimer && state !== 'disconnecting') { Util.Debug("Clearing disconnect timer"); clearTimeout(this._disconnTimer); this._disconnTimer = null; @@ -483,19 +483,19 @@ } switch (state) { - case 'normal': + case 'connected': if (oldstate === 'disconnected' || oldstate === 'failed') { - Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'connected'"); } break; - case 'connect': + case 'connecting': this._init_vars(); this._connect(); // WebSocket.onopen transitions to 'ProtocolVersion' break; - case 'disconnect': + case 'disconnecting': this._disconnTimer = setTimeout(function () { this._fail("Disconnect timeout"); }.bind(this), this._disconnectTimeout * 1000); @@ -508,7 +508,7 @@ case 'failed': if (oldstate === 'disconnected') { Util.Error("Invalid transition from 'disconnected' to 'failed'"); - } else if (oldstate === 'normal') { + } else if (oldstate === 'connected') { Util.Error("Error while connected."); } else if (oldstate === 'init') { Util.Error("Error while initializing."); @@ -577,7 +577,7 @@ case 'failed': Util.Error("Got data while disconnected"); break; - case 'normal': + case 'connected': if (this._normal_msg() && this._sock.rQlen() > 0) { // true means we can continue processing // Give other events a chance to run @@ -644,7 +644,7 @@ if (this._view_only) { return; } // View only, skip mouse events - if (this._rfb_connection_state !== "normal") { return; } + if (this._rfb_connection_state !== 'connected') { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, @@ -671,7 +671,7 @@ if (this._view_only) { return; } // View only, skip mouse events - if (this._rfb_connection_state !== "normal") { return; } + if (this._rfb_connection_state !== 'connected') { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, @@ -1058,9 +1058,9 @@ this._timing.pixels = 0; if (this._encrypt) { - this._updateConnectionState('normal', 'Connected (encrypted) to: ' + this._fb_name); + this._updateConnectionState('connected', 'Connected (encrypted) to: ' + this._fb_name); } else { - this._updateConnectionState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + this._updateConnectionState('connected', 'Connected (unencrypted) to: ' + this._fb_name); } return true; }, @@ -1275,7 +1275,7 @@ } while (this._FBU.rects > 0) { - if (this._rfb_connection_state !== "normal") { return false; } + if (this._rfb_connection_state !== 'connected') { return false; } if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } if (this._FBU.bytes === 0) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 88a78c72..9d812499 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -67,10 +67,10 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('#connect', function () { beforeEach(function () { client._updateConnectionState = sinon.spy(); }); - it('should set the current state to "connect"', function () { + it('should set the current state to "connecting"', function () { client.connect('host', 8675); expect(client._updateConnectionState).to.have.been.calledOnce; - expect(client._updateConnectionState).to.have.been.calledWith('connect'); + expect(client._updateConnectionState).to.have.been.calledWith('connecting'); }); it('should fail if we are missing a host', function () { @@ -97,10 +97,10 @@ describe('Remote Frame Buffer Protocol Client', function() { describe('#disconnect', function () { beforeEach(function () { client._updateConnectionState = sinon.spy(); }); - it('should set the current state to "disconnect"', function () { + it('should set the current state to "disconnecting"', function () { client.disconnect(); expect(client._updateConnectionState).to.have.been.calledOnce; - expect(client._updateConnectionState).to.have.been.calledWith('disconnect'); + expect(client._updateConnectionState).to.have.been.calledWith('disconnecting'); }); it('should unregister error event handler', function () { @@ -145,7 +145,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "normal"; + client._rfb_connection_state = 'connected'; client._view_only = false; }); @@ -181,7 +181,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "normal"; + client._rfb_connection_state = 'connected'; client._view_only = false; }); @@ -219,7 +219,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "normal"; + client._rfb_connection_state = 'connected'; client._view_only = false; }); @@ -243,7 +243,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "normal"; + client._rfb_connection_state = 'connected'; client._view_only = false; client._supportsSetDesktopSize = true; }); @@ -285,7 +285,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "normal"; + client._rfb_connection_state = 'connected'; client._view_only = false; client._rfb_xvp_ver = 1; }); @@ -329,10 +329,10 @@ describe('Remote Frame Buffer Protocol Client', function() { this.clock.restore(); }); - it('should clear the disconnect timer if the state is not disconnect', function () { + it('should clear the disconnect timer if the state is not "disconnecting"', function () { var spy = sinon.spy(); client._disconnTimer = setTimeout(spy, 50); - client._updateConnectionState('normal'); + client._updateConnectionState('connected'); this.clock.tick(51); expect(spy).to.not.have.been.called; expect(client._disconnTimer).to.be.null; @@ -388,33 +388,33 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); - describe('connect', function () { + describe('connecting', function () { var client; beforeEach(function () { client = make_rfb(); }); it('should reset the variable states', function () { sinon.spy(client, '_init_vars'); - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._init_vars).to.have.been.calledOnce; }); it('should actually connect to the websocket', function () { sinon.spy(client._sock, 'open'); - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._sock.open).to.have.been.calledOnce; }); it('should use wss:// to connect if encryption is enabled', function () { sinon.spy(client._sock, 'open'); client.set_encrypt(true); - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._sock.open.args[0][0]).to.contain('wss://'); }); it('should use ws:// to connect if encryption is not enabled', function () { sinon.spy(client._sock, 'open'); client.set_encrypt(true); - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._sock.open.args[0][0]).to.contain('wss://'); }); @@ -424,18 +424,18 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_host = 'HOST'; client._rfb_port = 8675; client._rfb_path = 'PATH'; - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); }); it('should attempt to close the websocket before we open an new one', function () { sinon.spy(client._sock, 'close'); - client._updateConnectionState('connect'); + client._updateConnectionState('connecting'); expect(client._sock.close).to.have.been.calledOnce; }); }); - describe('disconnect', function () { + describe('disconnecting', function () { var client; beforeEach(function () { this.clock = sinon.useFakeTimers(); @@ -449,13 +449,13 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should fail if we do not call Websock.onclose within the disconnection timeout', function () { client._sock._websocket.close = function () {}; // explicitly don't call onclose - client._updateConnectionState('disconnect'); + client._updateConnectionState('disconnecting'); this.clock.tick(client.get_disconnectTimeout() * 1000); expect(client._rfb_connection_state).to.equal('failed'); }); it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { - client._updateConnectionState('disconnect'); + client._updateConnectionState('disconnecting'); this.clock.tick(client.get_disconnectTimeout() * 500); client._sock._websocket.close(); this.clock.tick(client.get_disconnectTimeout() * 500 + 1); @@ -464,7 +464,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should close the WebSocket connection', function () { sinon.spy(client._sock, 'close'); - client._updateConnectionState('disconnect'); + client._updateConnectionState('disconnecting'); expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect }); }); @@ -513,7 +513,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); - // NB(directxman12): Normal does *nothing* in updateConnectionState + // NB(directxman12): Connected does *nothing* in updateConnectionState }); describe('Protocol Initialization States', function () { @@ -1069,7 +1069,7 @@ describe('Remote Frame Buffer Protocol Client', function() { } client._sock._websocket._receive_data(tight_data); - expect(client._rfb_connection_state).to.equal('normal'); + expect(client._rfb_connection_state).to.equal('connected'); }); it('should set the true color mode on the display to the configuration variable', function () { @@ -1131,9 +1131,9 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._sock).to.have.sent(expected._sQ); }); - it('should transition to the "normal" state', function () { + it('should transition to the "connected" state', function () { send_server_init({}, client); - expect(client._rfb_connection_state).to.equal('normal'); + expect(client._rfb_connection_state).to.equal('connected'); }); }); }); @@ -1145,7 +1145,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; @@ -1158,7 +1158,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; client._fb_width = 640; client._fb_height = 20; @@ -1348,7 +1348,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; // a really small frame client._fb_width = 4; @@ -1425,7 +1425,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; // a really small frame client._fb_width = 4; @@ -1601,7 +1601,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; client._supportsSetDesktopSize = false; // a really small frame @@ -1743,7 +1743,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client = make_rfb(); client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._fb_name = 'some device'; client._fb_width = 27; client._fb_height = 32; @@ -1873,7 +1873,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock.open('ws://', 'binary'); client._sock._websocket._open(); sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; }); it('should not send button messages in view-only mode', function () { @@ -2014,15 +2014,15 @@ describe('Remote Frame Buffer Protocol Client', function() { // message events it ('should do nothing if we receive an empty message and have nothing in the queue', function () { client.connect('host', 8675); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._normal_msg = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([])); expect(client._normal_msg).to.not.have.been.called; }); - it('should handle a message in the normal state as a normal message', function () { + it('should handle a message in the connected state as a normal message', function () { client.connect('host', 8675); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._normal_msg = sinon.spy(); client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); expect(client._normal_msg).to.have.been.calledOnce; @@ -2039,7 +2039,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should split up the handling of muplitle normal messages across 10ms intervals', function () { client.connect('host', 8675); client._sock._websocket._open(); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client.set_onBell(sinon.spy()); client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); expect(client.get_onBell()).to.have.been.calledOnce; @@ -2048,7 +2048,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); // open events - it('should update the state to ProtocolVersion on open (if the state is "connect")', function () { + it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () { client.connect('host', 8675); client._sock._websocket._open(); expect(client._rfb_init_state).to.equal('ProtocolVersion'); @@ -2062,16 +2062,16 @@ describe('Remote Frame Buffer Protocol Client', function() { }); // close events - it('should transition to "disconnected" from "disconnect" on a close event', function () { + it('should transition to "disconnected" from "disconnecting" on a close event', function () { client.connect('host', 8675); - client._rfb_connection_state = 'disconnect'; + client._rfb_connection_state = 'disconnecting'; client._sock._websocket.close(); expect(client._rfb_connection_state).to.equal('disconnected'); }); it('should transition to failed if we get a close event from any non-"disconnection" state', function () { client.connect('host', 8675); - client._rfb_connection_state = 'normal'; + client._rfb_connection_state = 'connected'; client._sock._websocket.close(); expect(client._rfb_connection_state).to.equal('failed'); }); @@ -2079,7 +2079,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should unregister close event handler', function () { sinon.spy(client._sock, 'off'); client.connect('host', 8675); - client._rfb_connection_state = 'disconnect'; + client._rfb_connection_state = 'disconnecting'; client._sock._websocket.close(); expect(client._sock.off).to.have.been.calledWith('close'); }); diff --git a/vnc_auto.html b/vnc_auto.html index 4362080d..11eac44e 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -143,13 +143,13 @@ switch (state) { case 'failed': level = "error"; break; case 'fatal': level = "error"; break; - case 'normal': level = "normal"; break; + case 'connected': level = "normal"; break; case 'disconnected': level = "normal"; break; case 'loaded': level = "normal"; break; default: level = "warn"; break; } - if (state === "normal") { + if (state === 'connected') { cad.disabled = false; } else { cad.disabled = true; From 3bb12056b114896595cc506fab4cf909766dc106 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 18 Sep 2016 00:52:25 +0200 Subject: [PATCH 09/15] Remove unnecessary substates of disconnected The states 'loaded', 'failed' and 'fatal' were all variations of the 'disconnected' state. Removing these states allows us to get rid of many ugly workarounds and special cases. Error messages to the UI can now instead be delivered via a new onDisconnected callback. --- app/ui.js | 113 ++++++++++----------- core/rfb.js | 146 +++++++++++++-------------- tests/test.rfb.js | 246 +++++++++++++++++++++++++++------------------- vnc_auto.html | 67 +++++++++---- 4 files changed, 317 insertions(+), 255 deletions(-) diff --git a/app/ui.js b/app/ui.js index 7251a4c6..f84b3b7e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -36,7 +36,8 @@ var UI; UI = { - rfb_state: 'loaded', + connected: false, + desktopName: "", resizeTimeout: null, statusTimeout: null, @@ -340,15 +341,16 @@ var UI; UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), 'onNotification': UI.notification, 'onUpdateState': UI.updateState, + 'onDisconnected': UI.disconnectFinished, 'onPasswordRequired': UI.passwordRequired, 'onXvpInit': UI.updateXvpButton, 'onClipboard': UI.clipboardReceive, 'onFBUComplete': UI.initialResize, 'onFBResize': UI.updateViewDrag, - 'onDesktopName': UI.updateDocumentTitle}); + 'onDesktopName': UI.updateDesktopName}); return true; } catch (exc) { - UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + UI.showStatus('Unable to create RFB client -- ' + exc, 'error'); return false; } }, @@ -359,26 +361,31 @@ var UI; * VISUAL * ------v------*/ - updateState: function(rfb, state, oldstate, msg) { - UI.rfb_state = state; - - if (typeof(msg) !== 'undefined') { - switch (state) { - case 'failed': - case 'fatal': - // zero means no timeout - UI.showStatus(msg, 'error', 0); - break; - case 'connected': - /* falls through */ - case 'disconnected': - case 'loaded': - UI.showStatus(msg, 'normal'); - break; - default: - UI.showStatus(msg, 'warn'); - break; - } + updateState: function(rfb, state, oldstate) { + switch (state) { + case 'connecting': + UI.showStatus("Connecting"); + break; + case 'connected': + UI.connected = true; + if (rfb && rfb.get_encrypt()) { + UI.showStatus("Connected (encrypted) to " + + UI.desktopName); + } else { + UI.showStatus("Connected (unencrypted) to " + + UI.desktopName); + } + break; + case 'disconnecting': + UI.showStatus("Disconnecting"); + break; + case 'disconnected': + UI.connected = false; + UI.showStatus("Disconnected"); + break; + default: + UI.showStatus("Invalid state", 'error'); + break; } UI.updateVisualState(); @@ -386,26 +393,24 @@ var UI; // Disable/enable controls depending on connection state updateVisualState: function() { - var connected = UI.rfb && UI.rfb_state === 'connected'; - //Util.Debug(">> updateVisualState"); - document.getElementById('noVNC_setting_encrypt').disabled = connected; - document.getElementById('noVNC_setting_true_color').disabled = connected; + document.getElementById('noVNC_setting_encrypt').disabled = UI.connected; + document.getElementById('noVNC_setting_true_color').disabled = UI.connected; if (Util.browserSupportsCursorURIs()) { - document.getElementById('noVNC_setting_cursor').disabled = connected; + document.getElementById('noVNC_setting_cursor').disabled = UI.connected; } else { UI.updateSetting('cursor', !UI.isTouchDevice); document.getElementById('noVNC_setting_cursor').disabled = true; } UI.enableDisableViewClip(); - document.getElementById('noVNC_setting_resize').disabled = connected; - document.getElementById('noVNC_setting_shared').disabled = connected; - document.getElementById('noVNC_setting_view_only').disabled = connected; - document.getElementById('noVNC_setting_path').disabled = connected; - document.getElementById('noVNC_setting_repeaterID').disabled = connected; + document.getElementById('noVNC_setting_resize').disabled = UI.connected; + document.getElementById('noVNC_setting_shared').disabled = UI.connected; + document.getElementById('noVNC_setting_view_only').disabled = UI.connected; + document.getElementById('noVNC_setting_path').disabled = UI.connected; + document.getElementById('noVNC_setting_repeaterID').disabled = UI.connected; - if (connected) { + if (UI.connected) { document.documentElement.classList.add("noVNC_connected"); UI.updateViewClip(); UI.setMouseButton(1); @@ -425,18 +430,6 @@ var UI; document.getElementById('noVNC_password_dlg') .classList.remove('noVNC_open'); - switch (UI.rfb_state) { - case 'fatal': - case 'failed': - case 'disconnected': - UI.openConnectPanel(); - break; - case 'loaded': - break; - default: - break; - } - //Util.Debug("<< updateVisualState"); }, @@ -949,7 +942,8 @@ var UI; } if ((!host) || (!port)) { - throw new Error("Must set host and port"); + UI.showStatus("Must set host and port", 'error'); + return; } if (!UI.initRFB()) return; @@ -974,6 +968,13 @@ var UI; // Don't display the connection settings until we're actually disconnected }, + disconnectFinished: function (rfb, reason) { + if (typeof reason !== 'undefined') { + UI.showStatus(reason, 'error'); + } + UI.openConnectPanel(); + }, + /* ------^------- * /CONNECTION * ============== @@ -992,7 +993,7 @@ var UI; if (typeof msg === 'undefined') { msg = "Password is required"; } - UI.updateState(null, "warning", null, msg); + UI.showStatus(msg, "warning"); }, setPassword: function() { @@ -1062,7 +1063,7 @@ var UI; var screen = UI.screenSize(); - if (screen && UI.rfb_state === 'connected' && UI.rfb.get_display()) { + if (screen && UI.connected && UI.rfb.get_display()) { var display = UI.rfb.get_display(); var resizeMode = UI.getSetting('resize'); @@ -1187,7 +1188,6 @@ var UI; // Handle special cases where clipping is forced on/off or locked enableDisableViewClip: function() { var resizeSetting = document.getElementById('noVNC_setting_resize'); - var connected = UI.rfb && UI.rfb_state === 'connected'; if (UI.isSafari) { // Safari auto-hides the scrollbars which makes them @@ -1209,9 +1209,11 @@ var UI; } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) { // Restore view clip to what it was before fullscreen on IE UI.setViewClip(UI.rememberedClipSetting); - document.getElementById('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; + document.getElementById('noVNC_setting_clip').disabled = + UI.connected || UI.isTouchDevice; } else { - document.getElementById('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; + document.getElementById('noVNC_setting_clip').disabled = + UI.connected || UI.isTouchDevice; if (UI.isTouchDevice) { UI.setViewClip(true); } @@ -1243,7 +1245,7 @@ var UI; updateViewDrag: function() { var clipping = false; - if (UI.rfb_state !== 'connected') return; + if (!UI.connected) return; // Check if viewport drag is possible. It is only possible // if the remote display is clipping the client display. @@ -1537,8 +1539,9 @@ var UI; UI.rfb.get_mouse().set_focused(true); }, - // Display the desktop name in the document title - updateDocumentTitle: function(rfb, name) { + updateDesktopName: function(rfb, name) { + UI.desktopName = name; + // Display the desktop name in the document title document.title = name + " - noVNC"; }, diff --git a/core/rfb.js b/core/rfb.js index d0341931..eb5f29b5 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -35,11 +35,12 @@ this._rfb_password = ''; this._rfb_path = ''; - this._rfb_connection_state = 'disconnected'; + this._rfb_connection_state = ''; this._rfb_init_state = ''; this._rfb_version = 0; this._rfb_max_version = 3.8; this._rfb_auth_scheme = ''; + this._rfb_disconnect_reason = ""; this._rfb_tightvnc = false; this._rfb_xvp_ver = 0; @@ -158,8 +159,9 @@ 'viewportDrag': false, // Move the viewport on mouse drags // Callback functions - 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change 'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI + 'onDisconnected': function () { }, // onDisconnected(rfb, reason): disconnection finished 'onPasswordRequired': function () { }, // onPasswordRequired(rfb, msg): VNC password is required 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received 'onBell': function () { }, // onBell(rfb): RFB Bell message received @@ -225,12 +227,11 @@ } switch (this._rfb_connection_state) { case 'disconnecting': - this._updateConnectionState('disconnected', 'VNC disconnected' + msg); + this._updateConnectionState('disconnected'); break; case 'connecting': this._fail('Failed to connect to server' + msg); break; - case 'failed': case 'disconnected': Util.Error("Received onclose while disconnected" + msg); break; @@ -245,10 +246,10 @@ }); this._init_vars(); + this._cleanupSocket(); var rmode = this._display.get_render_mode(); - Util.Info("Using native WebSockets"); - this._updateConnectionState('loaded', 'noVNC ready: native WebSockets, ' + rmode); + Util.Info("Using native WebSockets, render mode: " + rmode); Util.Debug("<< RFB.constructor"); }; @@ -272,7 +273,7 @@ }, disconnect: function () { - this._updateConnectionState('disconnecting', 'Disconnecting'); + this._updateConnectionState('disconnecting'); this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); @@ -407,7 +408,7 @@ } }, - _cleanupSocket: function (state) { + _cleanupSocket: function () { if (this._msgTimer) { clearInterval(this._msgTimer); this._msgTimer = null; @@ -416,10 +417,8 @@ if (this._display && this._display.get_context()) { this._keyboard.ungrab(); this._mouse.ungrab(); - if (state !== 'connecting' && state !== 'loaded') { - this._display.defaultCursor(); - } - if (Util.get_logging() !== 'debug' || state === 'loaded') { + this._display.defaultCursor(); + if (Util.get_logging() !== 'debug') { // Show noVNC logo on load and when disconnected, unless in // debug mode this._display.clear(); @@ -431,47 +430,27 @@ /* * Connection states: - * loaded - page load, equivalent to disconnected - * disconnected - idle state * connecting * connected * disconnecting - * failed - abnormal disconnect - * fatal - failed to load page, or fatal error + * disconnected - permanent state */ - _updateConnectionState: function (state, statusMsg) { + _updateConnectionState: function (state) { var oldstate = this._rfb_connection_state; if (state === oldstate) { - // Already here, ignore Util.Debug("Already in state '" + state + "', ignoring"); return; } + // The 'disconnected' state is permanent for each RFB object + if (oldstate === 'disconnected') { + Util.Error("Tried changing state of a disconnected RFB object"); + return; + } + this._rfb_connection_state = state; - /* - * These are disconnected states. A previous connect may - * asynchronously cause a connection so make sure we are closed. - */ - if (state in {'disconnected': 1, 'loaded': 1, 'connecting': 1, - 'disconnecting': 1, 'failed': 1, 'fatal': 1}) { - this._cleanupSocket(state); - } - - if (oldstate === 'fatal') { - Util.Error('Fatal error, cannot continue'); - } - - if (typeof(statusMsg) !== 'undefined') { - var cmsg = " Msg: " + statusMsg; - if (state === 'failed' || state === 'fatal') { - Util.Error(cmsg); - } else { - Util.Warn(cmsg); - } - } - var smsg = "New state '" + state + "', was '" + oldstate + "'."; Util.Debug(smsg); @@ -482,60 +461,73 @@ this._sock.off('close'); // make sure we don't get a double event } + this._onUpdateState(this, state, oldstate); switch (state) { case 'connected': - if (oldstate === 'disconnected' || oldstate === 'failed') { - Util.Error("Invalid transition from 'disconnected' or 'failed' to 'connected'"); + if (oldstate !== 'connecting') { + Util.Error("Bad transition to connected state, " + + "previous connection state: " + oldstate); + return; + } + break; + + case 'disconnected': + if (oldstate !== 'disconnecting') { + Util.Error("Bad transition to disconnected state, " + + "previous connection state: " + oldstate); + return; + } + + if (this._rfb_disconnect_reason !== "") { + this._onDisconnected(this, this._rfb_disconnect_reason); + } else { + // No reason means clean disconnect + this._onDisconnected(this); } break; case 'connecting': this._init_vars(); + + // WebSocket.onopen transitions to the RFB init states this._connect(); - // WebSocket.onopen transitions to 'ProtocolVersion' break; case 'disconnecting': + // WebSocket.onclose transitions to 'disconnected' + this._cleanupSocket(); + this._disconnTimer = setTimeout(function () { - this._fail("Disconnect timeout"); + this._rfb_disconnect_reason = "Disconnect timeout"; + this._updateConnectionState('disconnected'); }.bind(this), this._disconnectTimeout * 1000); this._print_stats(); - - // WebSocket.onclose transitions to 'disconnected' - break; - - case 'failed': - if (oldstate === 'disconnected') { - Util.Error("Invalid transition from 'disconnected' to 'failed'"); - } else if (oldstate === 'connected') { - Util.Error("Error while connected."); - } else if (oldstate === 'init') { - Util.Error("Error while initializing."); - } break; default: - // No state change action to take - } - - if (oldstate === 'failed' && state === 'disconnected') { - // do disconnect action, but stay in failed state and - // keep the previous status message - this._rfb_connection_state = 'failed'; - this._onUpdateState(this, state, oldstate); - } else { - this._onUpdateState(this, state, oldstate, statusMsg); - } - - // Make sure we transition to disconnected - if (state === 'failed') { - this._updateConnectionState('disconnected'); + Util.Error("Unknown connection state: " + state); + return; } }, _fail: function (msg) { - this._updateConnectionState('failed', msg); + switch (this._rfb_connection_state) { + case 'disconnecting': + Util.Error("Error while disconnecting: " + msg); + break; + case 'connected': + Util.Error("Error while connected: " + msg); + break; + case 'connecting': + Util.Error("Error while connecting: " + msg); + break; + default: + Util.Error("RFB error: " + msg); + break; + } + this._rfb_disconnect_reason = msg; + this._updateConnectionState('disconnecting'); return false; }, @@ -574,7 +566,6 @@ switch (this._rfb_connection_state) { case 'disconnected': - case 'failed': Util.Error("Got data while disconnected"); break; case 'connected': @@ -1057,11 +1048,7 @@ this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; - if (this._encrypt) { - this._updateConnectionState('connected', 'Connected (encrypted) to: ' + this._fb_name); - } else { - this._updateConnectionState('connected', 'Connected (unencrypted) to: ' + this._fb_name); - } + this._updateConnectionState('connected'); return true; }, @@ -1376,8 +1363,9 @@ ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags // Callback functions - ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change ['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI + ['onDisconnected', 'rw', 'func'], // onDisconnected(rfb, reason): disconnection finished ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb, msg): VNC password is required ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 9d812499..43633da2 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -73,24 +73,22 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._updateConnectionState).to.have.been.calledWith('connecting'); }); - it('should fail if we are missing a host', function () { - sinon.spy(client, '_fail'); + it('should not try to connect if we are missing a host', function () { + client._fail = sinon.spy(); + client._rfb_connection_state = ''; client.connect(undefined, 8675); expect(client._fail).to.have.been.calledOnce; + expect(client._updateConnectionState).to.not.have.been.called; + expect(client._rfb_connection_state).to.equal(''); }); - it('should fail if we are missing a port', function () { - sinon.spy(client, '_fail'); + it('should not try to connect if we are missing a port', function () { + client._fail = sinon.spy(); + client._rfb_connection_state = ''; client.connect('abc'); expect(client._fail).to.have.been.calledOnce; - }); - - it('should not update the state if we are missing a host or port', function () { - sinon.spy(client, '_fail'); - client.connect('abc'); - expect(client._fail).to.have.been.calledOnce; - expect(client._updateConnectionState).to.have.been.calledOnce; - expect(client._updateConnectionState).to.have.been.calledWith('failed'); + expect(client._updateConnectionState).to.not.have.been.called; + expect(client._rfb_connection_state).to.equal(''); }); }); @@ -337,6 +335,76 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(spy).to.not.have.been.called; expect(client._disconnTimer).to.be.null; }); + + it('should call the updateState callback', function () { + client.set_onUpdateState(sinon.spy()); + client._updateConnectionState('a specific state'); + var spy = client.get_onUpdateState(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal('a specific state'); + }); + + it('should set the rfb_connection_state', function () { + client._updateConnectionState('a specific state'); + expect(client._rfb_connection_state).to.equal('a specific state'); + }); + + it('should not change the state when we are disconnected', function () { + client._rfb_connection_state = 'disconnected'; + client._updateConnectionState('a specific state'); + expect(client._rfb_connection_state).to.not.equal('a specific state'); + }); + + it('should ignore state changes to the same state', function () { + client.set_onUpdateState(sinon.spy()); + client._rfb_connection_state = 'a specific state'; + client._updateConnectionState('a specific state'); + var spy = client.get_onUpdateState(); + expect(spy).to.not.have.been.called; + }); + }); + + describe('#_fail', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + client.connect('host', 8675); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._fail(); + expect(client._sock.close).to.have.been.calledOnce; + }); + + it('should transition to disconnected', function () { + sinon.spy(client, '_updateConnectionState'); + client._fail(); + this.clock.tick(2000); + expect(client._updateConnectionState).to.have.been.called; + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should set disconnect_reason', function () { + client._fail('a reason'); + expect(client._rfb_disconnect_reason).to.equal('a reason'); + }); + + it('should result in disconnect callback with message when reason given', function () { + client.set_onDisconnected(sinon.spy()); + client._fail('a reason'); + var spy = client.get_onDisconnected(); + this.clock.tick(2000); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0].length).to.equal(2); + expect(spy.args[0][1]).to.equal('a reason'); + }); + }); describe('#_notification', function () { @@ -359,35 +427,9 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(spy).to.not.have.been.called; }); }); - }); describe('Connection States', function () { - describe('loaded', function () { - var client; - beforeEach(function () { - client = make_rfb(); - client._rfb_connection_state = 'disconnected'; - }); - - it('should close any open WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('loaded'); - expect(client._sock.close).to.have.been.calledOnce; - }); - }); - - describe('disconnected', function () { - var client; - beforeEach(function () { client = make_rfb(); }); - - it('should close any open WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('disconnected'); - expect(client._sock.close).to.have.been.calledOnce; - }); - }); - describe('connecting', function () { var client; beforeEach(function () { client = make_rfb(); }); @@ -427,12 +469,6 @@ describe('Remote Frame Buffer Protocol Client', function() { client._updateConnectionState('connecting'); expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); }); - - it('should attempt to close the websocket before we open an new one', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('connecting'); - expect(client._sock.close).to.have.been.calledOnce; - }); }); describe('disconnecting', function () { @@ -447,11 +483,14 @@ describe('Remote Frame Buffer Protocol Client', function() { this.clock.restore(); }); - it('should fail if we do not call Websock.onclose within the disconnection timeout', function () { + it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () { + sinon.spy(client, '_updateConnectionState'); client._sock._websocket.close = function () {}; // explicitly don't call onclose client._updateConnectionState('disconnecting'); this.clock.tick(client.get_disconnectTimeout() * 1000); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._updateConnectionState).to.have.been.calledTwice; + expect(client._rfb_disconnect_reason).to.not.equal(""); + expect(client._rfb_connection_state).to.equal("disconnected"); }); it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { @@ -465,51 +504,49 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should close the WebSocket connection', function () { sinon.spy(client._sock, 'close'); client._updateConnectionState('disconnecting'); - expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect + expect(client._sock.close).to.have.been.calledOnce; }); }); - describe('failed', function () { - var client; - beforeEach(function () { - this.clock = sinon.useFakeTimers(); - client = make_rfb(); - client.connect('host', 8675); - }); - - afterEach(function () { - this.clock.restore(); - }); - - it('should close the WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('failed'); - expect(client._sock.close).to.have.been.called; - }); - - it('should transition to disconnected but stay in failed state', function () { - client.set_onUpdateState(sinon.spy()); - client._updateConnectionState('failed'); - this.clock.tick(50); - expect(client._rfb_connection_state).to.equal('failed'); - - var onUpdateState = client.get_onUpdateState(); - expect(onUpdateState).to.have.been.called; - // it should be specifically the last call - expect(onUpdateState.args[onUpdateState.args.length - 1][1]).to.equal('disconnected'); - expect(onUpdateState.args[onUpdateState.args.length - 1][2]).to.equal('failed'); - }); - - }); - - describe('fatal', function () { + describe('disconnected', function () { var client; beforeEach(function () { client = make_rfb(); }); - it('should close any open WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('fatal'); - expect(client._sock.close).to.have.been.calledOnce; + it('should call the disconnect callback if the state is "disconnected"', function () { + client.set_onDisconnected(sinon.spy()); + client._rfb_connection_state = 'disconnecting'; + client._rfb_disconnect_reason = "error"; + client._updateConnectionState('disconnected'); + var spy = client.get_onDisconnected(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal("error"); + }); + + it('should not call the disconnect callback if the state is not "disconnected"', function () { + client.set_onDisconnected(sinon.spy()); + client._updateConnectionState('disconnecting'); + var spy = client.get_onDisconnected(); + expect(spy).to.not.have.been.called; + }); + + it('should call the disconnect callback without msg when no reason given', function () { + client.set_onDisconnected(sinon.spy()); + client._rfb_connection_state = 'disconnecting'; + client._rfb_disconnect_reason = ""; + client._updateConnectionState('disconnected'); + var spy = client.get_onDisconnected(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0].length).to.equal(1); + }); + + it('should call the updateState callback before the disconnect callback', function () { + client.set_onDisconnected(sinon.spy()); + client.set_onUpdateState(sinon.spy()); + client._rfb_connection_state = 'other state'; + client._updateConnectionState('disconnected'); + var updateStateSpy = client.get_onUpdateState(); + var disconnectSpy = client.get_onDisconnected(); + expect(updateStateSpy.calledBefore(disconnectSpy)).to.be.true; }); }); @@ -594,8 +631,9 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on an invalid version', function () { + sinon.spy(client, "_fail"); send_ver('002.000', client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); }); @@ -665,10 +703,11 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail if there are no supported schemes for versions >= 3.7', function () { + sinon.spy(client, "_fail"); client._rfb_version = 3.7; var auth_schemes = [1, 32]; client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { @@ -717,7 +756,6 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); client._sock._websocket._receive_data(new Uint8Array(data)); - expect(client._rfb_connection_state).to.equal('failed'); expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies'); }); @@ -734,9 +772,10 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on an unknown auth scheme', function () { + sinon.spy(client, "_fail"); client._rfb_version = 3.8; send_security(57, client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); describe('VNC Authentication (type 2) Handler', function () { @@ -876,8 +915,9 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail if no supported tunnels are listed', function () { + sinon.spy(client, "_fail"); send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); it('should choose the notunnel tunnel type', function () { @@ -919,9 +959,10 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail if there are no supported auth types', function () { + sinon.spy(client, "_fail"); client._rfb_tightvnc = true; send_num_str_pairs([[23, 'stdv', 'badval__']], client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); }); }); @@ -947,14 +988,14 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; client._sock._websocket._receive_data(new Uint8Array(failure_data)); - expect(client._rfb_connection_state).to.equal('failed'); expect(client._fail).to.have.been.calledWith('whoops'); }); it('should fail on an error code of 1 with a standard message for version < 3.8', function () { + sinon.spy(client, '_fail'); client._rfb_version = 3.7; client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledWith('Authentication failure'); }); }); @@ -1316,10 +1357,10 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on an unsupported encoding', function () { - client.set_onFBUReceive(sinon.spy()); + sinon.spy(client, "_fail"); var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; send_fbu_msg([rect_info], [[]], client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); it('should be able to pause and resume receiving rects if not enought data', function () { @@ -1563,10 +1604,11 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on an invalid subencoding', function () { + sinon.spy(client,"_fail"); var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rects = [[45]]; // an invalid subencoding send_fbu_msg(info, rects, client); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); }); @@ -1766,8 +1808,9 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on unknown XVP message types', function () { + sinon.spy(client, "_fail"); client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237])); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); }); @@ -1859,8 +1902,9 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail on an unknown message type', function () { + sinon.spy(client, "_fail"); client._sock._websocket._receive_data(new Uint8Array([87])); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); }); @@ -2055,10 +2099,11 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fail if we are not currently ready to connect and we get an "open" event', function () { + sinon.spy(client, "_fail"); client.connect('host', 8675); client._rfb_connection_state = 'some_other_state'; client._sock._websocket._open(); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); // close events @@ -2070,10 +2115,11 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should transition to failed if we get a close event from any non-"disconnection" state', function () { + sinon.spy(client, "_fail"); client.connect('host', 8675); client._rfb_connection_state = 'connected'; client._sock._websocket.close(); - expect(client._rfb_connection_state).to.equal('failed'); + expect(client._fail).to.have.been.calledOnce; }); it('should unregister close event handler', function () { diff --git a/vnc_auto.html b/vnc_auto.html index 11eac44e..492abefc 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -86,6 +86,7 @@ var rfb; var resizeTimeout; + var desktopName; function UIresize() { @@ -102,6 +103,9 @@ UIresize(); rfb.set_onFBUComplete(function() { }); } + function updateDesktopName(rfb, name) { + desktopName = name; + } function passwordRequired(rfb, msg) { if (typeof msg === 'undefined') { msg = 'Password Required: '; @@ -112,8 +116,7 @@ html += msg; html += ''; html += '<\/form>'; - document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); - document.getElementById('noVNC_status').innerHTML = html; + status(html, "warn"); } function setPassword() { rfb.sendPassword(document.getElementById('password_input').value); @@ -135,18 +138,37 @@ rfb.xvpReset(); return false; } - function updateState(rfb, state, oldstate, msg) { - var s, sb, cad, level; - s = document.getElementById('noVNC_status'); - sb = document.getElementById('noVNC_status_bar'); - cad = document.getElementById('sendCtrlAltDelButton'); + function status(text, level) { + switch (level) { + case 'normal': + case 'warn': + case 'error': + break; + default: + level = "warn"; + } + document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_" + level); + document.getElementById('noVNC_status').innerHTML = text; + } + function updateState(rfb, state, oldstate) { + var cad = document.getElementById('sendCtrlAltDelButton'); + var encrypt = (rfb && rfb.get_encrypt()) ? 'encrypted' : 'unencrypted'; switch (state) { - case 'failed': level = "error"; break; - case 'fatal': level = "error"; break; - case 'connected': level = "normal"; break; - case 'disconnected': level = "normal"; break; - case 'loaded': level = "normal"; break; - default: level = "warn"; break; + case 'connecting': + status("Connecting", "normal"); + break; + case 'connected': + status("Connected (" + encrypt + ") to: " + desktopName, "normal"); + break; + case 'disconnecting': + status("Disconnecting", "normal"); + break; + case 'disconnected': + status("Disconnected", "normal"); + break; + default: + status(state, "warn"); + break; } if (state === 'connected') { @@ -156,13 +178,14 @@ xvpInit(0); } - if (typeof(msg) !== 'undefined') { - sb.setAttribute("class", "noVNC_status_" + level); - s.innerHTML = msg; + } + function disconnected(rfb, reason) { + if (typeof(reason) !== 'undefined') { + status(reason, "error"); } } function notification(rfb, msg, level, options) { - $D('noVNC_status').innerHTML = msg; + status(msg, level); } window.onresize = function () { @@ -226,7 +249,7 @@ } if ((!host) || (!port)) { - updateState(null, 'fatal', null, 'Must specify host and port in URL'); + status('Must specify host and port in URL', 'error'); return; } @@ -239,13 +262,15 @@ 'local_cursor': WebUtil.getConfigVar('cursor', true), 'shared': WebUtil.getConfigVar('shared', true), 'view_only': WebUtil.getConfigVar('view_only', false), - 'onNotification: notification, + 'onNotification': notification, 'onUpdateState': updateState, + 'onDisconnected': disconnected, 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired, - 'onFBUComplete': FBUComplete}); + 'onFBUComplete': FBUComplete, + 'onDesktopName': updateDesktopName}); } catch (exc) { - updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + status('Unable to create RFB client -- ' + exc, 'error'); return; // don't continue trying to connect } From 6c14514774dc0304d7ae1da17a05cc062305d60e Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 19 Sep 2016 00:32:48 +0200 Subject: [PATCH 10/15] Split cleanup from closing the socket --- core/rfb.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index eb5f29b5..ca173eae 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -246,7 +246,7 @@ }); this._init_vars(); - this._cleanupSocket(); + this._cleanup(); var rmode = this._display.get_render_mode(); Util.Info("Using native WebSockets, render mode: " + rmode); @@ -408,7 +408,7 @@ } }, - _cleanupSocket: function () { + _cleanup: function () { if (this._msgTimer) { clearInterval(this._msgTimer); this._msgTimer = null; @@ -424,8 +424,6 @@ this._display.clear(); } } - - this._sock.close(); }, /* @@ -494,8 +492,8 @@ break; case 'disconnecting': - // WebSocket.onclose transitions to 'disconnected' - this._cleanupSocket(); + this._cleanup(); + this._sock.close(); // transitions to 'disconnected' this._disconnTimer = setTimeout(function () { this._rfb_disconnect_reason = "Disconnect timeout"; From 67453adebce387914516f69655d40e7b27218f2f Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 22 Sep 2016 16:42:14 +0200 Subject: [PATCH 11/15] Remove padding which isn't needed anymore --- vnc_auto.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vnc_auto.html b/vnc_auto.html index 492abefc..48577f55 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -94,9 +94,8 @@ var innerW = window.innerWidth; var innerH = window.innerHeight; var controlbarH = document.getElementById('noVNC_status_bar').offsetHeight; - var padding = 5; if (innerW !== undefined && innerH !== undefined) - rfb.requestDesktopSize(innerW, innerH - controlbarH - padding); + rfb.requestDesktopSize(innerW, innerH - controlbarH); } } function FBUComplete(rfb, fbu) { From 9310577bd8d97fb554d7f1a60287d6938dfa6ae5 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 23 Sep 2016 09:42:13 +0200 Subject: [PATCH 12/15] Remove out-of-place comma --- core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/util.js b/core/util.js index 3cc1e876..058b4023 100644 --- a/core/util.js +++ b/core/util.js @@ -329,7 +329,7 @@ Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: ! 'presto': detectPresto(), 'trident': detectTrident(), 'webkit': detectInitialWebkit(), - 'gecko': detectGecko(), + 'gecko': detectGecko() }; if (Util.Engine.webkit) { From 7520ba52d8fa2c94b1d73abb4bee79c97401c107 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 30 Sep 2016 23:57:02 +0200 Subject: [PATCH 13/15] Don't close the control bar when disconnected If you were very quick to disconnect, you could end up in a state where the toolbar was hidden. --- app/ui.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/ui.js b/app/ui.js index f84b3b7e..13c00247 100644 --- a/app/ui.js +++ b/app/ui.js @@ -420,6 +420,7 @@ var UI; } else { document.documentElement.classList.remove("noVNC_connected"); UI.updateXvpButton(0); + UI.keepControlbar(); } // State change disables viewport dragging. From 74a4a2b46866ec8cd91aff02b309090004a8e9d2 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 1 Oct 2016 00:00:06 +0200 Subject: [PATCH 14/15] Do not timeout error messages --- app/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index 13c00247..c77b7dd1 100644 --- a/app/ui.js +++ b/app/ui.js @@ -470,8 +470,8 @@ var UI; time = 1500; } - // A specified time of zero means no timeout - if (time != 0) { + // Error messages do not timeout + if (status_type !== 'error') { UI.statusTimeout = window.setTimeout(UI.hideStatus, time); } }, From 4102b71c335ed48071d897426fe619e16aa1fba1 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 1 Oct 2016 00:00:49 +0200 Subject: [PATCH 15/15] Keep the connect panel when missing host or port --- app/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index c77b7dd1..889ceaa5 100644 --- a/app/ui.js +++ b/app/ui.js @@ -929,8 +929,6 @@ var UI; }, connect: function() { - UI.closeAllPanels(); - var host = document.getElementById('noVNC_setting_host').value; var port = document.getElementById('noVNC_setting_port').value; var password = document.getElementById('noVNC_setting_password').value; @@ -949,6 +947,8 @@ var UI; if (!UI.initRFB()) return; + UI.closeAllPanels(); + UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_true_color(UI.getSetting('true_color')); UI.rfb.set_local_cursor(UI.getSetting('cursor'));