@novnc/novnc 1.3.0 → 1.4.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORS +2 -2
- package/LICENSE.txt +1 -1
- package/README.md +23 -7
- package/core/decoders/jpeg.js +141 -0
- package/core/decoders/raw.js +1 -1
- package/core/decoders/zrle.js +185 -0
- package/core/des.js +1 -1
- package/core/display.js +12 -0
- package/core/encodings.js +4 -0
- package/core/input/keyboard.js +10 -0
- package/core/ra2.js +567 -0
- package/core/rfb.js +469 -84
- package/core/util/browser.js +56 -7
- package/core/util/cursor.js +4 -0
- package/core/util/md5.js +79 -0
- package/docs/API.md +318 -157
- package/lib/base64.js +20 -34
- package/lib/decoders/copyrect.js +5 -12
- package/lib/decoders/hextile.js +17 -47
- package/lib/decoders/jpeg.js +149 -0
- package/lib/decoders/raw.js +10 -23
- package/lib/decoders/rre.js +5 -16
- package/lib/decoders/tight.js +13 -79
- package/lib/decoders/tightpng.js +8 -28
- package/lib/decoders/zrle.js +188 -0
- package/lib/deflator.js +9 -23
- package/lib/des.js +24 -37
- package/lib/display.js +62 -108
- package/lib/encodings.js +7 -8
- package/lib/inflator.js +6 -19
- package/lib/input/domkeytable.js +77 -48
- package/lib/input/fixedkeys.js +8 -3
- package/lib/input/gesturehandler.js +86 -153
- package/lib/input/keyboard.js +62 -91
- package/lib/input/keysym.js +14 -270
- package/lib/input/keysymdef.js +5 -7
- package/lib/input/util.js +43 -85
- package/lib/input/vkeys.js +0 -3
- package/lib/input/xtscancodes.js +1 -168
- package/lib/ra2.js +1005 -0
- package/lib/rfb.js +795 -923
- package/lib/util/browser.js +66 -29
- package/lib/util/cursor.js +29 -66
- package/lib/util/element.js +3 -5
- package/lib/util/events.js +23 -30
- package/lib/util/eventtarget.js +5 -14
- package/lib/util/int.js +1 -2
- package/lib/util/logging.js +1 -19
- package/lib/util/md5.js +77 -0
- package/lib/util/strings.js +3 -5
- package/lib/vendor/pako/lib/utils/common.js +8 -17
- package/lib/vendor/pako/lib/zlib/adler32.js +3 -7
- package/lib/vendor/pako/lib/zlib/constants.js +2 -5
- package/lib/vendor/pako/lib/zlib/crc32.js +5 -12
- package/lib/vendor/pako/lib/zlib/deflate.js +213 -618
- package/lib/vendor/pako/lib/zlib/gzheader.js +1 -13
- package/lib/vendor/pako/lib/zlib/inffast.js +60 -176
- package/lib/vendor/pako/lib/zlib/inflate.js +398 -888
- package/lib/vendor/pako/lib/zlib/inftrees.js +63 -169
- package/lib/vendor/pako/lib/zlib/messages.js +1 -11
- package/lib/vendor/pako/lib/zlib/trees.js +246 -588
- package/lib/vendor/pako/lib/zlib/zstream.js +2 -18
- package/lib/websock.js +37 -88
- package/package.json +32 -35
package/core/rfb.js
CHANGED
|
@@ -25,6 +25,8 @@ import DES from "./des.js";
|
|
|
25
25
|
import KeyTable from "./input/keysym.js";
|
|
26
26
|
import XtScancode from "./input/xtscancodes.js";
|
|
27
27
|
import { encodings } from "./encodings.js";
|
|
28
|
+
import RSAAESAuthenticationState from "./ra2.js";
|
|
29
|
+
import { MD5 } from "./util/md5.js";
|
|
28
30
|
|
|
29
31
|
import RawDecoder from "./decoders/raw.js";
|
|
30
32
|
import CopyRectDecoder from "./decoders/copyrect.js";
|
|
@@ -32,6 +34,8 @@ import RREDecoder from "./decoders/rre.js";
|
|
|
32
34
|
import HextileDecoder from "./decoders/hextile.js";
|
|
33
35
|
import TightDecoder from "./decoders/tight.js";
|
|
34
36
|
import TightPNGDecoder from "./decoders/tightpng.js";
|
|
37
|
+
import ZRLEDecoder from "./decoders/zrle.js";
|
|
38
|
+
import JPEGDecoder from "./decoders/jpeg.js";
|
|
35
39
|
|
|
36
40
|
// How many seconds to wait for a disconnect to finish
|
|
37
41
|
const DISCONNECT_TIMEOUT = 3;
|
|
@@ -50,6 +54,22 @@ const GESTURE_SCRLSENS = 50;
|
|
|
50
54
|
const DOUBLE_TAP_TIMEOUT = 1000;
|
|
51
55
|
const DOUBLE_TAP_THRESHOLD = 50;
|
|
52
56
|
|
|
57
|
+
// Security types
|
|
58
|
+
const securityTypeNone = 1;
|
|
59
|
+
const securityTypeVNCAuth = 2;
|
|
60
|
+
const securityTypeRA2ne = 6;
|
|
61
|
+
const securityTypeTight = 16;
|
|
62
|
+
const securityTypeVeNCrypt = 19;
|
|
63
|
+
const securityTypeXVP = 22;
|
|
64
|
+
const securityTypeARD = 30;
|
|
65
|
+
const securityTypeMSLogonII = 113;
|
|
66
|
+
|
|
67
|
+
// Special Tight security types
|
|
68
|
+
const securityTypeUnixLogon = 129;
|
|
69
|
+
|
|
70
|
+
// VeNCrypt security types
|
|
71
|
+
const securityTypePlain = 256;
|
|
72
|
+
|
|
53
73
|
// Extended clipboard pseudo-encoding formats
|
|
54
74
|
const extendedClipboardFormatText = 1;
|
|
55
75
|
/*eslint-disable no-unused-vars */
|
|
@@ -75,6 +95,12 @@ export default class RFB extends EventTargetMixin {
|
|
|
75
95
|
throw new Error("Must specify URL, WebSocket or RTCDataChannel");
|
|
76
96
|
}
|
|
77
97
|
|
|
98
|
+
// We rely on modern APIs which might not be available in an
|
|
99
|
+
// insecure context
|
|
100
|
+
if (!window.isSecureContext) {
|
|
101
|
+
Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
|
|
102
|
+
}
|
|
103
|
+
|
|
78
104
|
super();
|
|
79
105
|
|
|
80
106
|
this._target = target;
|
|
@@ -98,6 +124,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
98
124
|
this._rfbInitState = '';
|
|
99
125
|
this._rfbAuthScheme = -1;
|
|
100
126
|
this._rfbCleanDisconnect = true;
|
|
127
|
+
this._rfbRSAAESAuthenticationState = null;
|
|
101
128
|
|
|
102
129
|
// Server capabilities
|
|
103
130
|
this._rfbVersion = 0;
|
|
@@ -176,6 +203,8 @@ export default class RFB extends EventTargetMixin {
|
|
|
176
203
|
handleMouse: this._handleMouse.bind(this),
|
|
177
204
|
handleWheel: this._handleWheel.bind(this),
|
|
178
205
|
handleGesture: this._handleGesture.bind(this),
|
|
206
|
+
handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
|
|
207
|
+
handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
|
|
179
208
|
};
|
|
180
209
|
|
|
181
210
|
// main setup
|
|
@@ -218,6 +247,8 @@ export default class RFB extends EventTargetMixin {
|
|
|
218
247
|
this._decoders[encodings.encodingHextile] = new HextileDecoder();
|
|
219
248
|
this._decoders[encodings.encodingTight] = new TightDecoder();
|
|
220
249
|
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
|
|
250
|
+
this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
|
|
251
|
+
this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
|
|
221
252
|
|
|
222
253
|
// NB: nothing that needs explicit teardown should be done
|
|
223
254
|
// before this point, since this can throw an exception
|
|
@@ -240,6 +271,8 @@ export default class RFB extends EventTargetMixin {
|
|
|
240
271
|
this._sock.on('message', this._handleMessage.bind(this));
|
|
241
272
|
this._sock.on('error', this._socketError.bind(this));
|
|
242
273
|
|
|
274
|
+
this._expectedClientWidth = null;
|
|
275
|
+
this._expectedClientHeight = null;
|
|
243
276
|
this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
|
|
244
277
|
|
|
245
278
|
// All prepared, kick off the connection
|
|
@@ -254,6 +287,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
254
287
|
|
|
255
288
|
this._viewOnly = false;
|
|
256
289
|
this._clipViewport = false;
|
|
290
|
+
this._clippingViewport = false;
|
|
257
291
|
this._scaleViewport = false;
|
|
258
292
|
this._resizeSession = false;
|
|
259
293
|
|
|
@@ -285,6 +319,16 @@ export default class RFB extends EventTargetMixin {
|
|
|
285
319
|
|
|
286
320
|
get capabilities() { return this._capabilities; }
|
|
287
321
|
|
|
322
|
+
get clippingViewport() { return this._clippingViewport; }
|
|
323
|
+
_setClippingViewport(on) {
|
|
324
|
+
if (on === this._clippingViewport) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this._clippingViewport = on;
|
|
328
|
+
this.dispatchEvent(new CustomEvent("clippingviewport",
|
|
329
|
+
{ detail: this._clippingViewport }));
|
|
330
|
+
}
|
|
331
|
+
|
|
288
332
|
get touchButton() { return 0; }
|
|
289
333
|
set touchButton(button) { Log.Warn("Using old API!"); }
|
|
290
334
|
|
|
@@ -372,11 +416,20 @@ export default class RFB extends EventTargetMixin {
|
|
|
372
416
|
this._sock.off('error');
|
|
373
417
|
this._sock.off('message');
|
|
374
418
|
this._sock.off('open');
|
|
419
|
+
if (this._rfbRSAAESAuthenticationState !== null) {
|
|
420
|
+
this._rfbRSAAESAuthenticationState.disconnect();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
approveServer() {
|
|
425
|
+
if (this._rfbRSAAESAuthenticationState !== null) {
|
|
426
|
+
this._rfbRSAAESAuthenticationState.approveServer();
|
|
427
|
+
}
|
|
375
428
|
}
|
|
376
429
|
|
|
377
430
|
sendCredentials(creds) {
|
|
378
431
|
this._rfbCredentials = creds;
|
|
379
|
-
|
|
432
|
+
this._resumeAuthentication();
|
|
380
433
|
}
|
|
381
434
|
|
|
382
435
|
sendCtrlAltDel() {
|
|
@@ -432,8 +485,8 @@ export default class RFB extends EventTargetMixin {
|
|
|
432
485
|
}
|
|
433
486
|
}
|
|
434
487
|
|
|
435
|
-
focus() {
|
|
436
|
-
this._canvas.focus();
|
|
488
|
+
focus(options) {
|
|
489
|
+
this._canvas.focus(options);
|
|
437
490
|
}
|
|
438
491
|
|
|
439
492
|
blur() {
|
|
@@ -449,16 +502,45 @@ export default class RFB extends EventTargetMixin {
|
|
|
449
502
|
this._clipboardText = text;
|
|
450
503
|
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
|
|
451
504
|
} else {
|
|
452
|
-
let
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
505
|
+
let length, i;
|
|
506
|
+
let data;
|
|
507
|
+
|
|
508
|
+
length = 0;
|
|
509
|
+
// eslint-disable-next-line no-unused-vars
|
|
510
|
+
for (let codePoint of text) {
|
|
511
|
+
length++;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
data = new Uint8Array(length);
|
|
515
|
+
|
|
516
|
+
i = 0;
|
|
517
|
+
for (let codePoint of text) {
|
|
518
|
+
let code = codePoint.codePointAt(0);
|
|
519
|
+
|
|
520
|
+
/* Only ISO 8859-1 is supported */
|
|
521
|
+
if (code > 0xff) {
|
|
522
|
+
code = 0x3f; // '?'
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
data[i++] = code;
|
|
456
526
|
}
|
|
457
527
|
|
|
458
528
|
RFB.messages.clientCutText(this._sock, data);
|
|
459
529
|
}
|
|
460
530
|
}
|
|
461
531
|
|
|
532
|
+
getImageData() {
|
|
533
|
+
return this._display.getImageData();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
toDataURL(type, encoderOptions) {
|
|
537
|
+
return this._display.toDataURL(type, encoderOptions);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
toBlob(callback, type, quality) {
|
|
541
|
+
return this._display.toBlob(callback, type, quality);
|
|
542
|
+
}
|
|
543
|
+
|
|
462
544
|
// ===== PRIVATE METHODS =====
|
|
463
545
|
|
|
464
546
|
_connect() {
|
|
@@ -609,7 +691,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
609
691
|
return;
|
|
610
692
|
}
|
|
611
693
|
|
|
612
|
-
this.focus();
|
|
694
|
+
this.focus({ preventScroll: true });
|
|
613
695
|
}
|
|
614
696
|
|
|
615
697
|
_setDesktopName(name) {
|
|
@@ -619,7 +701,26 @@ export default class RFB extends EventTargetMixin {
|
|
|
619
701
|
{ detail: { name: this._fbName } }));
|
|
620
702
|
}
|
|
621
703
|
|
|
704
|
+
_saveExpectedClientSize() {
|
|
705
|
+
this._expectedClientWidth = this._screen.clientWidth;
|
|
706
|
+
this._expectedClientHeight = this._screen.clientHeight;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
_currentClientSize() {
|
|
710
|
+
return [this._screen.clientWidth, this._screen.clientHeight];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
_clientHasExpectedSize() {
|
|
714
|
+
const [currentWidth, currentHeight] = this._currentClientSize();
|
|
715
|
+
return currentWidth == this._expectedClientWidth &&
|
|
716
|
+
currentHeight == this._expectedClientHeight;
|
|
717
|
+
}
|
|
718
|
+
|
|
622
719
|
_handleResize() {
|
|
720
|
+
// Don't change anything if the client size is already as expected
|
|
721
|
+
if (this._clientHasExpectedSize()) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
623
724
|
// If the window resized then our screen element might have
|
|
624
725
|
// as well. Update the viewport dimensions.
|
|
625
726
|
window.requestAnimationFrame(() => {
|
|
@@ -659,6 +760,16 @@ export default class RFB extends EventTargetMixin {
|
|
|
659
760
|
const size = this._screenSize();
|
|
660
761
|
this._display.viewportChangeSize(size.w, size.h);
|
|
661
762
|
this._fixScrollbars();
|
|
763
|
+
this._setClippingViewport(size.w < this._display.width ||
|
|
764
|
+
size.h < this._display.height);
|
|
765
|
+
} else {
|
|
766
|
+
this._setClippingViewport(false);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// When changing clipping we might show or hide scrollbars.
|
|
770
|
+
// This causes the expected client dimensions to change.
|
|
771
|
+
if (curClip !== newClip) {
|
|
772
|
+
this._saveExpectedClientSize();
|
|
662
773
|
}
|
|
663
774
|
}
|
|
664
775
|
|
|
@@ -684,6 +795,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
684
795
|
}
|
|
685
796
|
|
|
686
797
|
const size = this._screenSize();
|
|
798
|
+
|
|
687
799
|
RFB.messages.setDesktopSize(this._sock,
|
|
688
800
|
Math.floor(size.w), Math.floor(size.h),
|
|
689
801
|
this._screenID, this._screenFlags);
|
|
@@ -699,12 +811,13 @@ export default class RFB extends EventTargetMixin {
|
|
|
699
811
|
}
|
|
700
812
|
|
|
701
813
|
_fixScrollbars() {
|
|
702
|
-
// This is a hack because
|
|
703
|
-
// for when scrollbars are needed.
|
|
704
|
-
//
|
|
814
|
+
// This is a hack because Safari on macOS screws up the calculation
|
|
815
|
+
// for when scrollbars are needed. We get scrollbars when making the
|
|
816
|
+
// browser smaller, despite remote resize being enabled. So to fix it
|
|
817
|
+
// we temporarily toggle them off and on.
|
|
705
818
|
const orig = this._screen.style.overflow;
|
|
706
819
|
this._screen.style.overflow = 'hidden';
|
|
707
|
-
// Force
|
|
820
|
+
// Force Safari to recalculate the layout by asking for
|
|
708
821
|
// an element's dimensions
|
|
709
822
|
this._screen.getBoundingClientRect();
|
|
710
823
|
this._screen.style.overflow = orig;
|
|
@@ -869,8 +982,15 @@ export default class RFB extends EventTargetMixin {
|
|
|
869
982
|
}
|
|
870
983
|
}
|
|
871
984
|
break;
|
|
985
|
+
case 'connecting':
|
|
986
|
+
while (this._rfbConnectionState === 'connecting') {
|
|
987
|
+
if (!this._initMsg()) {
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
break;
|
|
872
992
|
default:
|
|
873
|
-
|
|
993
|
+
Log.Error("Got data while in an invalid state");
|
|
874
994
|
break;
|
|
875
995
|
}
|
|
876
996
|
}
|
|
@@ -1242,13 +1362,13 @@ export default class RFB extends EventTargetMixin {
|
|
|
1242
1362
|
break;
|
|
1243
1363
|
case "003.003":
|
|
1244
1364
|
case "003.006": // UltraVNC
|
|
1245
|
-
case "003.889": // Apple Remote Desktop
|
|
1246
1365
|
this._rfbVersion = 3.3;
|
|
1247
1366
|
break;
|
|
1248
1367
|
case "003.007":
|
|
1249
1368
|
this._rfbVersion = 3.7;
|
|
1250
1369
|
break;
|
|
1251
1370
|
case "003.008":
|
|
1371
|
+
case "003.889": // Apple Remote Desktop
|
|
1252
1372
|
case "004.000": // Intel AMT KVM
|
|
1253
1373
|
case "004.001": // RealVNC 4.6
|
|
1254
1374
|
case "005.000": // RealVNC 5.3
|
|
@@ -1279,6 +1399,22 @@ export default class RFB extends EventTargetMixin {
|
|
|
1279
1399
|
this._rfbInitState = 'Security';
|
|
1280
1400
|
}
|
|
1281
1401
|
|
|
1402
|
+
_isSupportedSecurityType(type) {
|
|
1403
|
+
const clientTypes = [
|
|
1404
|
+
securityTypeNone,
|
|
1405
|
+
securityTypeVNCAuth,
|
|
1406
|
+
securityTypeRA2ne,
|
|
1407
|
+
securityTypeTight,
|
|
1408
|
+
securityTypeVeNCrypt,
|
|
1409
|
+
securityTypeXVP,
|
|
1410
|
+
securityTypeARD,
|
|
1411
|
+
securityTypeMSLogonII,
|
|
1412
|
+
securityTypePlain,
|
|
1413
|
+
];
|
|
1414
|
+
|
|
1415
|
+
return clientTypes.includes(type);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1282
1418
|
_negotiateSecurity() {
|
|
1283
1419
|
if (this._rfbVersion >= 3.7) {
|
|
1284
1420
|
// Server sends supported list, client decides
|
|
@@ -1289,24 +1425,23 @@ export default class RFB extends EventTargetMixin {
|
|
|
1289
1425
|
this._rfbInitState = "SecurityReason";
|
|
1290
1426
|
this._securityContext = "no security types";
|
|
1291
1427
|
this._securityStatus = 1;
|
|
1292
|
-
return
|
|
1428
|
+
return true;
|
|
1293
1429
|
}
|
|
1294
1430
|
|
|
1295
1431
|
const types = this._sock.rQshiftBytes(numTypes);
|
|
1296
1432
|
Log.Debug("Server security types: " + types);
|
|
1297
1433
|
|
|
1298
|
-
// Look for
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
this.
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
} else {
|
|
1434
|
+
// Look for a matching security type in the order that the
|
|
1435
|
+
// server prefers
|
|
1436
|
+
this._rfbAuthScheme = -1;
|
|
1437
|
+
for (let type of types) {
|
|
1438
|
+
if (this._isSupportedSecurityType(type)) {
|
|
1439
|
+
this._rfbAuthScheme = type;
|
|
1440
|
+
break;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (this._rfbAuthScheme === -1) {
|
|
1310
1445
|
return this._fail("Unsupported security types (types: " + types + ")");
|
|
1311
1446
|
}
|
|
1312
1447
|
|
|
@@ -1320,14 +1455,14 @@ export default class RFB extends EventTargetMixin {
|
|
|
1320
1455
|
this._rfbInitState = "SecurityReason";
|
|
1321
1456
|
this._securityContext = "authentication scheme";
|
|
1322
1457
|
this._securityStatus = 1;
|
|
1323
|
-
return
|
|
1458
|
+
return true;
|
|
1324
1459
|
}
|
|
1325
1460
|
}
|
|
1326
1461
|
|
|
1327
1462
|
this._rfbInitState = 'Authentication';
|
|
1328
1463
|
Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
|
|
1329
1464
|
|
|
1330
|
-
return
|
|
1465
|
+
return true;
|
|
1331
1466
|
}
|
|
1332
1467
|
|
|
1333
1468
|
_handleSecurityReason() {
|
|
@@ -1377,7 +1512,7 @@ export default class RFB extends EventTargetMixin {
|
|
|
1377
1512
|
this._rfbCredentials.username +
|
|
1378
1513
|
this._rfbCredentials.target;
|
|
1379
1514
|
this._sock.sendString(xvpAuthStr);
|
|
1380
|
-
this._rfbAuthScheme =
|
|
1515
|
+
this._rfbAuthScheme = securityTypeVNCAuth;
|
|
1381
1516
|
return this._negotiateAuthentication();
|
|
1382
1517
|
}
|
|
1383
1518
|
|
|
@@ -1435,49 +1570,66 @@ export default class RFB extends EventTargetMixin {
|
|
|
1435
1570
|
subtypes.push(this._sock.rQshift32());
|
|
1436
1571
|
}
|
|
1437
1572
|
|
|
1438
|
-
//
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1573
|
+
// Look for a matching security type in the order that the
|
|
1574
|
+
// server prefers
|
|
1575
|
+
this._rfbAuthScheme = -1;
|
|
1576
|
+
for (let type of subtypes) {
|
|
1577
|
+
// Avoid getting in to a loop
|
|
1578
|
+
if (type === securityTypeVeNCrypt) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (this._isSupportedSecurityType(type)) {
|
|
1583
|
+
this._rfbAuthScheme = type;
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1445
1586
|
}
|
|
1446
|
-
}
|
|
1447
1587
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
if (this._rfbCredentials.username === undefined ||
|
|
1451
|
-
this._rfbCredentials.password === undefined) {
|
|
1452
|
-
this.dispatchEvent(new CustomEvent(
|
|
1453
|
-
"credentialsrequired",
|
|
1454
|
-
{ detail: { types: ["username", "password"] } }));
|
|
1455
|
-
return false;
|
|
1588
|
+
if (this._rfbAuthScheme === -1) {
|
|
1589
|
+
return this._fail("Unsupported security types (types: " + subtypes + ")");
|
|
1456
1590
|
}
|
|
1457
1591
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
(user.length >> 24) & 0xFF,
|
|
1463
|
-
(user.length >> 16) & 0xFF,
|
|
1464
|
-
(user.length >> 8) & 0xFF,
|
|
1465
|
-
user.length & 0xFF
|
|
1466
|
-
]);
|
|
1467
|
-
this._sock.send([
|
|
1468
|
-
(pass.length >> 24) & 0xFF,
|
|
1469
|
-
(pass.length >> 16) & 0xFF,
|
|
1470
|
-
(pass.length >> 8) & 0xFF,
|
|
1471
|
-
pass.length & 0xFF
|
|
1472
|
-
]);
|
|
1473
|
-
this._sock.sendString(user);
|
|
1474
|
-
this._sock.sendString(pass);
|
|
1592
|
+
this._sock.send([this._rfbAuthScheme >> 24,
|
|
1593
|
+
this._rfbAuthScheme >> 16,
|
|
1594
|
+
this._rfbAuthScheme >> 8,
|
|
1595
|
+
this._rfbAuthScheme]);
|
|
1475
1596
|
|
|
1476
|
-
this.
|
|
1597
|
+
this._rfbVeNCryptState == 4;
|
|
1477
1598
|
return true;
|
|
1478
1599
|
}
|
|
1479
1600
|
}
|
|
1480
1601
|
|
|
1602
|
+
_negotiatePlainAuth() {
|
|
1603
|
+
if (this._rfbCredentials.username === undefined ||
|
|
1604
|
+
this._rfbCredentials.password === undefined) {
|
|
1605
|
+
this.dispatchEvent(new CustomEvent(
|
|
1606
|
+
"credentialsrequired",
|
|
1607
|
+
{ detail: { types: ["username", "password"] } }));
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const user = encodeUTF8(this._rfbCredentials.username);
|
|
1612
|
+
const pass = encodeUTF8(this._rfbCredentials.password);
|
|
1613
|
+
|
|
1614
|
+
this._sock.send([
|
|
1615
|
+
(user.length >> 24) & 0xFF,
|
|
1616
|
+
(user.length >> 16) & 0xFF,
|
|
1617
|
+
(user.length >> 8) & 0xFF,
|
|
1618
|
+
user.length & 0xFF
|
|
1619
|
+
]);
|
|
1620
|
+
this._sock.send([
|
|
1621
|
+
(pass.length >> 24) & 0xFF,
|
|
1622
|
+
(pass.length >> 16) & 0xFF,
|
|
1623
|
+
(pass.length >> 8) & 0xFF,
|
|
1624
|
+
pass.length & 0xFF
|
|
1625
|
+
]);
|
|
1626
|
+
this._sock.sendString(user);
|
|
1627
|
+
this._sock.sendString(pass);
|
|
1628
|
+
|
|
1629
|
+
this._rfbInitState = "SecurityResult";
|
|
1630
|
+
return true;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1481
1633
|
_negotiateStdVNCAuth() {
|
|
1482
1634
|
if (this._sock.rQwait("auth challenge", 16)) { return false; }
|
|
1483
1635
|
|
|
@@ -1496,6 +1648,117 @@ export default class RFB extends EventTargetMixin {
|
|
|
1496
1648
|
return true;
|
|
1497
1649
|
}
|
|
1498
1650
|
|
|
1651
|
+
_negotiateARDAuth() {
|
|
1652
|
+
|
|
1653
|
+
if (this._rfbCredentials.username === undefined ||
|
|
1654
|
+
this._rfbCredentials.password === undefined) {
|
|
1655
|
+
this.dispatchEvent(new CustomEvent(
|
|
1656
|
+
"credentialsrequired",
|
|
1657
|
+
{ detail: { types: ["username", "password"] } }));
|
|
1658
|
+
return false;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (this._rfbCredentials.ardPublicKey != undefined &&
|
|
1662
|
+
this._rfbCredentials.ardCredentials != undefined) {
|
|
1663
|
+
// if the async web crypto is done return the results
|
|
1664
|
+
this._sock.send(this._rfbCredentials.ardCredentials);
|
|
1665
|
+
this._sock.send(this._rfbCredentials.ardPublicKey);
|
|
1666
|
+
this._rfbCredentials.ardCredentials = null;
|
|
1667
|
+
this._rfbCredentials.ardPublicKey = null;
|
|
1668
|
+
this._rfbInitState = "SecurityResult";
|
|
1669
|
+
return true;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (this._sock.rQwait("read ard", 4)) { return false; }
|
|
1673
|
+
|
|
1674
|
+
let generator = this._sock.rQshiftBytes(2); // DH base generator value
|
|
1675
|
+
|
|
1676
|
+
let keyLength = this._sock.rQshift16();
|
|
1677
|
+
|
|
1678
|
+
if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
|
|
1679
|
+
|
|
1680
|
+
// read the server values
|
|
1681
|
+
let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
|
|
1682
|
+
let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
|
|
1683
|
+
|
|
1684
|
+
let clientPrivateKey = window.crypto.getRandomValues(new Uint8Array(keyLength));
|
|
1685
|
+
let padding = Array.from(window.crypto.getRandomValues(new Uint8Array(64)), byte => String.fromCharCode(65+byte%26)).join('');
|
|
1686
|
+
|
|
1687
|
+
this._negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding);
|
|
1688
|
+
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
_modPow(base, exponent, modulus) {
|
|
1693
|
+
|
|
1694
|
+
let baseHex = "0x"+Array.from(base, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
1695
|
+
let exponentHex = "0x"+Array.from(exponent, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
1696
|
+
let modulusHex = "0x"+Array.from(modulus, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');
|
|
1697
|
+
|
|
1698
|
+
let b = BigInt(baseHex);
|
|
1699
|
+
let e = BigInt(exponentHex);
|
|
1700
|
+
let m = BigInt(modulusHex);
|
|
1701
|
+
let r = 1n;
|
|
1702
|
+
b = b % m;
|
|
1703
|
+
while (e > 0) {
|
|
1704
|
+
if (e % 2n === 1n) {
|
|
1705
|
+
r = (r * b) % m;
|
|
1706
|
+
}
|
|
1707
|
+
e = e / 2n;
|
|
1708
|
+
b = (b * b) % m;
|
|
1709
|
+
}
|
|
1710
|
+
let hexResult = r.toString(16);
|
|
1711
|
+
|
|
1712
|
+
while (hexResult.length/2<exponent.length || (hexResult.length%2 != 0)) {
|
|
1713
|
+
hexResult = "0"+hexResult;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
let bytesResult = [];
|
|
1717
|
+
for (let c = 0; c < hexResult.length; c += 2) {
|
|
1718
|
+
bytesResult.push(parseInt(hexResult.substr(c, 2), 16));
|
|
1719
|
+
}
|
|
1720
|
+
return bytesResult;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async _aesEcbEncrypt(string, key) {
|
|
1724
|
+
// perform AES-ECB blocks
|
|
1725
|
+
let keyString = Array.from(key, byte => String.fromCharCode(byte)).join('');
|
|
1726
|
+
let aesKey = await window.crypto.subtle.importKey("raw", MD5(keyString), {name: "AES-CBC"}, false, ["encrypt"]);
|
|
1727
|
+
let data = new Uint8Array(string.length);
|
|
1728
|
+
for (let i = 0; i < string.length; ++i) {
|
|
1729
|
+
data[i] = string.charCodeAt(i);
|
|
1730
|
+
}
|
|
1731
|
+
let encrypted = new Uint8Array(data.length);
|
|
1732
|
+
for (let i=0;i<data.length;i+=16) {
|
|
1733
|
+
let block = data.slice(i, i+16);
|
|
1734
|
+
let encryptedBlock = await window.crypto.subtle.encrypt({name: "AES-CBC", iv: block},
|
|
1735
|
+
aesKey, new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
|
1736
|
+
);
|
|
1737
|
+
encrypted.set((new Uint8Array(encryptedBlock)).slice(0, 16), i);
|
|
1738
|
+
}
|
|
1739
|
+
return encrypted;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async _negotiateARDAuthAsync(generator, keyLength, prime, serverPublicKey, clientPrivateKey, padding) {
|
|
1743
|
+
// calculate the DH keys
|
|
1744
|
+
let clientPublicKey = this._modPow(generator, clientPrivateKey, prime);
|
|
1745
|
+
let sharedKey = this._modPow(serverPublicKey, clientPrivateKey, prime);
|
|
1746
|
+
|
|
1747
|
+
let username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
|
|
1748
|
+
let password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
|
1749
|
+
|
|
1750
|
+
let paddedUsername = username + '\0' + padding.substring(0, 63);
|
|
1751
|
+
let paddedPassword = password + '\0' + padding.substring(0, 63);
|
|
1752
|
+
let credentials = paddedUsername.substring(0, 64) + paddedPassword.substring(0, 64);
|
|
1753
|
+
|
|
1754
|
+
let encrypted = await this._aesEcbEncrypt(credentials, sharedKey);
|
|
1755
|
+
|
|
1756
|
+
this._rfbCredentials.ardCredentials = encrypted;
|
|
1757
|
+
this._rfbCredentials.ardPublicKey = clientPublicKey;
|
|
1758
|
+
|
|
1759
|
+
this._resumeAuthentication();
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1499
1762
|
_negotiateTightUnixAuth() {
|
|
1500
1763
|
if (this._rfbCredentials.username === undefined ||
|
|
1501
1764
|
this._rfbCredentials.password === undefined) {
|
|
@@ -1603,12 +1866,12 @@ export default class RFB extends EventTargetMixin {
|
|
|
1603
1866
|
case 'STDVNOAUTH__': // no auth
|
|
1604
1867
|
this._rfbInitState = 'SecurityResult';
|
|
1605
1868
|
return true;
|
|
1606
|
-
case 'STDVVNCAUTH_':
|
|
1607
|
-
this._rfbAuthScheme =
|
|
1608
|
-
return
|
|
1609
|
-
case 'TGHTULGNAUTH':
|
|
1610
|
-
this._rfbAuthScheme =
|
|
1611
|
-
return
|
|
1869
|
+
case 'STDVVNCAUTH_':
|
|
1870
|
+
this._rfbAuthScheme = securityTypeVNCAuth;
|
|
1871
|
+
return true;
|
|
1872
|
+
case 'TGHTULGNAUTH':
|
|
1873
|
+
this._rfbAuthScheme = securityTypeUnixLogon;
|
|
1874
|
+
return true;
|
|
1612
1875
|
default:
|
|
1613
1876
|
return this._fail("Unsupported tiny auth scheme " +
|
|
1614
1877
|
"(scheme: " + authType + ")");
|
|
@@ -1619,31 +1882,133 @@ export default class RFB extends EventTargetMixin {
|
|
|
1619
1882
|
return this._fail("No supported sub-auth types!");
|
|
1620
1883
|
}
|
|
1621
1884
|
|
|
1885
|
+
_handleRSAAESCredentialsRequired(event) {
|
|
1886
|
+
this.dispatchEvent(event);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
_handleRSAAESServerVerification(event) {
|
|
1890
|
+
this.dispatchEvent(event);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
_negotiateRA2neAuth() {
|
|
1894
|
+
if (this._rfbRSAAESAuthenticationState === null) {
|
|
1895
|
+
this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
|
|
1896
|
+
this._rfbRSAAESAuthenticationState.addEventListener(
|
|
1897
|
+
"serververification", this._eventHandlers.handleRSAAESServerVerification);
|
|
1898
|
+
this._rfbRSAAESAuthenticationState.addEventListener(
|
|
1899
|
+
"credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
|
|
1900
|
+
}
|
|
1901
|
+
this._rfbRSAAESAuthenticationState.checkInternalEvents();
|
|
1902
|
+
if (!this._rfbRSAAESAuthenticationState.hasStarted) {
|
|
1903
|
+
this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
|
|
1904
|
+
.catch((e) => {
|
|
1905
|
+
if (e.message !== "disconnect normally") {
|
|
1906
|
+
this._fail(e.message);
|
|
1907
|
+
}
|
|
1908
|
+
}).then(() => {
|
|
1909
|
+
this.dispatchEvent(new CustomEvent('securityresult'));
|
|
1910
|
+
this._rfbInitState = "SecurityResult";
|
|
1911
|
+
return true;
|
|
1912
|
+
}).finally(() => {
|
|
1913
|
+
this._rfbRSAAESAuthenticationState.removeEventListener(
|
|
1914
|
+
"serververification", this._eventHandlers.handleRSAAESServerVerification);
|
|
1915
|
+
this._rfbRSAAESAuthenticationState.removeEventListener(
|
|
1916
|
+
"credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
|
|
1917
|
+
this._rfbRSAAESAuthenticationState = null;
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
_negotiateMSLogonIIAuth() {
|
|
1924
|
+
if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
|
|
1925
|
+
|
|
1926
|
+
if (this._rfbCredentials.username === undefined ||
|
|
1927
|
+
this._rfbCredentials.password === undefined) {
|
|
1928
|
+
this.dispatchEvent(new CustomEvent(
|
|
1929
|
+
"credentialsrequired",
|
|
1930
|
+
{ detail: { types: ["username", "password"] } }));
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
const g = this._sock.rQshiftBytes(8);
|
|
1935
|
+
const p = this._sock.rQshiftBytes(8);
|
|
1936
|
+
const A = this._sock.rQshiftBytes(8);
|
|
1937
|
+
const b = window.crypto.getRandomValues(new Uint8Array(8));
|
|
1938
|
+
const B = new Uint8Array(this._modPow(g, b, p));
|
|
1939
|
+
const secret = new Uint8Array(this._modPow(A, b, p));
|
|
1940
|
+
|
|
1941
|
+
const des = new DES(secret);
|
|
1942
|
+
const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
|
|
1943
|
+
const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
|
|
1944
|
+
const usernameBytes = new Uint8Array(256);
|
|
1945
|
+
const passwordBytes = new Uint8Array(64);
|
|
1946
|
+
window.crypto.getRandomValues(usernameBytes);
|
|
1947
|
+
window.crypto.getRandomValues(passwordBytes);
|
|
1948
|
+
for (let i = 0; i < username.length; i++) {
|
|
1949
|
+
usernameBytes[i] = username.charCodeAt(i);
|
|
1950
|
+
}
|
|
1951
|
+
usernameBytes[username.length] = 0;
|
|
1952
|
+
for (let i = 0; i < password.length; i++) {
|
|
1953
|
+
passwordBytes[i] = password.charCodeAt(i);
|
|
1954
|
+
}
|
|
1955
|
+
passwordBytes[password.length] = 0;
|
|
1956
|
+
let x = new Uint8Array(secret);
|
|
1957
|
+
for (let i = 0; i < 32; i++) {
|
|
1958
|
+
for (let j = 0; j < 8; j++) {
|
|
1959
|
+
x[j] ^= usernameBytes[i * 8 + j];
|
|
1960
|
+
}
|
|
1961
|
+
x = des.enc8(x);
|
|
1962
|
+
usernameBytes.set(x, i * 8);
|
|
1963
|
+
}
|
|
1964
|
+
x = new Uint8Array(secret);
|
|
1965
|
+
for (let i = 0; i < 8; i++) {
|
|
1966
|
+
for (let j = 0; j < 8; j++) {
|
|
1967
|
+
x[j] ^= passwordBytes[i * 8 + j];
|
|
1968
|
+
}
|
|
1969
|
+
x = des.enc8(x);
|
|
1970
|
+
passwordBytes.set(x, i * 8);
|
|
1971
|
+
}
|
|
1972
|
+
this._sock.send(B);
|
|
1973
|
+
this._sock.send(usernameBytes);
|
|
1974
|
+
this._sock.send(passwordBytes);
|
|
1975
|
+
this._rfbInitState = "SecurityResult";
|
|
1976
|
+
return true;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1622
1979
|
_negotiateAuthentication() {
|
|
1623
1980
|
switch (this._rfbAuthScheme) {
|
|
1624
|
-
case
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
return true;
|
|
1628
|
-
}
|
|
1629
|
-
this._rfbInitState = 'ClientInitialisation';
|
|
1630
|
-
return this._initMsg();
|
|
1981
|
+
case securityTypeNone:
|
|
1982
|
+
this._rfbInitState = 'SecurityResult';
|
|
1983
|
+
return true;
|
|
1631
1984
|
|
|
1632
|
-
case
|
|
1985
|
+
case securityTypeXVP:
|
|
1633
1986
|
return this._negotiateXvpAuth();
|
|
1634
1987
|
|
|
1635
|
-
case
|
|
1988
|
+
case securityTypeARD:
|
|
1989
|
+
return this._negotiateARDAuth();
|
|
1990
|
+
|
|
1991
|
+
case securityTypeVNCAuth:
|
|
1636
1992
|
return this._negotiateStdVNCAuth();
|
|
1637
1993
|
|
|
1638
|
-
case
|
|
1994
|
+
case securityTypeTight:
|
|
1639
1995
|
return this._negotiateTightAuth();
|
|
1640
1996
|
|
|
1641
|
-
case
|
|
1997
|
+
case securityTypeVeNCrypt:
|
|
1642
1998
|
return this._negotiateVeNCryptAuth();
|
|
1643
1999
|
|
|
1644
|
-
case
|
|
2000
|
+
case securityTypePlain:
|
|
2001
|
+
return this._negotiatePlainAuth();
|
|
2002
|
+
|
|
2003
|
+
case securityTypeUnixLogon:
|
|
1645
2004
|
return this._negotiateTightUnixAuth();
|
|
1646
2005
|
|
|
2006
|
+
case securityTypeRA2ne:
|
|
2007
|
+
return this._negotiateRA2neAuth();
|
|
2008
|
+
|
|
2009
|
+
case securityTypeMSLogonII:
|
|
2010
|
+
return this._negotiateMSLogonIIAuth();
|
|
2011
|
+
|
|
1647
2012
|
default:
|
|
1648
2013
|
return this._fail("Unsupported auth scheme (scheme: " +
|
|
1649
2014
|
this._rfbAuthScheme + ")");
|
|
@@ -1651,6 +2016,13 @@ export default class RFB extends EventTargetMixin {
|
|
|
1651
2016
|
}
|
|
1652
2017
|
|
|
1653
2018
|
_handleSecurityResult() {
|
|
2019
|
+
// There is no security choice, and hence no security result
|
|
2020
|
+
// until RFB 3.7
|
|
2021
|
+
if (this._rfbVersion < 3.7) {
|
|
2022
|
+
this._rfbInitState = 'ClientInitialisation';
|
|
2023
|
+
return true;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1654
2026
|
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
|
|
1655
2027
|
|
|
1656
2028
|
const status = this._sock.rQshift32();
|
|
@@ -1658,13 +2030,13 @@ export default class RFB extends EventTargetMixin {
|
|
|
1658
2030
|
if (status === 0) { // OK
|
|
1659
2031
|
this._rfbInitState = 'ClientInitialisation';
|
|
1660
2032
|
Log.Debug('Authentication OK');
|
|
1661
|
-
return
|
|
2033
|
+
return true;
|
|
1662
2034
|
} else {
|
|
1663
2035
|
if (this._rfbVersion >= 3.8) {
|
|
1664
2036
|
this._rfbInitState = "SecurityReason";
|
|
1665
2037
|
this._securityContext = "security result";
|
|
1666
2038
|
this._securityStatus = status;
|
|
1667
|
-
return
|
|
2039
|
+
return true;
|
|
1668
2040
|
} else {
|
|
1669
2041
|
this.dispatchEvent(new CustomEvent(
|
|
1670
2042
|
"securityfailure",
|
|
@@ -1772,6 +2144,8 @@ export default class RFB extends EventTargetMixin {
|
|
|
1772
2144
|
if (this._fbDepth == 24) {
|
|
1773
2145
|
encs.push(encodings.encodingTight);
|
|
1774
2146
|
encs.push(encodings.encodingTightPNG);
|
|
2147
|
+
encs.push(encodings.encodingZRLE);
|
|
2148
|
+
encs.push(encodings.encodingJPEG);
|
|
1775
2149
|
encs.push(encodings.encodingHextile);
|
|
1776
2150
|
encs.push(encodings.encodingRRE);
|
|
1777
2151
|
}
|
|
@@ -1838,6 +2212,14 @@ export default class RFB extends EventTargetMixin {
|
|
|
1838
2212
|
}
|
|
1839
2213
|
}
|
|
1840
2214
|
|
|
2215
|
+
// Resume authentication handshake after it was paused for some
|
|
2216
|
+
// reason, e.g. waiting for a password from the user
|
|
2217
|
+
_resumeAuthentication() {
|
|
2218
|
+
// We use setTimeout() so it's run in its own context, just like
|
|
2219
|
+
// it originally did via the WebSocket's event handler
|
|
2220
|
+
setTimeout(this._initMsg.bind(this), 0);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1841
2223
|
_handleSetColourMapMsg() {
|
|
1842
2224
|
Log.Debug("SetColorMapEntries");
|
|
1843
2225
|
|
|
@@ -2500,6 +2882,9 @@ export default class RFB extends EventTargetMixin {
|
|
|
2500
2882
|
this._updateScale();
|
|
2501
2883
|
|
|
2502
2884
|
this._updateContinuousUpdates();
|
|
2885
|
+
|
|
2886
|
+
// Keep this size until browser client size changes
|
|
2887
|
+
this._saveExpectedClientSize();
|
|
2503
2888
|
}
|
|
2504
2889
|
|
|
2505
2890
|
_xvpOp(ver, op) {
|