This commit is contained in:
Pierre Ossman 2022-08-19 10:10:10 +02:00
commit 1d148a8478
2 changed files with 262 additions and 196 deletions

View File

@ -54,6 +54,21 @@ const GESTURE_SCRLSENS = 50;
const DOUBLE_TAP_TIMEOUT = 1000;
const DOUBLE_TAP_THRESHOLD = 50;
// Security types
const securityTypeNone = 1;
const securityTypeVNCAuth = 2;
const securityTypeRA2ne = 6;
const securityTypeTight = 16;
const securityTypeVeNCrypt = 19;
const securityTypeXVP = 22;
const securityTypeARD = 30;
// Special Tight security types
const securityTypeUnixLogon = 129;
// VeNCrypt security types
const securityTypePlain = 256;
// Extended clipboard pseudo-encoding formats
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
@ -402,7 +417,7 @@ export default class RFB extends EventTargetMixin {
sendCredentials(creds) {
this._rfbCredentials = creds;
setTimeout(this._initMsg.bind(this), 0);
this._resumeAuthentication();
}
sendCtrlAltDel() {
@ -922,8 +937,15 @@ export default class RFB extends EventTargetMixin {
}
}
break;
case 'connecting':
while (this._rfbConnectionState === 'connecting') {
if (!this._initMsg()) {
break;
}
}
break;
default:
this._initMsg();
Log.Error("Got data while in an invalid state");
break;
}
}
@ -1332,6 +1354,21 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = 'Security';
}
_isSupportedSecurityType(type) {
const clientTypes = [
securityTypeNone,
securityTypeVNCAuth,
securityTypeRA2ne,
securityTypeTight,
securityTypeVeNCrypt,
securityTypeXVP,
securityTypeARD,
securityTypePlain,
];
return clientTypes.includes(type);
}
_negotiateSecurity() {
if (this._rfbVersion >= 3.7) {
// Server sends supported list, client decides
@ -1342,28 +1379,23 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = "SecurityReason";
this._securityContext = "no security types";
this._securityStatus = 1;
return this._initMsg();
return true;
}
const types = this._sock.rQshiftBytes(numTypes);
Log.Debug("Server security types: " + types);
// Look for each auth in preferred order
if (types.includes(1)) {
this._rfbAuthScheme = 1; // None
} else if (types.includes(22)) {
this._rfbAuthScheme = 22; // XVP
} else if (types.includes(16)) {
this._rfbAuthScheme = 16; // Tight
} else if (types.includes(6)) {
this._rfbAuthScheme = 6; // RA2ne Auth
} else if (types.includes(2)) {
this._rfbAuthScheme = 2; // VNC Auth
} else if (types.includes(30)) {
this._rfbAuthScheme = 30; // ARD Auth
} else if (types.includes(19)) {
this._rfbAuthScheme = 19; // VeNCrypt Auth
} else {
// Look for a matching security type in the order that the
// server prefers
this._rfbAuthScheme = -1;
for (let type of types) {
if (this._isSupportedSecurityType(type)) {
this._rfbAuthScheme = type;
break;
}
}
if (this._rfbAuthScheme === -1) {
return this._fail("Unsupported security types (types: " + types + ")");
}
@ -1377,14 +1409,14 @@ export default class RFB extends EventTargetMixin {
this._rfbInitState = "SecurityReason";
this._securityContext = "authentication scheme";
this._securityStatus = 1;
return this._initMsg();
return true;
}
}
this._rfbInitState = 'Authentication';
Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
return this._initMsg(); // jump to authentication
return true;
}
_handleSecurityReason() {
@ -1434,7 +1466,7 @@ export default class RFB extends EventTargetMixin {
this._rfbCredentials.username +
this._rfbCredentials.target;
this._sock.sendString(xvpAuthStr);
this._rfbAuthScheme = 2;
this._rfbAuthScheme = securityTypeVNCAuth;
return this._negotiateAuthentication();
}
@ -1492,49 +1524,66 @@ export default class RFB extends EventTargetMixin {
subtypes.push(this._sock.rQshift32());
}
// 256 = Plain subtype
if (subtypes.indexOf(256) != -1) {
// 0x100 = 256
this._sock.send([0, 0, 1, 0]);
this._rfbVeNCryptState = 4;
} else {
return this._fail("VeNCrypt Plain subtype not offered by server");
}
}
// Look for a matching security type in the order that the
// server prefers
this._rfbAuthScheme = -1;
for (let type of subtypes) {
// Avoid getting in to a loop
if (type === securityTypeVeNCrypt) {
continue;
}
// negotiated Plain subtype, server waits for password
if (this._rfbVeNCryptState == 4) {
if (this._rfbCredentials.username === undefined ||
this._rfbCredentials.password === undefined) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["username", "password"] } }));
return false;
if (this._isSupportedSecurityType(type)) {
this._rfbAuthScheme = type;
break;
}
}
const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password);
if (this._rfbAuthScheme === -1) {
return this._fail("Unsupported security types (types: " + subtypes + ")");
}
this._sock.send([
(user.length >> 24) & 0xFF,
(user.length >> 16) & 0xFF,
(user.length >> 8) & 0xFF,
user.length & 0xFF
]);
this._sock.send([
(pass.length >> 24) & 0xFF,
(pass.length >> 16) & 0xFF,
(pass.length >> 8) & 0xFF,
pass.length & 0xFF
]);
this._sock.sendString(user);
this._sock.sendString(pass);
this._sock.send([this._rfbAuthScheme >> 24,
this._rfbAuthScheme >> 16,
this._rfbAuthScheme >> 8,
this._rfbAuthScheme]);
this._rfbInitState = "SecurityResult";
this._rfbVeNCryptState == 4;
return true;
}
}
_negotiatePlainAuth() {
if (this._rfbCredentials.username === undefined ||
this._rfbCredentials.password === undefined) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["username", "password"] } }));
return false;
}
const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password);
this._sock.send([
(user.length >> 24) & 0xFF,
(user.length >> 16) & 0xFF,
(user.length >> 8) & 0xFF,
user.length & 0xFF
]);
this._sock.send([
(pass.length >> 24) & 0xFF,
(pass.length >> 16) & 0xFF,
(pass.length >> 8) & 0xFF,
pass.length & 0xFF
]);
this._sock.sendString(user);
this._sock.sendString(pass);
this._rfbInitState = "SecurityResult";
return true;
}
_negotiateStdVNCAuth() {
if (this._sock.rQwait("auth challenge", 16)) { return false; }
@ -1661,7 +1710,7 @@ export default class RFB extends EventTargetMixin {
this._rfbCredentials.ardCredentials = encrypted;
this._rfbCredentials.ardPublicKey = clientPublicKey;
setTimeout(this._initMsg.bind(this), 0);
this._resumeAuthentication();
}
_negotiateTightUnixAuth() {
@ -1771,12 +1820,12 @@ export default class RFB extends EventTargetMixin {
case 'STDVNOAUTH__': // no auth
this._rfbInitState = 'SecurityResult';
return true;
case 'STDVVNCAUTH_': // VNC auth
this._rfbAuthScheme = 2;
return this._initMsg();
case 'TGHTULGNAUTH': // UNIX auth
this._rfbAuthScheme = 129;
return this._initMsg();
case 'STDVVNCAUTH_':
this._rfbAuthScheme = securityTypeVNCAuth;
return true;
case 'TGHTULGNAUTH':
this._rfbAuthScheme = securityTypeUnixLogon;
return true;
default:
return this._fail("Unsupported tiny auth scheme " +
"(scheme: " + authType + ")");
@ -1813,7 +1862,7 @@ export default class RFB extends EventTargetMixin {
}).then(() => {
this.dispatchEvent(new CustomEvent('securityresult'));
this._rfbInitState = "SecurityResult";
this._initMsg();
return true;
}).finally(() => {
this._rfbRSAAESAuthenticationState.removeEventListener(
"serververification", this._eventHandlers.handleRSAAESServerVerification);
@ -1827,33 +1876,32 @@ export default class RFB extends EventTargetMixin {
_negotiateAuthentication() {
switch (this._rfbAuthScheme) {
case 1: // no auth
if (this._rfbVersion >= 3.8) {
this._rfbInitState = 'SecurityResult';
return true;
}
this._rfbInitState = 'ClientInitialisation';
return this._initMsg();
case securityTypeNone:
this._rfbInitState = 'SecurityResult';
return true;
case 22: // XVP auth
case securityTypeXVP:
return this._negotiateXvpAuth();
case 30: // ARD auth
case securityTypeARD:
return this._negotiateARDAuth();
case 2: // VNC authentication
case securityTypeVNCAuth:
return this._negotiateStdVNCAuth();
case 16: // TightVNC Security Type
case securityTypeTight:
return this._negotiateTightAuth();
case 19: // VeNCrypt Security Type
case securityTypeVeNCrypt:
return this._negotiateVeNCryptAuth();
case 129: // TightVNC UNIX Security Type
case securityTypePlain:
return this._negotiatePlainAuth();
case securityTypeUnixLogon:
return this._negotiateTightUnixAuth();
case 6: // RA2ne Security Type
case securityTypeRA2ne:
return this._negotiateRA2neAuth();
default:
@ -1863,6 +1911,13 @@ export default class RFB extends EventTargetMixin {
}
_handleSecurityResult() {
// There is no security choice, and hence no security result
// until RFB 3.7
if (this._rfbVersion < 3.7) {
this._rfbInitState = 'ClientInitialisation';
return true;
}
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
const status = this._sock.rQshift32();
@ -1870,13 +1925,13 @@ export default class RFB extends EventTargetMixin {
if (status === 0) { // OK
this._rfbInitState = 'ClientInitialisation';
Log.Debug('Authentication OK');
return this._initMsg();
return true;
} else {
if (this._rfbVersion >= 3.8) {
this._rfbInitState = "SecurityReason";
this._securityContext = "security result";
this._securityStatus = status;
return this._initMsg();
return true;
} else {
this.dispatchEvent(new CustomEvent(
"securityfailure",
@ -2052,6 +2107,14 @@ export default class RFB extends EventTargetMixin {
}
}
// Resume authentication handshake after it was paused for some
// reason, e.g. waiting for a password from the user
_resumeAuthentication() {
// We use setTimeout() so it's run in its own context, just like
// it originally did via the WebSocket's event handler
setTimeout(this._initMsg.bind(this), 0);
}
_handleSetColourMapMsg() {
Log.Debug("SetColorMapEntries");

View File

@ -1026,17 +1026,21 @@ describe('Remote Frame Buffer Protocol Client', function () {
client._rfbConnectionState = 'connecting';
});
describe('ProtocolVersion', function () {
function sendVer(ver, client) {
const arr = new Uint8Array(12);
for (let i = 0; i < ver.length; i++) {
arr[i+4] = ver.charCodeAt(i);
}
arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
arr[11] = '\n';
client._sock._websocket._receiveData(arr);
function sendVer(ver, client) {
const arr = new Uint8Array(12);
for (let i = 0; i < ver.length; i++) {
arr[i+4] = ver.charCodeAt(i);
}
arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' ';
arr[11] = '\n';
client._sock._websocket._receiveData(arr);
}
function sendSecurity(type, cl) {
cl._sock._websocket._receiveData(new Uint8Array([1, type]));
}
describe('ProtocolVersion', function () {
describe('version parsing', function () {
it('should interpret version 003.003 as version 3.3', function () {
sendVer('003.003', client);
@ -1127,44 +1131,24 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('Security', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
sendVer('003.008\n', client);
client._sock._websocket._getSentData();
});
it('should simply receive the auth scheme when for versions < 3.7', function () {
client._rfbVersion = 3.6;
const authSchemeRaw = [1, 2, 3, 4];
const authScheme = (authSchemeRaw[0] << 24) + (authSchemeRaw[1] << 16) +
(authSchemeRaw[2] << 8) + authSchemeRaw[3];
client._sock._websocket._receiveData(new Uint8Array(authSchemeRaw));
expect(client._rfbAuthScheme).to.equal(authScheme);
});
it('should prefer no authentication is possible', function () {
client._rfbVersion = 3.7;
const authSchemes = [2, 1, 3];
it('should respect server preference order', function () {
const authSchemes = [ 6, 79, 30, 188, 16, 6, 1 ];
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
expect(client._rfbAuthScheme).to.equal(1);
expect(client._sock).to.have.sent(new Uint8Array([1, 1]));
expect(client._sock).to.have.sent(new Uint8Array([30]));
});
it('should choose for the most prefered scheme possible for versions >= 3.7', function () {
client._rfbVersion = 3.7;
const authSchemes = [2, 22, 16];
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
expect(client._rfbAuthScheme).to.equal(22);
expect(client._sock).to.have.sent(new Uint8Array([22]));
});
it('should fail if there are no supported schemes for versions >= 3.7', function () {
it('should fail if there are no supported schemes', function () {
sinon.spy(client, "_fail");
client._rfbVersion = 3.7;
const authSchemes = [1, 32];
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
expect(client._fail).to.have.been.calledOnce;
});
it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () {
client._rfbVersion = 3.7;
it('should fail with the appropriate message if no types are sent', function () {
const failureData = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
sinon.spy(client, '_fail');
client._sock._websocket._receiveData(new Uint8Array(failureData));
@ -1175,7 +1159,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
it('should transition to the Authentication state and continue on successful negotiation', function () {
client._rfbVersion = 3.7;
const authSchemes = [1, 1];
client._negotiateAuthentication = sinon.spy();
client._sock._websocket._receiveData(new Uint8Array(authSchemes));
@ -1184,17 +1167,8 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
describe('Authentication', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
});
function sendSecurity(type, cl) {
cl._sock._websocket._receiveData(new Uint8Array([1, type]));
}
describe('Legacy Authentication', function () {
it('should fail on auth scheme 0 (pre 3.7) with the given message', function () {
client._rfbVersion = 3.6;
const errMsg = "Whoopsies";
const data = [0, 0, 0, 0];
const errLen = errMsg.length;
@ -1203,37 +1177,42 @@ describe('Remote Frame Buffer Protocol Client', function () {
data.push(errMsg.charCodeAt(i));
}
sendVer('003.006\n', client);
client._sock._websocket._getSentData();
sinon.spy(client, '_fail');
client._sock._websocket._receiveData(new Uint8Array(data));
expect(client._fail).to.have.been.calledWith(
'Security negotiation failed on authentication scheme (reason: Whoopsies)');
});
it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
client._rfbVersion = 3.8;
it('should transition straight to ServerInitialisation on "no auth" for versions < 3.7', function () {
sendVer('003.006\n', client);
client._sock._websocket._getSentData();
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));
expect(client._rfbInitState).to.equal('ServerInitialisation');
});
});
describe('Authentication', function () {
beforeEach(function () {
sendVer('003.008\n', client);
client._sock._websocket._getSentData();
});
it('should transition straight to SecurityResult on "no auth" (1)', function () {
sendSecurity(1, client);
expect(client._rfbInitState).to.equal('SecurityResult');
});
it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () {
client._rfbVersion = 3.7;
sendSecurity(1, client);
expect(client._rfbInitState).to.equal('ServerInitialisation');
});
it('should fail on an unknown auth scheme', function () {
sinon.spy(client, "_fail");
client._rfbVersion = 3.8;
sendSecurity(57, client);
expect(client._fail).to.have.been.calledOnce;
});
describe('VNC Authentication (type 2) Handler', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
client._rfbVersion = 3.8;
});
it('should fire the credentialsrequired event if missing a password', function () {
const spy = sinon.spy();
client.addEventListener("credentialsrequired", spy);
@ -1274,12 +1253,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
describe('ARD Authentication (type 30) Handler', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
client._rfbVersion = 3.8;
});
it('should fire the credentialsrequired event if all credentials are missing', function () {
const spy = sinon.spy();
client.addEventListener("credentialsrequired", spy);
@ -1347,11 +1320,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
describe('XVP Authentication (type 22) Handler', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
client._rfbVersion = 3.8;
});
it('should fall through to standard VNC authentication upon completion', function () {
client._rfbCredentials = { username: 'user',
target: 'target',
@ -1400,8 +1368,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('TightVNC Authentication (type 16) Handler', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
client._rfbVersion = 3.8;
sendSecurity(16, client);
client._sock._websocket._getSentData(); // skip the security reply
});
@ -1487,8 +1453,6 @@ describe('Remote Frame Buffer Protocol Client', function () {
describe('VeNCrypt Authentication (type 19) Handler', function () {
beforeEach(function () {
client._rfbInitState = 'Security';
client._rfbVersion = 3.8;
sendSecurity(19, client);
expect(client._sock).to.have.sent(new Uint8Array([19]));
});
@ -1499,18 +1463,70 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(client._fail).to.have.been.calledOnce;
});
it('should fail if the Plain authentication is not present', function () {
it('should fail if there are no supported subtypes', function () {
// VeNCrypt version
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
// Server ACK.
client._sock._websocket._receiveData(new Uint8Array([0]));
// Subtype list, only list subtype 1.
// Subtype list
sinon.spy(client, "_fail");
client._sock._websocket._receiveData(new Uint8Array([1, 0, 0, 0, 1]));
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 9, 0, 0, 1, 4]));
expect(client._fail).to.have.been.calledOnce;
});
it('should support standard types', function () {
// VeNCrypt version
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
// Server ACK.
client._sock._websocket._receiveData(new Uint8Array([0]));
// Subtype list
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 2, 0, 0, 1, 4]));
let expectedResponse = [];
push32(expectedResponse, 2); // Chosen subtype.
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
});
it('should respect server preference order', function () {
// VeNCrypt version
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
// Server ACK.
client._sock._websocket._receiveData(new Uint8Array([0]));
// Subtype list
let subtypes = [ 6 ];
push32(subtypes, 79);
push32(subtypes, 30);
push32(subtypes, 188);
push32(subtypes, 256);
push32(subtypes, 6);
push32(subtypes, 1);
client._sock._websocket._receiveData(new Uint8Array(subtypes));
let expectedResponse = [];
push32(expectedResponse, 30); // Chosen subtype.
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
});
it('should ignore redundant VeNCrypt subtype', function () {
// VeNCrypt version
client._sock._websocket._receiveData(new Uint8Array([0, 2]));
expect(client._sock).to.have.sent(new Uint8Array([0, 2]));
// Server ACK.
client._sock._websocket._receiveData(new Uint8Array([0]));
// Subtype list
client._sock._websocket._receiveData(new Uint8Array([2, 0, 0, 0, 19, 0, 0, 0, 2]));
let expectedResponse = [];
push32(expectedResponse, 2); // Chosen subtype.
expect(client._sock).to.have.sent(new Uint8Array(expectedResponse));
});
it('should support Plain authentication', function () {
client._rfbCredentials = { username: 'username', password: 'password' };
// VeNCrypt version
@ -1582,9 +1598,30 @@ describe('Remote Frame Buffer Protocol Client', function () {
});
});
describe('Legacy SecurityResult', function () {
beforeEach(function () {
sendVer('003.007\n', client);
client._sock._websocket._getSentData();
sendSecurity(1, client);
client._sock._websocket._getSentData();
});
it('should not include reason in securityfailure event', function () {
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(2);
expect('reason' in spy.args[0][0].detail).to.be.false;
});
});
describe('SecurityResult', function () {
beforeEach(function () {
client._rfbInitState = 'SecurityResult';
sendVer('003.008\n', client);
client._sock._websocket._getSentData();
sendSecurity(1, client);
client._sock._websocket._getSentData();
});
it('should fall through to ServerInitialisation on a response code of 0', function () {
@ -1592,60 +1629,26 @@ describe('Remote Frame Buffer Protocol Client', function () {
expect(client._rfbInitState).to.equal('ServerInitialisation');
});
it('should fail on an error code of 1 with the given message for versions >= 3.8', function () {
client._rfbVersion = 3.8;
sinon.spy(client, '_fail');
const failureData = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
client._sock._websocket._receiveData(new Uint8Array(failureData));
expect(client._fail).to.have.been.calledWith(
'Security negotiation failed on security result (reason: whoops)');
});
it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
sinon.spy(client, '_fail');
client._rfbVersion = 3.7;
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 1]));
expect(client._fail).to.have.been.calledWith(
'Security handshake failed');
});
it('should result in securityfailure event when receiving a non zero status', function () {
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(2);
});
it('should include reason when provided in securityfailure event', function () {
client._rfbVersion = 3.8;
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
const failureData = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,
32, 102, 97, 105, 108, 117, 114, 101];
client._sock._websocket._receiveData(new Uint8Array(failureData));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(1);
expect(spy.args[0][0].detail.reason).to.equal('such failure');
});
it('should not include reason when length is zero in securityfailure event', function () {
client._rfbVersion = 3.9;
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
const failureData = [0, 0, 0, 1, 0, 0, 0, 0];
client._sock._websocket._receiveData(new Uint8Array(failureData));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(1);
expect('reason' in spy.args[0][0].detail).to.be.false;
});
it('should not include reason in securityfailure event for version < 3.8', function () {
client._rfbVersion = 3.6;
const spy = sinon.spy();
client.addEventListener("securityfailure", spy);
client._sock._websocket._receiveData(new Uint8Array([0, 0, 0, 2]));
expect(spy.args[0][0].detail.status).to.equal(2);
expect('reason' in spy.args[0][0].detail).to.be.false;
});
});
describe('ClientInitialisation', function () {