main
1/*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2020 The noVNC authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
5 *
6 * See README.md for usage and integration instructions.
7 *
8 */
9
10import { toUnsigned32bit, toSigned32bit } from './util/int.js';
11import * as Log from './util/logging.js';
12import { encodeUTF8, decodeUTF8 } from './util/strings.js';
13import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';
14import { clientToElement } from './util/element.js';
15import { setCapture } from './util/events.js';
16import EventTargetMixin from './util/eventtarget.js';
17import Display from "./display.js";
18import AsyncClipboard from "./clipboard.js";
19import Inflator from "./inflator.js";
20import Deflator from "./deflator.js";
21import Keyboard from "./input/keyboard.js";
22import GestureHandler from "./input/gesturehandler.js";
23import Cursor from "./util/cursor.js";
24import Websock from "./websock.js";
25import KeyTable from "./input/keysym.js";
26import XtScancode from "./input/xtscancodes.js";
27import { encodings } from "./encodings.js";
28import RSAAESAuthenticationState from "./ra2.js";
29import legacyCrypto from "./crypto/crypto.js";
30
31import RawDecoder from "./decoders/raw.js";
32import CopyRectDecoder from "./decoders/copyrect.js";
33import RREDecoder from "./decoders/rre.js";
34import HextileDecoder from "./decoders/hextile.js";
35import ZlibDecoder from './decoders/zlib.js';
36import TightDecoder from "./decoders/tight.js";
37import TightPNGDecoder from "./decoders/tightpng.js";
38import ZRLEDecoder from "./decoders/zrle.js";
39import JPEGDecoder from "./decoders/jpeg.js";
40import H264Decoder from "./decoders/h264.js";
41
42// How many seconds to wait for a disconnect to finish
43const DISCONNECT_TIMEOUT = 3;
44const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
45
46// Minimum wait (ms) between two mouse moves
47const MOUSE_MOVE_DELAY = 17;
48
49// Wheel thresholds
50const WHEEL_STEP = 50; // Pixels needed for one step
51const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
52
53// Gesture thresholds
54const GESTURE_ZOOMSENS = 75;
55const GESTURE_SCRLSENS = 50;
56const DOUBLE_TAP_TIMEOUT = 1000;
57const DOUBLE_TAP_THRESHOLD = 50;
58
59// Security types
60const securityTypeNone = 1;
61const securityTypeVNCAuth = 2;
62const securityTypeRA2ne = 6;
63const securityTypeTight = 16;
64const securityTypeVeNCrypt = 19;
65const securityTypeXVP = 22;
66const securityTypeARD = 30;
67const securityTypeMSLogonII = 113;
68
69// Special Tight security types
70const securityTypeUnixLogon = 129;
71
72// VeNCrypt security types
73const securityTypePlain = 256;
74
75// Extended clipboard pseudo-encoding formats
76const extendedClipboardFormatText = 1;
77/*eslint-disable no-unused-vars */
78const extendedClipboardFormatRtf = 1 << 1;
79const extendedClipboardFormatHtml = 1 << 2;
80const extendedClipboardFormatDib = 1 << 3;
81const extendedClipboardFormatFiles = 1 << 4;
82/*eslint-enable */
83
84// Extended clipboard pseudo-encoding actions
85const extendedClipboardActionCaps = 1 << 24;
86const extendedClipboardActionRequest = 1 << 25;
87const extendedClipboardActionPeek = 1 << 26;
88const extendedClipboardActionNotify = 1 << 27;
89const extendedClipboardActionProvide = 1 << 28;
90
91export default class RFB extends EventTargetMixin {
92 constructor(target, urlOrChannel, options) {
93 if (!target) {
94 throw new Error("Must specify target");
95 }
96 if (!urlOrChannel) {
97 throw new Error("Must specify URL, WebSocket or RTCDataChannel");
98 }
99
100 // We rely on modern APIs which might not be available in an
101 // insecure context
102 if (!window.isSecureContext) {
103 Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
104 }
105
106 super();
107
108 this._target = target;
109
110 if (typeof urlOrChannel === "string") {
111 this._url = urlOrChannel;
112 } else {
113 this._url = null;
114 this._rawChannel = urlOrChannel;
115 }
116
117 // Connection details
118 options = options || {};
119 this._rfbCredentials = options.credentials || {};
120 this._shared = 'shared' in options ? !!options.shared : true;
121 this._repeaterID = options.repeaterID || '';
122 this._wsProtocols = options.wsProtocols || [];
123
124 // Internal state
125 this._rfbConnectionState = '';
126 this._rfbInitState = '';
127 this._rfbAuthScheme = -1;
128 this._rfbCleanDisconnect = true;
129 this._rfbRSAAESAuthenticationState = null;
130
131 // Server capabilities
132 this._rfbVersion = 0;
133 this._rfbMaxVersion = 3.8;
134 this._rfbTightVNC = false;
135 this._rfbVeNCryptState = 0;
136 this._rfbXvpVer = 0;
137
138 this._fbWidth = 0;
139 this._fbHeight = 0;
140
141 this._fbName = "";
142
143 this._capabilities = { power: false };
144
145 this._supportsFence = false;
146
147 this._supportsContinuousUpdates = false;
148 this._enabledContinuousUpdates = false;
149
150 this._supportsSetDesktopSize = false;
151 this._screenID = 0;
152 this._screenFlags = 0;
153 this._pendingRemoteResize = false;
154 this._lastResize = 0;
155
156 this._qemuExtKeyEventSupported = false;
157
158 this._extendedPointerEventSupported = false;
159
160 this._clipboardText = null;
161 this._clipboardServerCapabilitiesActions = {};
162 this._clipboardServerCapabilitiesFormats = {};
163
164 // Internal objects
165 this._sock = null; // Websock object
166 this._display = null; // Display object
167 this._flushing = false; // Display flushing state
168 this._asyncClipboard = null; // Async clipboard object
169 this._keyboard = null; // Keyboard input handler object
170 this._gestures = null; // Gesture input handler object
171 this._resizeObserver = null; // Resize observer object
172
173 // Timers
174 this._disconnTimer = null; // disconnection timer
175 this._resizeTimeout = null; // resize rate limiting
176 this._mouseMoveTimer = null;
177
178 // Decoder states
179 this._decoders = {};
180
181 this._FBU = {
182 rects: 0,
183 x: 0,
184 y: 0,
185 width: 0,
186 height: 0,
187 encoding: null,
188 };
189
190 // Mouse state
191 this._mousePos = {};
192 this._mouseButtonMask = 0;
193 this._mouseLastMoveTime = 0;
194 this._viewportDragging = false;
195 this._viewportDragPos = {};
196 this._viewportHasMoved = false;
197 this._accumulatedWheelDeltaX = 0;
198 this._accumulatedWheelDeltaY = 0;
199
200 // Gesture state
201 this._gestureLastTapTime = null;
202 this._gestureFirstDoubleTapEv = null;
203 this._gestureLastMagnitudeX = 0;
204 this._gestureLastMagnitudeY = 0;
205
206 // Bound event handlers
207 this._eventHandlers = {
208 focusCanvas: this._focusCanvas.bind(this),
209 handleResize: this._handleResize.bind(this),
210 handleMouse: this._handleMouse.bind(this),
211 handleWheel: this._handleWheel.bind(this),
212 handleGesture: this._handleGesture.bind(this),
213 handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
214 handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
215 };
216
217 // main setup
218 Log.Debug(">> RFB.constructor");
219
220 // Create DOM elements
221 this._screen = document.createElement('div');
222 this._screen.style.display = 'flex';
223 this._screen.style.width = '100%';
224 this._screen.style.height = '100%';
225 this._screen.style.overflow = 'auto';
226 this._screen.style.background = DEFAULT_BACKGROUND;
227 this._canvas = document.createElement('canvas');
228 this._canvas.style.margin = 'auto';
229 // Some browsers add an outline on focus
230 this._canvas.style.outline = 'none';
231 this._canvas.width = 0;
232 this._canvas.height = 0;
233 this._canvas.tabIndex = -1;
234 this._screen.appendChild(this._canvas);
235
236 // Cursor
237 this._cursor = new Cursor();
238
239 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
240 // it. Result: no cursor at all until a window border or an edit field
241 // is hit blindly. But there are also VNC servers that draw the cursor
242 // in the framebuffer and don't send the empty local cursor. There is
243 // no way to satisfy both sides.
244 //
245 // The spec is unclear on this "initial cursor" issue. Many other
246 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
247 // initial cursor instead.
248 this._cursorImage = RFB.cursors.none;
249
250 // populate decoder array with objects
251 this._decoders[encodings.encodingRaw] = new RawDecoder();
252 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
253 this._decoders[encodings.encodingRRE] = new RREDecoder();
254 this._decoders[encodings.encodingHextile] = new HextileDecoder();
255 this._decoders[encodings.encodingZlib] = new ZlibDecoder();
256 this._decoders[encodings.encodingTight] = new TightDecoder();
257 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
258 this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
259 this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
260 this._decoders[encodings.encodingH264] = new H264Decoder();
261
262 // NB: nothing that needs explicit teardown should be done
263 // before this point, since this can throw an exception
264 try {
265 this._display = new Display(this._canvas);
266 } catch (exc) {
267 Log.Error("Display exception: " + exc);
268 throw exc;
269 }
270
271 this._asyncClipboard = new AsyncClipboard(this._canvas);
272 this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
273
274 this._keyboard = new Keyboard(this._canvas);
275 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
276 this._remoteCapsLock = null; // Null indicates unknown or irrelevant
277 this._remoteNumLock = null;
278
279 this._gestures = new GestureHandler();
280
281 this._sock = new Websock();
282 this._sock.on('open', this._socketOpen.bind(this));
283 this._sock.on('close', this._socketClose.bind(this));
284 this._sock.on('message', this._handleMessage.bind(this));
285 this._sock.on('error', this._socketError.bind(this));
286
287 this._expectedClientWidth = null;
288 this._expectedClientHeight = null;
289 this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
290
291 // All prepared, kick off the connection
292 this._updateConnectionState('connecting');
293
294 Log.Debug("<< RFB.constructor");
295
296 // ===== PROPERTIES =====
297
298 this.dragViewport = false;
299 this.focusOnClick = true;
300
301 this._viewOnly = false;
302 this._clipViewport = false;
303 this._clippingViewport = false;
304 this._scaleViewport = false;
305 this._resizeSession = false;
306
307 this._showDotCursor = false;
308
309 this._qualityLevel = 6;
310 this._compressionLevel = 2;
311 }
312
313 // ===== PROPERTIES =====
314
315 get viewOnly() { return this._viewOnly; }
316 set viewOnly(viewOnly) {
317 this._viewOnly = viewOnly;
318
319 if (this._rfbConnectionState === "connecting" ||
320 this._rfbConnectionState === "connected") {
321 if (viewOnly) {
322 this._keyboard.ungrab();
323 this._asyncClipboard.ungrab();
324 } else {
325 this._keyboard.grab();
326 this._asyncClipboard.grab();
327 }
328 }
329 }
330
331 get capabilities() { return this._capabilities; }
332
333 get clippingViewport() { return this._clippingViewport; }
334 _setClippingViewport(on) {
335 if (on === this._clippingViewport) {
336 return;
337 }
338 this._clippingViewport = on;
339 this.dispatchEvent(new CustomEvent("clippingviewport",
340 { detail: this._clippingViewport }));
341 }
342
343 get touchButton() { return 0; }
344 set touchButton(button) { Log.Warn("Using old API!"); }
345
346 get clipViewport() { return this._clipViewport; }
347 set clipViewport(viewport) {
348 this._clipViewport = viewport;
349 this._updateClip();
350 }
351
352 get scaleViewport() { return this._scaleViewport; }
353 set scaleViewport(scale) {
354 this._scaleViewport = scale;
355 // Scaling trumps clipping, so we may need to adjust
356 // clipping when enabling or disabling scaling
357 if (scale && this._clipViewport) {
358 this._updateClip();
359 }
360 this._updateScale();
361 if (!scale && this._clipViewport) {
362 this._updateClip();
363 }
364 }
365
366 get resizeSession() { return this._resizeSession; }
367 set resizeSession(resize) {
368 this._resizeSession = resize;
369 if (resize) {
370 this._requestRemoteResize();
371 }
372 }
373
374 get showDotCursor() { return this._showDotCursor; }
375 set showDotCursor(show) {
376 this._showDotCursor = show;
377 this._refreshCursor();
378 }
379
380 get background() { return this._screen.style.background; }
381 set background(cssValue) { this._screen.style.background = cssValue; }
382
383 get qualityLevel() {
384 return this._qualityLevel;
385 }
386 set qualityLevel(qualityLevel) {
387 if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
388 Log.Error("qualityLevel must be an integer between 0 and 9");
389 return;
390 }
391
392 if (this._qualityLevel === qualityLevel) {
393 return;
394 }
395
396 this._qualityLevel = qualityLevel;
397
398 if (this._rfbConnectionState === 'connected') {
399 this._sendEncodings();
400 }
401 }
402
403 get compressionLevel() {
404 return this._compressionLevel;
405 }
406 set compressionLevel(compressionLevel) {
407 if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
408 Log.Error("compressionLevel must be an integer between 0 and 9");
409 return;
410 }
411
412 if (this._compressionLevel === compressionLevel) {
413 return;
414 }
415
416 this._compressionLevel = compressionLevel;
417
418 if (this._rfbConnectionState === 'connected') {
419 this._sendEncodings();
420 }
421 }
422
423 // ===== PUBLIC METHODS =====
424
425 disconnect() {
426 this._updateConnectionState('disconnecting');
427 this._sock.off('error');
428 this._sock.off('message');
429 this._sock.off('open');
430 if (this._rfbRSAAESAuthenticationState !== null) {
431 this._rfbRSAAESAuthenticationState.disconnect();
432 }
433 }
434
435 approveServer() {
436 if (this._rfbRSAAESAuthenticationState !== null) {
437 this._rfbRSAAESAuthenticationState.approveServer();
438 }
439 }
440
441 sendCredentials(creds) {
442 this._rfbCredentials = creds;
443 this._resumeAuthentication();
444 }
445
446 sendCtrlAltDel() {
447 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
448 Log.Info("Sending Ctrl-Alt-Del");
449
450 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
451 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
452 this.sendKey(KeyTable.XK_Delete, "Delete", true);
453 this.sendKey(KeyTable.XK_Delete, "Delete", false);
454 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
455 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
456 }
457
458 machineShutdown() {
459 this._xvpOp(1, 2);
460 }
461
462 machineReboot() {
463 this._xvpOp(1, 3);
464 }
465
466 machineReset() {
467 this._xvpOp(1, 4);
468 }
469
470 // Send a key press. If 'down' is not specified then send a down key
471 // followed by an up key.
472 sendKey(keysym, code, down) {
473 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
474
475 if (down === undefined) {
476 this.sendKey(keysym, code, true);
477 this.sendKey(keysym, code, false);
478 return;
479 }
480
481 const scancode = XtScancode[code];
482
483 if (this._qemuExtKeyEventSupported && scancode) {
484 // 0 is NoSymbol
485 keysym = keysym || 0;
486
487 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
488
489 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
490 } else {
491 if (!keysym) {
492 return;
493 }
494 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
495 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
496 }
497 }
498
499 focus(options) {
500 this._canvas.focus(options);
501 }
502
503 blur() {
504 this._canvas.blur();
505 }
506
507 clipboardPasteFrom(text) {
508 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
509
510 if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
511 this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
512
513 this._clipboardText = text;
514 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
515 } else {
516 let length, i;
517 let data;
518
519 length = 0;
520 // eslint-disable-next-line no-unused-vars
521 for (let codePoint of text) {
522 length++;
523 }
524
525 data = new Uint8Array(length);
526
527 i = 0;
528 for (let codePoint of text) {
529 let code = codePoint.codePointAt(0);
530
531 /* Only ISO 8859-1 is supported */
532 if (code > 0xff) {
533 code = 0x3f; // '?'
534 }
535
536 data[i++] = code;
537 }
538
539 RFB.messages.clientCutText(this._sock, data);
540 }
541 }
542
543 getImageData() {
544 return this._display.getImageData();
545 }
546
547 toDataURL(type, encoderOptions) {
548 return this._display.toDataURL(type, encoderOptions);
549 }
550
551 toBlob(callback, type, quality) {
552 return this._display.toBlob(callback, type, quality);
553 }
554
555 // ===== PRIVATE METHODS =====
556
557 _connect() {
558 Log.Debug(">> RFB.connect");
559
560 if (this._url) {
561 Log.Info(`connecting to ${this._url}`);
562 this._sock.open(this._url, this._wsProtocols);
563 } else {
564 Log.Info(`attaching ${this._rawChannel} to Websock`);
565 this._sock.attach(this._rawChannel);
566
567 if (this._sock.readyState === 'closed') {
568 throw Error("Cannot use already closed WebSocket/RTCDataChannel");
569 }
570
571 if (this._sock.readyState === 'open') {
572 // FIXME: _socketOpen() can in theory call _fail(), which
573 // isn't allowed this early, but I'm not sure that can
574 // happen without a bug messing up our state variables
575 this._socketOpen();
576 }
577 }
578
579 // Make our elements part of the page
580 this._target.appendChild(this._screen);
581
582 this._gestures.attach(this._canvas);
583
584 this._cursor.attach(this._canvas);
585 this._refreshCursor();
586
587 // Monitor size changes of the screen element
588 this._resizeObserver.observe(this._screen);
589
590 // Always grab focus on some kind of click event
591 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
592 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
593
594 // Mouse events
595 this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
596 this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
597 this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
598 // Prevent middle-click pasting (see handler for why we bind to document)
599 this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
600 // preventDefault() on mousedown doesn't stop this event for some
601 // reason so we have to explicitly block it
602 this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
603
604 // Wheel events
605 this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
606
607 // Gesture events
608 this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
609 this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
610 this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
611
612 Log.Debug("<< RFB.connect");
613 }
614
615 _disconnect() {
616 Log.Debug(">> RFB.disconnect");
617 this._cursor.detach();
618 this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
619 this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
620 this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
621 this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
622 this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
623 this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
624 this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
625 this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
626 this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
627 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
628 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
629 this._resizeObserver.disconnect();
630 this._keyboard.ungrab();
631 this._gestures.detach();
632 this._sock.close();
633 try {
634 this._target.removeChild(this._screen);
635 } catch (e) {
636 if (e.name === 'NotFoundError') {
637 // Some cases where the initial connection fails
638 // can disconnect before the _screen is created
639 } else {
640 throw e;
641 }
642 }
643 clearTimeout(this._resizeTimeout);
644 clearTimeout(this._mouseMoveTimer);
645 Log.Debug("<< RFB.disconnect");
646 }
647
648 _socketOpen() {
649 if ((this._rfbConnectionState === 'connecting') &&
650 (this._rfbInitState === '')) {
651 this._rfbInitState = 'ProtocolVersion';
652 Log.Debug("Starting VNC handshake");
653 } else {
654 this._fail("Unexpected server connection while " +
655 this._rfbConnectionState);
656 }
657 }
658
659 _socketClose(e) {
660 Log.Debug("WebSocket on-close event");
661 let msg = "";
662 if (e.code) {
663 msg = "(code: " + e.code;
664 if (e.reason) {
665 msg += ", reason: " + e.reason;
666 }
667 msg += ")";
668 }
669 switch (this._rfbConnectionState) {
670 case 'connecting':
671 this._fail("Connection closed " + msg);
672 break;
673 case 'connected':
674 // Handle disconnects that were initiated server-side
675 this._updateConnectionState('disconnecting');
676 this._updateConnectionState('disconnected');
677 break;
678 case 'disconnecting':
679 // Normal disconnection path
680 this._updateConnectionState('disconnected');
681 break;
682 case 'disconnected':
683 this._fail("Unexpected server disconnect " +
684 "when already disconnected " + msg);
685 break;
686 default:
687 this._fail("Unexpected server disconnect before connecting " +
688 msg);
689 break;
690 }
691 this._sock.off('close');
692 // Delete reference to raw channel to allow cleanup.
693 this._rawChannel = null;
694 }
695
696 _socketError(e) {
697 Log.Warn("WebSocket on-error event");
698 }
699
700 _focusCanvas(event) {
701 if (!this.focusOnClick) {
702 return;
703 }
704
705 this.focus({ preventScroll: true });
706 }
707
708 _setDesktopName(name) {
709 this._fbName = name;
710 this.dispatchEvent(new CustomEvent(
711 "desktopname",
712 { detail: { name: this._fbName } }));
713 }
714
715 _saveExpectedClientSize() {
716 this._expectedClientWidth = this._screen.clientWidth;
717 this._expectedClientHeight = this._screen.clientHeight;
718 }
719
720 _currentClientSize() {
721 return [this._screen.clientWidth, this._screen.clientHeight];
722 }
723
724 _clientHasExpectedSize() {
725 const [currentWidth, currentHeight] = this._currentClientSize();
726 return currentWidth == this._expectedClientWidth &&
727 currentHeight == this._expectedClientHeight;
728 }
729
730 // Handle browser window resizes
731 _handleResize() {
732 // Don't change anything if the client size is already as expected
733 if (this._clientHasExpectedSize()) {
734 return;
735 }
736 // If the window resized then our screen element might have
737 // as well. Update the viewport dimensions.
738 window.requestAnimationFrame(() => {
739 this._updateClip();
740 this._updateScale();
741 this._saveExpectedClientSize();
742 });
743
744 // Request changing the resolution of the remote display to
745 // the size of the local browser viewport.
746 this._requestRemoteResize();
747 }
748
749 // Update state of clipping in Display object, and make sure the
750 // configured viewport matches the current screen size
751 _updateClip() {
752 const curClip = this._display.clipViewport;
753 let newClip = this._clipViewport;
754
755 if (this._scaleViewport) {
756 // Disable viewport clipping if we are scaling
757 newClip = false;
758 }
759
760 if (curClip !== newClip) {
761 this._display.clipViewport = newClip;
762 }
763
764 if (newClip) {
765 // When clipping is enabled, the screen is limited to
766 // the size of the container.
767 const size = this._screenSize();
768 this._display.viewportChangeSize(size.w, size.h);
769 this._fixScrollbars();
770 this._setClippingViewport(size.w < this._display.width ||
771 size.h < this._display.height);
772 } else {
773 this._setClippingViewport(false);
774 }
775
776 // When changing clipping we might show or hide scrollbars.
777 // This causes the expected client dimensions to change.
778 if (curClip !== newClip) {
779 this._saveExpectedClientSize();
780 }
781 }
782
783 _updateScale() {
784 if (!this._scaleViewport) {
785 this._display.scale = 1.0;
786 } else {
787 const size = this._screenSize();
788 this._display.autoscale(size.w, size.h);
789 }
790 this._fixScrollbars();
791 }
792
793 // Requests a change of remote desktop size. This message is an extension
794 // and may only be sent if we have received an ExtendedDesktopSize message
795 _requestRemoteResize() {
796 if (!this._resizeSession) {
797 return;
798 }
799 if (this._viewOnly) {
800 return;
801 }
802 if (!this._supportsSetDesktopSize) {
803 return;
804 }
805
806 // Rate limit to one pending resize at a time
807 if (this._pendingRemoteResize) {
808 return;
809 }
810
811 // And no more than once every 100ms
812 if ((Date.now() - this._lastResize) < 100) {
813 clearTimeout(this._resizeTimeout);
814 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),
815 100 - (Date.now() - this._lastResize));
816 return;
817 }
818 this._resizeTimeout = null;
819
820 const size = this._screenSize();
821
822 // Do we actually change anything?
823 if (size.w === this._fbWidth && size.h === this._fbHeight) {
824 return;
825 }
826
827 this._pendingRemoteResize = true;
828 this._lastResize = Date.now();
829 RFB.messages.setDesktopSize(this._sock,
830 Math.floor(size.w), Math.floor(size.h),
831 this._screenID, this._screenFlags);
832
833 Log.Debug('Requested new desktop size: ' +
834 size.w + 'x' + size.h);
835 }
836
837 // Gets the the size of the available screen
838 _screenSize() {
839 let r = this._screen.getBoundingClientRect();
840 return { w: r.width, h: r.height };
841 }
842
843 _fixScrollbars() {
844 // This is a hack because Safari on macOS screws up the calculation
845 // for when scrollbars are needed. We get scrollbars when making the
846 // browser smaller, despite remote resize being enabled. So to fix it
847 // we temporarily toggle them off and on.
848 const orig = this._screen.style.overflow;
849 this._screen.style.overflow = 'hidden';
850 // Force Safari to recalculate the layout by asking for
851 // an element's dimensions
852 this._screen.getBoundingClientRect();
853 this._screen.style.overflow = orig;
854 }
855
856 /*
857 * Connection states:
858 * connecting
859 * connected
860 * disconnecting
861 * disconnected - permanent state
862 */
863 _updateConnectionState(state) {
864 const oldstate = this._rfbConnectionState;
865
866 if (state === oldstate) {
867 Log.Debug("Already in state '" + state + "', ignoring");
868 return;
869 }
870
871 // The 'disconnected' state is permanent for each RFB object
872 if (oldstate === 'disconnected') {
873 Log.Error("Tried changing state of a disconnected RFB object");
874 return;
875 }
876
877 // Ensure proper transitions before doing anything
878 switch (state) {
879 case 'connected':
880 if (oldstate !== 'connecting') {
881 Log.Error("Bad transition to connected state, " +
882 "previous connection state: " + oldstate);
883 return;
884 }
885 break;
886
887 case 'disconnected':
888 if (oldstate !== 'disconnecting') {
889 Log.Error("Bad transition to disconnected state, " +
890 "previous connection state: " + oldstate);
891 return;
892 }
893 break;
894
895 case 'connecting':
896 if (oldstate !== '') {
897 Log.Error("Bad transition to connecting state, " +
898 "previous connection state: " + oldstate);
899 return;
900 }
901 break;
902
903 case 'disconnecting':
904 if (oldstate !== 'connected' && oldstate !== 'connecting') {
905 Log.Error("Bad transition to disconnecting state, " +
906 "previous connection state: " + oldstate);
907 return;
908 }
909 break;
910
911 default:
912 Log.Error("Unknown connection state: " + state);
913 return;
914 }
915
916 // State change actions
917
918 this._rfbConnectionState = state;
919
920 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
921
922 if (this._disconnTimer && state !== 'disconnecting') {
923 Log.Debug("Clearing disconnect timer");
924 clearTimeout(this._disconnTimer);
925 this._disconnTimer = null;
926
927 // make sure we don't get a double event
928 this._sock.off('close');
929 }
930
931 switch (state) {
932 case 'connecting':
933 this._connect();
934 break;
935
936 case 'connected':
937 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
938 break;
939
940 case 'disconnecting':
941 this._disconnect();
942
943 this._disconnTimer = setTimeout(() => {
944 Log.Error("Disconnection timed out.");
945 this._updateConnectionState('disconnected');
946 }, DISCONNECT_TIMEOUT * 1000);
947 break;
948
949 case 'disconnected':
950 this.dispatchEvent(new CustomEvent(
951 "disconnect", { detail:
952 { clean: this._rfbCleanDisconnect } }));
953 break;
954 }
955 }
956
957 /* Print errors and disconnect
958 *
959 * The parameter 'details' is used for information that
960 * should be logged but not sent to the user interface.
961 */
962 _fail(details) {
963 switch (this._rfbConnectionState) {
964 case 'disconnecting':
965 Log.Error("Failed when disconnecting: " + details);
966 break;
967 case 'connected':
968 Log.Error("Failed while connected: " + details);
969 break;
970 case 'connecting':
971 Log.Error("Failed when connecting: " + details);
972 break;
973 default:
974 Log.Error("RFB failure: " + details);
975 break;
976 }
977 this._rfbCleanDisconnect = false; //This is sent to the UI
978
979 // Transition to disconnected without waiting for socket to close
980 this._updateConnectionState('disconnecting');
981 this._updateConnectionState('disconnected');
982
983 return false;
984 }
985
986 _setCapability(cap, val) {
987 this._capabilities[cap] = val;
988 this.dispatchEvent(new CustomEvent("capabilities",
989 { detail: { capabilities: this._capabilities } }));
990 }
991
992 _handleMessage() {
993 if (this._sock.rQwait("message", 1)) {
994 Log.Warn("handleMessage called on an empty receive queue");
995 return;
996 }
997
998 switch (this._rfbConnectionState) {
999 case 'disconnected':
1000 Log.Error("Got data while disconnected");
1001 break;
1002 case 'connected':
1003 while (true) {
1004 if (this._flushing) {
1005 break;
1006 }
1007 if (!this._normalMsg()) {
1008 break;
1009 }
1010 if (this._sock.rQwait("message", 1)) {
1011 break;
1012 }
1013 }
1014 break;
1015 case 'connecting':
1016 while (this._rfbConnectionState === 'connecting') {
1017 if (!this._initMsg()) {
1018 break;
1019 }
1020 }
1021 break;
1022 default:
1023 Log.Error("Got data while in an invalid state");
1024 break;
1025 }
1026 }
1027
1028 _handleKeyEvent(keysym, code, down, numlock, capslock) {
1029 // If remote state of capslock is known, and it doesn't match the local led state of
1030 // the keyboard, we send a capslock keypress first to bring it into sync.
1031 // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
1032 // we clear the remote state so that we don't send duplicate or spurious fixes,
1033 // since it may take some time to receive the new remote CapsLock state.
1034 if (code == 'CapsLock' && down) {
1035 this._remoteCapsLock = null;
1036 }
1037 if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
1038 Log.Debug("Fixing remote caps lock");
1039
1040 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
1041 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
1042 // We clear the remote capsLock state when we do this to prevent issues with doing this twice
1043 // before we receive an update of the the remote state.
1044 this._remoteCapsLock = null;
1045 }
1046
1047 // Logic for numlock is exactly the same.
1048 if (code == 'NumLock' && down) {
1049 this._remoteNumLock = null;
1050 }
1051 if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
1052 Log.Debug("Fixing remote num lock");
1053 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
1054 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
1055 this._remoteNumLock = null;
1056 }
1057 this.sendKey(keysym, code, down);
1058 }
1059
1060 static _convertButtonMask(buttons) {
1061 /* The bits in MouseEvent.buttons property correspond
1062 * to the following mouse buttons:
1063 * 0: Left
1064 * 1: Right
1065 * 2: Middle
1066 * 3: Back
1067 * 4: Forward
1068 *
1069 * These bits needs to be converted to what they are defined as
1070 * in the RFB protocol.
1071 */
1072
1073 const buttonMaskMap = {
1074 0: 1 << 0, // Left
1075 1: 1 << 2, // Right
1076 2: 1 << 1, // Middle
1077 3: 1 << 7, // Back
1078 4: 1 << 8, // Forward
1079 };
1080
1081 let bmask = 0;
1082 for (let i = 0; i < 5; i++) {
1083 if (buttons & (1 << i)) {
1084 bmask |= buttonMaskMap[i];
1085 }
1086 }
1087 return bmask;
1088 }
1089
1090 _handleMouse(ev) {
1091 /*
1092 * We don't check connection status or viewOnly here as the
1093 * mouse events might be used to control the viewport
1094 */
1095
1096 if (ev.type === 'click') {
1097 /*
1098 * Note: This is only needed for the 'click' event as it fails
1099 * to fire properly for the target element so we have
1100 * to listen on the document element instead.
1101 */
1102 if (ev.target !== this._canvas) {
1103 return;
1104 }
1105 }
1106
1107 // FIXME: if we're in view-only and not dragging,
1108 // should we stop events?
1109 ev.stopPropagation();
1110 ev.preventDefault();
1111
1112 if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
1113 return;
1114 }
1115
1116 let pos = clientToElement(ev.clientX, ev.clientY,
1117 this._canvas);
1118
1119 let bmask = RFB._convertButtonMask(ev.buttons);
1120
1121 let down = ev.type == 'mousedown';
1122 switch (ev.type) {
1123 case 'mousedown':
1124 case 'mouseup':
1125 if (this.dragViewport) {
1126 if (down && !this._viewportDragging) {
1127 this._viewportDragging = true;
1128 this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1129 this._viewportHasMoved = false;
1130
1131 this._flushMouseMoveTimer(pos.x, pos.y);
1132
1133 // Skip sending mouse events, instead save the current
1134 // mouse mask so we can send it later.
1135 this._mouseButtonMask = bmask;
1136 break;
1137 } else {
1138 this._viewportDragging = false;
1139
1140 // If we actually performed a drag then we are done
1141 // here and should not send any mouse events
1142 if (this._viewportHasMoved) {
1143 this._mouseButtonMask = bmask;
1144 break;
1145 }
1146 // Otherwise we treat this as a mouse click event.
1147 // Send the previously saved button mask, followed
1148 // by the current button mask at the end of this
1149 // function.
1150 this._sendMouse(pos.x, pos.y, this._mouseButtonMask);
1151 }
1152 }
1153 if (down) {
1154 setCapture(this._canvas);
1155 }
1156 this._handleMouseButton(pos.x, pos.y, bmask);
1157 break;
1158 case 'mousemove':
1159 if (this._viewportDragging) {
1160 const deltaX = this._viewportDragPos.x - pos.x;
1161 const deltaY = this._viewportDragPos.y - pos.y;
1162
1163 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
1164 Math.abs(deltaY) > dragThreshold)) {
1165 this._viewportHasMoved = true;
1166
1167 this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1168 this._display.viewportChangePos(deltaX, deltaY);
1169 }
1170
1171 // Skip sending mouse events
1172 break;
1173 }
1174 this._handleMouseMove(pos.x, pos.y);
1175 break;
1176 }
1177 }
1178
1179 _handleMouseButton(x, y, bmask) {
1180 // Flush waiting move event first
1181 this._flushMouseMoveTimer(x, y);
1182
1183 this._mouseButtonMask = bmask;
1184 this._sendMouse(x, y, this._mouseButtonMask);
1185 }
1186
1187 _handleMouseMove(x, y) {
1188 this._mousePos = { 'x': x, 'y': y };
1189
1190 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
1191 if (this._mouseMoveTimer == null) {
1192
1193 const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
1194 if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
1195 this._sendMouse(x, y, this._mouseButtonMask);
1196 this._mouseLastMoveTime = Date.now();
1197 } else {
1198 // Too soon since the latest move, wait the remaining time
1199 this._mouseMoveTimer = setTimeout(() => {
1200 this._handleDelayedMouseMove();
1201 }, MOUSE_MOVE_DELAY - timeSinceLastMove);
1202 }
1203 }
1204 }
1205
1206 _handleDelayedMouseMove() {
1207 this._mouseMoveTimer = null;
1208 this._sendMouse(this._mousePos.x, this._mousePos.y,
1209 this._mouseButtonMask);
1210 this._mouseLastMoveTime = Date.now();
1211 }
1212
1213 _sendMouse(x, y, mask) {
1214 if (this._rfbConnectionState !== 'connected') { return; }
1215 if (this._viewOnly) { return; } // View only, skip mouse events
1216
1217 // Highest bit in mask is never sent to the server
1218 if (mask & 0x8000) {
1219 throw new Error("Illegal mouse button mask (mask: " + mask + ")");
1220 }
1221
1222 let extendedMouseButtons = mask & 0x7f80;
1223
1224 if (this._extendedPointerEventSupported && extendedMouseButtons) {
1225 RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
1226 this._display.absY(y), mask);
1227 } else {
1228 RFB.messages.pointerEvent(this._sock, this._display.absX(x),
1229 this._display.absY(y), mask);
1230 }
1231 }
1232
1233 _handleWheel(ev) {
1234 if (this._rfbConnectionState !== 'connected') { return; }
1235 if (this._viewOnly) { return; } // View only, skip mouse events
1236
1237 ev.stopPropagation();
1238 ev.preventDefault();
1239
1240 let pos = clientToElement(ev.clientX, ev.clientY,
1241 this._canvas);
1242
1243 let bmask = RFB._convertButtonMask(ev.buttons);
1244 let dX = ev.deltaX;
1245 let dY = ev.deltaY;
1246
1247 // Pixel units unless it's non-zero.
1248 // Note that if deltamode is line or page won't matter since we aren't
1249 // sending the mouse wheel delta to the server anyway.
1250 // The difference between pixel and line can be important however since
1251 // we have a threshold that can be smaller than the line height.
1252 if (ev.deltaMode !== 0) {
1253 dX *= WHEEL_LINE_HEIGHT;
1254 dY *= WHEEL_LINE_HEIGHT;
1255 }
1256
1257 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1258 // protocol can't handle a wheel event with specific distance or speed.
1259 // Therefor, if we get a lot of small mouse wheel events we combine them.
1260 this._accumulatedWheelDeltaX += dX;
1261 this._accumulatedWheelDeltaY += dY;
1262
1263
1264 // Generate a mouse wheel step event when the accumulated delta
1265 // for one of the axes is large enough.
1266 if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
1267 if (this._accumulatedWheelDeltaX < 0) {
1268 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);
1269 this._handleMouseButton(pos.x, pos.y, bmask);
1270 } else if (this._accumulatedWheelDeltaX > 0) {
1271 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);
1272 this._handleMouseButton(pos.x, pos.y, bmask);
1273 }
1274
1275 this._accumulatedWheelDeltaX = 0;
1276 }
1277 if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
1278 if (this._accumulatedWheelDeltaY < 0) {
1279 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);
1280 this._handleMouseButton(pos.x, pos.y, bmask);
1281 } else if (this._accumulatedWheelDeltaY > 0) {
1282 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);
1283 this._handleMouseButton(pos.x, pos.y, bmask);
1284 }
1285
1286 this._accumulatedWheelDeltaY = 0;
1287 }
1288 }
1289
1290 _fakeMouseMove(ev, elementX, elementY) {
1291 this._handleMouseMove(elementX, elementY);
1292 this._cursor.move(ev.detail.clientX, ev.detail.clientY);
1293 }
1294
1295 _handleTapEvent(ev, bmask) {
1296 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1297 this._canvas);
1298
1299 // If the user quickly taps multiple times we assume they meant to
1300 // hit the same spot, so slightly adjust coordinates
1301
1302 if ((this._gestureLastTapTime !== null) &&
1303 ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
1304 (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
1305 let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
1306 let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
1307 let distance = Math.hypot(dx, dy);
1308
1309 if (distance < DOUBLE_TAP_THRESHOLD) {
1310 pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
1311 this._gestureFirstDoubleTapEv.detail.clientY,
1312 this._canvas);
1313 } else {
1314 this._gestureFirstDoubleTapEv = ev;
1315 }
1316 } else {
1317 this._gestureFirstDoubleTapEv = ev;
1318 }
1319 this._gestureLastTapTime = Date.now();
1320
1321 this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
1322 this._handleMouseButton(pos.x, pos.y, bmask);
1323 this._handleMouseButton(pos.x, pos.y, 0x0);
1324 }
1325
1326 _handleGesture(ev) {
1327 let magnitude;
1328
1329 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1330 this._canvas);
1331 switch (ev.type) {
1332 case 'gesturestart':
1333 switch (ev.detail.type) {
1334 case 'onetap':
1335 this._handleTapEvent(ev, 0x1);
1336 break;
1337 case 'twotap':
1338 this._handleTapEvent(ev, 0x4);
1339 break;
1340 case 'threetap':
1341 this._handleTapEvent(ev, 0x2);
1342 break;
1343 case 'drag':
1344 if (this.dragViewport) {
1345 this._viewportHasMoved = false;
1346 this._viewportDragging = true;
1347 this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1348 } else {
1349 this._fakeMouseMove(ev, pos.x, pos.y);
1350 this._handleMouseButton(pos.x, pos.y, 0x1);
1351 }
1352 break;
1353 case 'longpress':
1354 if (this.dragViewport) {
1355 // If dragViewport is true, we need to wait to see
1356 // if we have dragged outside the threshold before
1357 // sending any events to the server.
1358 this._viewportHasMoved = false;
1359 this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1360 } else {
1361 this._fakeMouseMove(ev, pos.x, pos.y);
1362 this._handleMouseButton(pos.x, pos.y, 0x4);
1363 }
1364 break;
1365 case 'twodrag':
1366 this._gestureLastMagnitudeX = ev.detail.magnitudeX;
1367 this._gestureLastMagnitudeY = ev.detail.magnitudeY;
1368 this._fakeMouseMove(ev, pos.x, pos.y);
1369 break;
1370 case 'pinch':
1371 this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
1372 ev.detail.magnitudeY);
1373 this._fakeMouseMove(ev, pos.x, pos.y);
1374 break;
1375 }
1376 break;
1377
1378 case 'gesturemove':
1379 switch (ev.detail.type) {
1380 case 'onetap':
1381 case 'twotap':
1382 case 'threetap':
1383 break;
1384 case 'drag':
1385 case 'longpress':
1386 if (this.dragViewport) {
1387 this._viewportDragging = true;
1388 const deltaX = this._viewportDragPos.x - pos.x;
1389 const deltaY = this._viewportDragPos.y - pos.y;
1390
1391 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
1392 Math.abs(deltaY) > dragThreshold)) {
1393 this._viewportHasMoved = true;
1394
1395 this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1396 this._display.viewportChangePos(deltaX, deltaY);
1397 }
1398 } else {
1399 this._fakeMouseMove(ev, pos.x, pos.y);
1400 }
1401 break;
1402 case 'twodrag':
1403 // Always scroll in the same position.
1404 // We don't know if the mouse was moved so we need to move it
1405 // every update.
1406 this._fakeMouseMove(ev, pos.x, pos.y);
1407 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
1408 this._handleMouseButton(pos.x, pos.y, 0x8);
1409 this._handleMouseButton(pos.x, pos.y, 0x0);
1410 this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
1411 }
1412 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
1413 this._handleMouseButton(pos.x, pos.y, 0x10);
1414 this._handleMouseButton(pos.x, pos.y, 0x0);
1415 this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
1416 }
1417 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
1418 this._handleMouseButton(pos.x, pos.y, 0x20);
1419 this._handleMouseButton(pos.x, pos.y, 0x0);
1420 this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
1421 }
1422 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
1423 this._handleMouseButton(pos.x, pos.y, 0x40);
1424 this._handleMouseButton(pos.x, pos.y, 0x0);
1425 this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
1426 }
1427 break;
1428 case 'pinch':
1429 // Always scroll in the same position.
1430 // We don't know if the mouse was moved so we need to move it
1431 // every update.
1432 this._fakeMouseMove(ev, pos.x, pos.y);
1433 magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
1434 if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1435 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
1436 while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1437 this._handleMouseButton(pos.x, pos.y, 0x8);
1438 this._handleMouseButton(pos.x, pos.y, 0x0);
1439 this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
1440 }
1441 while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
1442 this._handleMouseButton(pos.x, pos.y, 0x10);
1443 this._handleMouseButton(pos.x, pos.y, 0x0);
1444 this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
1445 }
1446 }
1447 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
1448 break;
1449 }
1450 break;
1451
1452 case 'gestureend':
1453 switch (ev.detail.type) {
1454 case 'onetap':
1455 case 'twotap':
1456 case 'threetap':
1457 case 'pinch':
1458 case 'twodrag':
1459 break;
1460 case 'drag':
1461 if (this.dragViewport) {
1462 this._viewportDragging = false;
1463 } else {
1464 this._fakeMouseMove(ev, pos.x, pos.y);
1465 this._handleMouseButton(pos.x, pos.y, 0x0);
1466 }
1467 break;
1468 case 'longpress':
1469 if (this._viewportHasMoved) {
1470 // We don't want to send any events if we have moved
1471 // our viewport
1472 break;
1473 }
1474
1475 if (this.dragViewport && !this._viewportHasMoved) {
1476 this._fakeMouseMove(ev, pos.x, pos.y);
1477 // If dragViewport is true, we need to wait to see
1478 // if we have dragged outside the threshold before
1479 // sending any events to the server.
1480 this._handleMouseButton(pos.x, pos.y, 0x4);
1481 this._handleMouseButton(pos.x, pos.y, 0x0);
1482 this._viewportDragging = false;
1483 } else {
1484 this._fakeMouseMove(ev, pos.x, pos.y);
1485 this._handleMouseButton(pos.x, pos.y, 0x0);
1486 }
1487 break;
1488 }
1489 break;
1490 }
1491 }
1492
1493 _flushMouseMoveTimer(x, y) {
1494 if (this._mouseMoveTimer !== null) {
1495 clearTimeout(this._mouseMoveTimer);
1496 this._mouseMoveTimer = null;
1497 this._sendMouse(x, y, this._mouseButtonMask);
1498 }
1499 }
1500
1501 // Message handlers
1502
1503 _negotiateProtocolVersion() {
1504 if (this._sock.rQwait("version", 12)) {
1505 return false;
1506 }
1507
1508 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
1509 Log.Info("Server ProtocolVersion: " + sversion);
1510 let isRepeater = 0;
1511 switch (sversion) {
1512 case "000.000": // UltraVNC repeater
1513 isRepeater = 1;
1514 break;
1515 case "003.003":
1516 case "003.006": // UltraVNC
1517 this._rfbVersion = 3.3;
1518 break;
1519 case "003.007":
1520 this._rfbVersion = 3.7;
1521 break;
1522 case "003.008":
1523 case "003.889": // Apple Remote Desktop
1524 case "004.000": // Intel AMT KVM
1525 case "004.001": // RealVNC 4.6
1526 case "005.000": // RealVNC 5.3
1527 this._rfbVersion = 3.8;
1528 break;
1529 default:
1530 return this._fail("Invalid server version " + sversion);
1531 }
1532
1533 if (isRepeater) {
1534 let repeaterID = "ID:" + this._repeaterID;
1535 while (repeaterID.length < 250) {
1536 repeaterID += "\0";
1537 }
1538 this._sock.sQpushString(repeaterID);
1539 this._sock.flush();
1540 return true;
1541 }
1542
1543 if (this._rfbVersion > this._rfbMaxVersion) {
1544 this._rfbVersion = this._rfbMaxVersion;
1545 }
1546
1547 const cversion = "00" + parseInt(this._rfbVersion, 10) +
1548 ".00" + ((this._rfbVersion * 10) % 10);
1549 this._sock.sQpushString("RFB " + cversion + "\n");
1550 this._sock.flush();
1551 Log.Debug('Sent ProtocolVersion: ' + cversion);
1552
1553 this._rfbInitState = 'Security';
1554 }
1555
1556 _isSupportedSecurityType(type) {
1557 const clientTypes = [
1558 securityTypeNone,
1559 securityTypeVNCAuth,
1560 securityTypeRA2ne,
1561 securityTypeTight,
1562 securityTypeVeNCrypt,
1563 securityTypeXVP,
1564 securityTypeARD,
1565 securityTypeMSLogonII,
1566 securityTypePlain,
1567 ];
1568
1569 return clientTypes.includes(type);
1570 }
1571
1572 _negotiateSecurity() {
1573 if (this._rfbVersion >= 3.7) {
1574 // Server sends supported list, client decides
1575 const numTypes = this._sock.rQshift8();
1576 if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
1577
1578 if (numTypes === 0) {
1579 this._rfbInitState = "SecurityReason";
1580 this._securityContext = "no security types";
1581 this._securityStatus = 1;
1582 return true;
1583 }
1584
1585 const types = this._sock.rQshiftBytes(numTypes);
1586 Log.Debug("Server security types: " + types);
1587
1588 // Look for a matching security type in the order that the
1589 // server prefers
1590 this._rfbAuthScheme = -1;
1591 for (let type of types) {
1592 if (this._isSupportedSecurityType(type)) {
1593 this._rfbAuthScheme = type;
1594 break;
1595 }
1596 }
1597
1598 if (this._rfbAuthScheme === -1) {
1599 return this._fail("Unsupported security types (types: " + types + ")");
1600 }
1601
1602 this._sock.sQpush8(this._rfbAuthScheme);
1603 this._sock.flush();
1604 } else {
1605 // Server decides
1606 if (this._sock.rQwait("security scheme", 4)) { return false; }
1607 this._rfbAuthScheme = this._sock.rQshift32();
1608
1609 if (this._rfbAuthScheme == 0) {
1610 this._rfbInitState = "SecurityReason";
1611 this._securityContext = "authentication scheme";
1612 this._securityStatus = 1;
1613 return true;
1614 }
1615 }
1616
1617 this._rfbInitState = 'Authentication';
1618 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
1619
1620 return true;
1621 }
1622
1623 _handleSecurityReason() {
1624 if (this._sock.rQwait("reason length", 4)) {
1625 return false;
1626 }
1627 const strlen = this._sock.rQshift32();
1628 let reason = "";
1629
1630 if (strlen > 0) {
1631 if (this._sock.rQwait("reason", strlen, 4)) { return false; }
1632 reason = this._sock.rQshiftStr(strlen);
1633 }
1634
1635 if (reason !== "") {
1636 this.dispatchEvent(new CustomEvent(
1637 "securityfailure",
1638 { detail: { status: this._securityStatus,
1639 reason: reason } }));
1640
1641 return this._fail("Security negotiation failed on " +
1642 this._securityContext +
1643 " (reason: " + reason + ")");
1644 } else {
1645 this.dispatchEvent(new CustomEvent(
1646 "securityfailure",
1647 { detail: { status: this._securityStatus } }));
1648
1649 return this._fail("Security negotiation failed on " +
1650 this._securityContext);
1651 }
1652 }
1653
1654 // authentication
1655 _negotiateXvpAuth() {
1656 if (this._rfbCredentials.username === undefined ||
1657 this._rfbCredentials.password === undefined ||
1658 this._rfbCredentials.target === undefined) {
1659 this.dispatchEvent(new CustomEvent(
1660 "credentialsrequired",
1661 { detail: { types: ["username", "password", "target"] } }));
1662 return false;
1663 }
1664
1665 this._sock.sQpush8(this._rfbCredentials.username.length);
1666 this._sock.sQpush8(this._rfbCredentials.target.length);
1667 this._sock.sQpushString(this._rfbCredentials.username);
1668 this._sock.sQpushString(this._rfbCredentials.target);
1669
1670 this._sock.flush();
1671
1672 this._rfbAuthScheme = securityTypeVNCAuth;
1673
1674 return this._negotiateAuthentication();
1675 }
1676
1677 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1678 _negotiateVeNCryptAuth() {
1679
1680 // waiting for VeNCrypt version
1681 if (this._rfbVeNCryptState == 0) {
1682 if (this._sock.rQwait("vencrypt version", 2)) { return false; }
1683
1684 const major = this._sock.rQshift8();
1685 const minor = this._sock.rQshift8();
1686
1687 if (!(major == 0 && minor == 2)) {
1688 return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
1689 }
1690
1691 this._sock.sQpush8(0);
1692 this._sock.sQpush8(2);
1693 this._sock.flush();
1694 this._rfbVeNCryptState = 1;
1695 }
1696
1697 // waiting for ACK
1698 if (this._rfbVeNCryptState == 1) {
1699 if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
1700
1701 const res = this._sock.rQshift8();
1702
1703 if (res != 0) {
1704 return this._fail("VeNCrypt failure " + res);
1705 }
1706
1707 this._rfbVeNCryptState = 2;
1708 }
1709 // must fall through here (i.e. no "else if"), beacause we may have already received
1710 // the subtypes length and won't be called again
1711
1712 if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
1713 if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
1714
1715 const subtypesLength = this._sock.rQshift8();
1716 if (subtypesLength < 1) {
1717 return this._fail("VeNCrypt subtypes empty");
1718 }
1719
1720 this._rfbVeNCryptSubtypesLength = subtypesLength;
1721 this._rfbVeNCryptState = 3;
1722 }
1723
1724 // waiting for subtypes list
1725 if (this._rfbVeNCryptState == 3) {
1726 if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
1727
1728 const subtypes = [];
1729 for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
1730 subtypes.push(this._sock.rQshift32());
1731 }
1732
1733 // Look for a matching security type in the order that the
1734 // server prefers
1735 this._rfbAuthScheme = -1;
1736 for (let type of subtypes) {
1737 // Avoid getting in to a loop
1738 if (type === securityTypeVeNCrypt) {
1739 continue;
1740 }
1741
1742 if (this._isSupportedSecurityType(type)) {
1743 this._rfbAuthScheme = type;
1744 break;
1745 }
1746 }
1747
1748 if (this._rfbAuthScheme === -1) {
1749 return this._fail("Unsupported security types (types: " + subtypes + ")");
1750 }
1751
1752 this._sock.sQpush32(this._rfbAuthScheme);
1753 this._sock.flush();
1754
1755 this._rfbVeNCryptState = 4;
1756 return true;
1757 }
1758 }
1759
1760 _negotiatePlainAuth() {
1761 if (this._rfbCredentials.username === undefined ||
1762 this._rfbCredentials.password === undefined) {
1763 this.dispatchEvent(new CustomEvent(
1764 "credentialsrequired",
1765 { detail: { types: ["username", "password"] } }));
1766 return false;
1767 }
1768
1769 const user = encodeUTF8(this._rfbCredentials.username);
1770 const pass = encodeUTF8(this._rfbCredentials.password);
1771
1772 this._sock.sQpush32(user.length);
1773 this._sock.sQpush32(pass.length);
1774 this._sock.sQpushString(user);
1775 this._sock.sQpushString(pass);
1776 this._sock.flush();
1777
1778 this._rfbInitState = "SecurityResult";
1779 return true;
1780 }
1781
1782 _negotiateStdVNCAuth() {
1783 if (this._sock.rQwait("auth challenge", 16)) { return false; }
1784
1785 if (this._rfbCredentials.password === undefined) {
1786 this.dispatchEvent(new CustomEvent(
1787 "credentialsrequired",
1788 { detail: { types: ["password"] } }));
1789 return false;
1790 }
1791
1792 // TODO(directxman12): make genDES not require an Array
1793 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
1794 const response = RFB.genDES(this._rfbCredentials.password, challenge);
1795 this._sock.sQpushBytes(response);
1796 this._sock.flush();
1797 this._rfbInitState = "SecurityResult";
1798 return true;
1799 }
1800
1801 _negotiateARDAuth() {
1802
1803 if (this._rfbCredentials.username === undefined ||
1804 this._rfbCredentials.password === undefined) {
1805 this.dispatchEvent(new CustomEvent(
1806 "credentialsrequired",
1807 { detail: { types: ["username", "password"] } }));
1808 return false;
1809 }
1810
1811 if (this._rfbCredentials.ardPublicKey != undefined &&
1812 this._rfbCredentials.ardCredentials != undefined) {
1813 // if the async web crypto is done return the results
1814 this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
1815 this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
1816 this._sock.flush();
1817 this._rfbCredentials.ardCredentials = null;
1818 this._rfbCredentials.ardPublicKey = null;
1819 this._rfbInitState = "SecurityResult";
1820 return true;
1821 }
1822
1823 if (this._sock.rQwait("read ard", 4)) { return false; }
1824
1825 let generator = this._sock.rQshiftBytes(2); // DH base generator value
1826
1827 let keyLength = this._sock.rQshift16();
1828
1829 if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
1830
1831 // read the server values
1832 let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
1833 let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
1834
1835 let clientKey = legacyCrypto.generateKey(
1836 { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
1837 this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
1838
1839 return false;
1840 }
1841
1842 async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
1843 const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
1844 const sharedKey = legacyCrypto.deriveBits(
1845 { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
1846
1847 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
1848 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1849
1850 const credentials = window.crypto.getRandomValues(new Uint8Array(128));
1851 for (let i = 0; i < username.length; i++) {
1852 credentials[i] = username.charCodeAt(i);
1853 }
1854 credentials[username.length] = 0;
1855 for (let i = 0; i < password.length; i++) {
1856 credentials[64 + i] = password.charCodeAt(i);
1857 }
1858 credentials[64 + password.length] = 0;
1859
1860 const key = await legacyCrypto.digest("MD5", sharedKey);
1861 const cipher = await legacyCrypto.importKey(
1862 "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
1863 const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
1864
1865 this._rfbCredentials.ardCredentials = encrypted;
1866 this._rfbCredentials.ardPublicKey = clientPublicKey;
1867
1868 this._resumeAuthentication();
1869 }
1870
1871 _negotiateTightUnixAuth() {
1872 if (this._rfbCredentials.username === undefined ||
1873 this._rfbCredentials.password === undefined) {
1874 this.dispatchEvent(new CustomEvent(
1875 "credentialsrequired",
1876 { detail: { types: ["username", "password"] } }));
1877 return false;
1878 }
1879
1880 this._sock.sQpush32(this._rfbCredentials.username.length);
1881 this._sock.sQpush32(this._rfbCredentials.password.length);
1882 this._sock.sQpushString(this._rfbCredentials.username);
1883 this._sock.sQpushString(this._rfbCredentials.password);
1884 this._sock.flush();
1885
1886 this._rfbInitState = "SecurityResult";
1887 return true;
1888 }
1889
1890 _negotiateTightTunnels(numTunnels) {
1891 const clientSupportedTunnelTypes = {
1892 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
1893 };
1894 const serverSupportedTunnelTypes = {};
1895 // receive tunnel capabilities
1896 for (let i = 0; i < numTunnels; i++) {
1897 const capCode = this._sock.rQshift32();
1898 const capVendor = this._sock.rQshiftStr(4);
1899 const capSignature = this._sock.rQshiftStr(8);
1900 serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
1901 }
1902
1903 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
1904
1905 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1906 // but forgets to advertise it. Try to detect such servers by
1907 // looking for their custom tunnel type.
1908 if (serverSupportedTunnelTypes[1] &&
1909 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
1910 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
1911 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1912 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
1913 }
1914
1915 // choose the notunnel type
1916 if (serverSupportedTunnelTypes[0]) {
1917 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
1918 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
1919 return this._fail("Client's tunnel type had the incorrect " +
1920 "vendor or signature");
1921 }
1922 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1923 this._sock.sQpush32(0); // use NOTUNNEL
1924 this._sock.flush();
1925 return false; // wait until we receive the sub auth count to continue
1926 } else {
1927 return this._fail("Server wanted tunnels, but doesn't support " +
1928 "the notunnel type");
1929 }
1930 }
1931
1932 _negotiateTightAuth() {
1933 if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
1934 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1935 const numTunnels = this._sock.rQshift32();
1936 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1937
1938 this._rfbTightVNC = true;
1939
1940 if (numTunnels > 0) {
1941 this._negotiateTightTunnels(numTunnels);
1942 return false; // wait until we receive the sub auth to continue
1943 }
1944 }
1945
1946 // second pass, do the sub-auth negotiation
1947 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1948 const subAuthCount = this._sock.rQshift32();
1949 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1950 this._rfbInitState = 'SecurityResult';
1951 return true;
1952 }
1953
1954 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1955
1956 const clientSupportedTypes = {
1957 'STDVNOAUTH__': 1,
1958 'STDVVNCAUTH_': 2,
1959 'TGHTULGNAUTH': 129
1960 };
1961
1962 const serverSupportedTypes = [];
1963
1964 for (let i = 0; i < subAuthCount; i++) {
1965 this._sock.rQshift32(); // capNum
1966 const capabilities = this._sock.rQshiftStr(12);
1967 serverSupportedTypes.push(capabilities);
1968 }
1969
1970 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1971
1972 for (let authType in clientSupportedTypes) {
1973 if (serverSupportedTypes.indexOf(authType) != -1) {
1974 this._sock.sQpush32(clientSupportedTypes[authType]);
1975 this._sock.flush();
1976 Log.Debug("Selected authentication type: " + authType);
1977
1978 switch (authType) {
1979 case 'STDVNOAUTH__': // no auth
1980 this._rfbInitState = 'SecurityResult';
1981 return true;
1982 case 'STDVVNCAUTH_':
1983 this._rfbAuthScheme = securityTypeVNCAuth;
1984 return true;
1985 case 'TGHTULGNAUTH':
1986 this._rfbAuthScheme = securityTypeUnixLogon;
1987 return true;
1988 default:
1989 return this._fail("Unsupported tiny auth scheme " +
1990 "(scheme: " + authType + ")");
1991 }
1992 }
1993 }
1994
1995 return this._fail("No supported sub-auth types!");
1996 }
1997
1998 _handleRSAAESCredentialsRequired(event) {
1999 this.dispatchEvent(event);
2000 }
2001
2002 _handleRSAAESServerVerification(event) {
2003 this.dispatchEvent(event);
2004 }
2005
2006 _negotiateRA2neAuth() {
2007 if (this._rfbRSAAESAuthenticationState === null) {
2008 this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
2009 this._rfbRSAAESAuthenticationState.addEventListener(
2010 "serververification", this._eventHandlers.handleRSAAESServerVerification);
2011 this._rfbRSAAESAuthenticationState.addEventListener(
2012 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
2013 }
2014 this._rfbRSAAESAuthenticationState.checkInternalEvents();
2015 if (!this._rfbRSAAESAuthenticationState.hasStarted) {
2016 this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
2017 .catch((e) => {
2018 if (e.message !== "disconnect normally") {
2019 this._fail(e.message);
2020 }
2021 })
2022 .then(() => {
2023 this._rfbInitState = "SecurityResult";
2024 return true;
2025 }).finally(() => {
2026 this._rfbRSAAESAuthenticationState.removeEventListener(
2027 "serververification", this._eventHandlers.handleRSAAESServerVerification);
2028 this._rfbRSAAESAuthenticationState.removeEventListener(
2029 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
2030 this._rfbRSAAESAuthenticationState = null;
2031 });
2032 }
2033 return false;
2034 }
2035
2036 _negotiateMSLogonIIAuth() {
2037 if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
2038
2039 if (this._rfbCredentials.username === undefined ||
2040 this._rfbCredentials.password === undefined) {
2041 this.dispatchEvent(new CustomEvent(
2042 "credentialsrequired",
2043 { detail: { types: ["username", "password"] } }));
2044 return false;
2045 }
2046
2047 const g = this._sock.rQshiftBytes(8);
2048 const p = this._sock.rQshiftBytes(8);
2049 const A = this._sock.rQshiftBytes(8);
2050 const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
2051 const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
2052 const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
2053
2054 const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
2055 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
2056 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
2057 let usernameBytes = new Uint8Array(256);
2058 let passwordBytes = new Uint8Array(64);
2059 window.crypto.getRandomValues(usernameBytes);
2060 window.crypto.getRandomValues(passwordBytes);
2061 for (let i = 0; i < username.length; i++) {
2062 usernameBytes[i] = username.charCodeAt(i);
2063 }
2064 usernameBytes[username.length] = 0;
2065 for (let i = 0; i < password.length; i++) {
2066 passwordBytes[i] = password.charCodeAt(i);
2067 }
2068 passwordBytes[password.length] = 0;
2069 usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
2070 passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
2071 this._sock.sQpushBytes(B);
2072 this._sock.sQpushBytes(usernameBytes);
2073 this._sock.sQpushBytes(passwordBytes);
2074 this._sock.flush();
2075 this._rfbInitState = "SecurityResult";
2076 return true;
2077 }
2078
2079 _negotiateAuthentication() {
2080 switch (this._rfbAuthScheme) {
2081 case securityTypeNone:
2082 if (this._rfbVersion >= 3.8) {
2083 this._rfbInitState = 'SecurityResult';
2084 } else {
2085 this._rfbInitState = 'ClientInitialisation';
2086 }
2087 return true;
2088
2089 case securityTypeXVP:
2090 return this._negotiateXvpAuth();
2091
2092 case securityTypeARD:
2093 return this._negotiateARDAuth();
2094
2095 case securityTypeVNCAuth:
2096 return this._negotiateStdVNCAuth();
2097
2098 case securityTypeTight:
2099 return this._negotiateTightAuth();
2100
2101 case securityTypeVeNCrypt:
2102 return this._negotiateVeNCryptAuth();
2103
2104 case securityTypePlain:
2105 return this._negotiatePlainAuth();
2106
2107 case securityTypeUnixLogon:
2108 return this._negotiateTightUnixAuth();
2109
2110 case securityTypeRA2ne:
2111 return this._negotiateRA2neAuth();
2112
2113 case securityTypeMSLogonII:
2114 return this._negotiateMSLogonIIAuth();
2115
2116 default:
2117 return this._fail("Unsupported auth scheme (scheme: " +
2118 this._rfbAuthScheme + ")");
2119 }
2120 }
2121
2122 _handleSecurityResult() {
2123 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
2124
2125 const status = this._sock.rQshift32();
2126
2127 if (status === 0) { // OK
2128 this._rfbInitState = 'ClientInitialisation';
2129 Log.Debug('Authentication OK');
2130 return true;
2131 } else {
2132 if (this._rfbVersion >= 3.8) {
2133 this._rfbInitState = "SecurityReason";
2134 this._securityContext = "security result";
2135 this._securityStatus = status;
2136 return true;
2137 } else {
2138 this.dispatchEvent(new CustomEvent(
2139 "securityfailure",
2140 { detail: { status: status } }));
2141
2142 return this._fail("Security handshake failed");
2143 }
2144 }
2145 }
2146
2147 _negotiateServerInit() {
2148 if (this._sock.rQwait("server initialization", 24)) { return false; }
2149
2150 /* Screen size */
2151 const width = this._sock.rQshift16();
2152 const height = this._sock.rQshift16();
2153
2154 /* PIXEL_FORMAT */
2155 const bpp = this._sock.rQshift8();
2156 const depth = this._sock.rQshift8();
2157 const bigEndian = this._sock.rQshift8();
2158 const trueColor = this._sock.rQshift8();
2159
2160 const redMax = this._sock.rQshift16();
2161 const greenMax = this._sock.rQshift16();
2162 const blueMax = this._sock.rQshift16();
2163 const redShift = this._sock.rQshift8();
2164 const greenShift = this._sock.rQshift8();
2165 const blueShift = this._sock.rQshift8();
2166 this._sock.rQskipBytes(3); // padding
2167
2168 // NB(directxman12): we don't want to call any callbacks or print messages until
2169 // *after* we're past the point where we could backtrack
2170
2171 /* Connection name/title */
2172 const nameLength = this._sock.rQshift32();
2173 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
2174 let name = this._sock.rQshiftStr(nameLength);
2175 name = decodeUTF8(name, true);
2176
2177 if (this._rfbTightVNC) {
2178 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
2179 // In TightVNC mode, ServerInit message is extended
2180 const numServerMessages = this._sock.rQshift16();
2181 const numClientMessages = this._sock.rQshift16();
2182 const numEncodings = this._sock.rQshift16();
2183 this._sock.rQskipBytes(2); // padding
2184
2185 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
2186 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
2187
2188 // we don't actually do anything with the capability information that TIGHT sends,
2189 // so we just skip the all of this.
2190
2191 // TIGHT server message capabilities
2192 this._sock.rQskipBytes(16 * numServerMessages);
2193
2194 // TIGHT client message capabilities
2195 this._sock.rQskipBytes(16 * numClientMessages);
2196
2197 // TIGHT encoding capabilities
2198 this._sock.rQskipBytes(16 * numEncodings);
2199 }
2200
2201 // NB(directxman12): these are down here so that we don't run them multiple times
2202 // if we backtrack
2203 Log.Info("Screen: " + width + "x" + height +
2204 ", bpp: " + bpp + ", depth: " + depth +
2205 ", bigEndian: " + bigEndian +
2206 ", trueColor: " + trueColor +
2207 ", redMax: " + redMax +
2208 ", greenMax: " + greenMax +
2209 ", blueMax: " + blueMax +
2210 ", redShift: " + redShift +
2211 ", greenShift: " + greenShift +
2212 ", blueShift: " + blueShift);
2213
2214 // we're past the point where we could backtrack, so it's safe to call this
2215 this._setDesktopName(name);
2216 this._resize(width, height);
2217
2218 if (!this._viewOnly) {
2219 this._keyboard.grab();
2220 this._asyncClipboard.grab();
2221 }
2222
2223 this._fbDepth = 24;
2224
2225 if (this._fbName === "Intel(r) AMT KVM") {
2226 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
2227 this._fbDepth = 8;
2228 }
2229
2230 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
2231 this._sendEncodings();
2232 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
2233
2234 this._updateConnectionState('connected');
2235 return true;
2236 }
2237
2238 _sendEncodings() {
2239 const encs = [];
2240
2241 // In preference order
2242 encs.push(encodings.encodingCopyRect);
2243 // Only supported with full depth support
2244 if (this._fbDepth == 24) {
2245 if (supportsWebCodecsH264Decode) {
2246 encs.push(encodings.encodingH264);
2247 }
2248 encs.push(encodings.encodingTight);
2249 encs.push(encodings.encodingTightPNG);
2250 encs.push(encodings.encodingZRLE);
2251 encs.push(encodings.encodingJPEG);
2252 encs.push(encodings.encodingHextile);
2253 encs.push(encodings.encodingRRE);
2254 encs.push(encodings.encodingZlib);
2255 }
2256 encs.push(encodings.encodingRaw);
2257
2258 // Psuedo-encoding settings
2259 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
2260 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
2261
2262 encs.push(encodings.pseudoEncodingDesktopSize);
2263 encs.push(encodings.pseudoEncodingLastRect);
2264 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
2265 encs.push(encodings.pseudoEncodingQEMULedEvent);
2266 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
2267 encs.push(encodings.pseudoEncodingXvp);
2268 encs.push(encodings.pseudoEncodingFence);
2269 encs.push(encodings.pseudoEncodingContinuousUpdates);
2270 encs.push(encodings.pseudoEncodingDesktopName);
2271 encs.push(encodings.pseudoEncodingExtendedClipboard);
2272 encs.push(encodings.pseudoEncodingExtendedMouseButtons);
2273
2274 if (this._fbDepth == 24) {
2275 encs.push(encodings.pseudoEncodingVMwareCursor);
2276 encs.push(encodings.pseudoEncodingCursor);
2277 }
2278
2279 RFB.messages.clientEncodings(this._sock, encs);
2280 }
2281
2282 /* RFB protocol initialization states:
2283 * ProtocolVersion
2284 * Security
2285 * Authentication
2286 * SecurityResult
2287 * ClientInitialization - not triggered by server message
2288 * ServerInitialization
2289 */
2290 _initMsg() {
2291 switch (this._rfbInitState) {
2292 case 'ProtocolVersion':
2293 return this._negotiateProtocolVersion();
2294
2295 case 'Security':
2296 return this._negotiateSecurity();
2297
2298 case 'Authentication':
2299 return this._negotiateAuthentication();
2300
2301 case 'SecurityResult':
2302 return this._handleSecurityResult();
2303
2304 case 'SecurityReason':
2305 return this._handleSecurityReason();
2306
2307 case 'ClientInitialisation':
2308 this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
2309 this._sock.flush();
2310 this._rfbInitState = 'ServerInitialisation';
2311 return true;
2312
2313 case 'ServerInitialisation':
2314 return this._negotiateServerInit();
2315
2316 default:
2317 return this._fail("Unknown init state (state: " +
2318 this._rfbInitState + ")");
2319 }
2320 }
2321
2322 // Resume authentication handshake after it was paused for some
2323 // reason, e.g. waiting for a password from the user
2324 _resumeAuthentication() {
2325 // We use setTimeout() so it's run in its own context, just like
2326 // it originally did via the WebSocket's event handler
2327 setTimeout(this._initMsg.bind(this), 0);
2328 }
2329
2330 _handleSetColourMapMsg() {
2331 Log.Debug("SetColorMapEntries");
2332
2333 return this._fail("Unexpected SetColorMapEntries message");
2334 }
2335
2336 _writeClipboard(text) {
2337 if (this._viewOnly) return;
2338 if (this._asyncClipboard.writeClipboard(text)) return;
2339 // Fallback clipboard
2340 this.dispatchEvent(
2341 new CustomEvent("clipboard", {detail: {text: text}})
2342 );
2343 }
2344
2345 _handleServerCutText() {
2346 Log.Debug("ServerCutText");
2347
2348 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
2349
2350 this._sock.rQskipBytes(3); // Padding
2351
2352 let length = this._sock.rQshift32();
2353 length = toSigned32bit(length);
2354
2355 if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
2356
2357 if (length >= 0) {
2358 //Standard msg
2359 const text = this._sock.rQshiftStr(length);
2360 if (this._viewOnly) {
2361 return true;
2362 }
2363
2364 this._writeClipboard(text);
2365
2366 } else {
2367 //Extended msg.
2368 length = Math.abs(length);
2369 const flags = this._sock.rQshift32();
2370 let formats = flags & 0x0000FFFF;
2371 let actions = flags & 0xFF000000;
2372
2373 let isCaps = (!!(actions & extendedClipboardActionCaps));
2374 if (isCaps) {
2375 this._clipboardServerCapabilitiesFormats = {};
2376 this._clipboardServerCapabilitiesActions = {};
2377
2378 // Update our server capabilities for Formats
2379 for (let i = 0; i <= 15; i++) {
2380 let index = 1 << i;
2381
2382 // Check if format flag is set.
2383 if ((formats & index)) {
2384 this._clipboardServerCapabilitiesFormats[index] = true;
2385 // We don't send unsolicited clipboard, so we
2386 // ignore the size
2387 this._sock.rQshift32();
2388 }
2389 }
2390
2391 // Update our server capabilities for Actions
2392 for (let i = 24; i <= 31; i++) {
2393 let index = 1 << i;
2394 this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
2395 }
2396
2397 /* Caps handling done, send caps with the clients
2398 capabilities set as a response */
2399 let clientActions = [
2400 extendedClipboardActionCaps,
2401 extendedClipboardActionRequest,
2402 extendedClipboardActionPeek,
2403 extendedClipboardActionNotify,
2404 extendedClipboardActionProvide
2405 ];
2406 RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
2407
2408 } else if (actions === extendedClipboardActionRequest) {
2409 if (this._viewOnly) {
2410 return true;
2411 }
2412
2413 // Check if server has told us it can handle Provide and there is clipboard data to send.
2414 if (this._clipboardText != null &&
2415 this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
2416
2417 if (formats & extendedClipboardFormatText) {
2418 RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
2419 }
2420 }
2421
2422 } else if (actions === extendedClipboardActionPeek) {
2423 if (this._viewOnly) {
2424 return true;
2425 }
2426
2427 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
2428
2429 if (this._clipboardText != null) {
2430 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
2431 } else {
2432 RFB.messages.extendedClipboardNotify(this._sock, []);
2433 }
2434 }
2435
2436 } else if (actions === extendedClipboardActionNotify) {
2437 if (this._viewOnly) {
2438 return true;
2439 }
2440
2441 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
2442
2443 if (formats & extendedClipboardFormatText) {
2444 RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
2445 }
2446 }
2447
2448 } else if (actions === extendedClipboardActionProvide) {
2449 if (this._viewOnly) {
2450 return true;
2451 }
2452
2453 if (!(formats & extendedClipboardFormatText)) {
2454 return true;
2455 }
2456 // Ignore what we had in our clipboard client side.
2457 this._clipboardText = null;
2458
2459 // FIXME: Should probably verify that this data was actually requested
2460 let zlibStream = this._sock.rQshiftBytes(length - 4);
2461 let streamInflator = new Inflator();
2462 let textData = null;
2463
2464 streamInflator.setInput(zlibStream);
2465 for (let i = 0; i <= 15; i++) {
2466 let format = 1 << i;
2467
2468 if (formats & format) {
2469
2470 let size = 0x00;
2471 let sizeArray = streamInflator.inflate(4);
2472
2473 size |= (sizeArray[0] << 24);
2474 size |= (sizeArray[1] << 16);
2475 size |= (sizeArray[2] << 8);
2476 size |= (sizeArray[3]);
2477 let chunk = streamInflator.inflate(size);
2478
2479 if (format === extendedClipboardFormatText) {
2480 textData = chunk;
2481 }
2482 }
2483 }
2484 streamInflator.setInput(null);
2485
2486 if (textData !== null) {
2487 let tmpText = "";
2488 for (let i = 0; i < textData.length; i++) {
2489 tmpText += String.fromCharCode(textData[i]);
2490 }
2491 textData = tmpText;
2492
2493 textData = decodeUTF8(textData);
2494 if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
2495 textData = textData.slice(0, -1);
2496 }
2497
2498 textData = textData.replaceAll("\r\n", "\n");
2499
2500 this._writeClipboard(textData);
2501 }
2502 } else {
2503 return this._fail("Unexpected action in extended clipboard message: " + actions);
2504 }
2505 }
2506 return true;
2507 }
2508
2509 _handleServerFenceMsg() {
2510 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
2511 this._sock.rQskipBytes(3); // Padding
2512 let flags = this._sock.rQshift32();
2513 let length = this._sock.rQshift8();
2514
2515 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
2516
2517 if (length > 64) {
2518 Log.Warn("Bad payload length (" + length + ") in fence response");
2519 length = 64;
2520 }
2521
2522 const payload = this._sock.rQshiftStr(length);
2523
2524 this._supportsFence = true;
2525
2526 /*
2527 * Fence flags
2528 *
2529 * (1<<0) - BlockBefore
2530 * (1<<1) - BlockAfter
2531 * (1<<2) - SyncNext
2532 * (1<<31) - Request
2533 */
2534
2535 if (!(flags & (1<<31))) {
2536 return this._fail("Unexpected fence response");
2537 }
2538
2539 // Filter out unsupported flags
2540 // FIXME: support syncNext
2541 flags &= (1<<0) | (1<<1);
2542
2543 // BlockBefore and BlockAfter are automatically handled by
2544 // the fact that we process each incoming message
2545 // synchronuosly.
2546 RFB.messages.clientFence(this._sock, flags, payload);
2547
2548 return true;
2549 }
2550
2551 _handleXvpMsg() {
2552 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
2553 this._sock.rQskipBytes(1); // Padding
2554 const xvpVer = this._sock.rQshift8();
2555 const xvpMsg = this._sock.rQshift8();
2556
2557 switch (xvpMsg) {
2558 case 0: // XVP_FAIL
2559 Log.Error("XVP operation failed");
2560 break;
2561 case 1: // XVP_INIT
2562 this._rfbXvpVer = xvpVer;
2563 Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
2564 this._setCapability("power", true);
2565 break;
2566 default:
2567 this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
2568 break;
2569 }
2570
2571 return true;
2572 }
2573
2574 _normalMsg() {
2575 let msgType;
2576 if (this._FBU.rects > 0) {
2577 msgType = 0;
2578 } else {
2579 msgType = this._sock.rQshift8();
2580 }
2581
2582 let first, ret;
2583 switch (msgType) {
2584 case 0: // FramebufferUpdate
2585 ret = this._framebufferUpdate();
2586 if (ret && !this._enabledContinuousUpdates) {
2587 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
2588 this._fbWidth, this._fbHeight);
2589 }
2590 return ret;
2591
2592 case 1: // SetColorMapEntries
2593 return this._handleSetColourMapMsg();
2594
2595 case 2: // Bell
2596 Log.Debug("Bell");
2597 this.dispatchEvent(new CustomEvent(
2598 "bell",
2599 { detail: {} }));
2600 return true;
2601
2602 case 3: // ServerCutText
2603 return this._handleServerCutText();
2604
2605 case 150: // EndOfContinuousUpdates
2606 first = !this._supportsContinuousUpdates;
2607 this._supportsContinuousUpdates = true;
2608 this._enabledContinuousUpdates = false;
2609 if (first) {
2610 this._enabledContinuousUpdates = true;
2611 this._updateContinuousUpdates();
2612 Log.Info("Enabling continuous updates.");
2613 } else {
2614 // FIXME: We need to send a framebufferupdaterequest here
2615 // if we add support for turning off continuous updates
2616 }
2617 return true;
2618
2619 case 248: // ServerFence
2620 return this._handleServerFenceMsg();
2621
2622 case 250: // XVP
2623 return this._handleXvpMsg();
2624
2625 default:
2626 this._fail("Unexpected server message (type " + msgType + ")");
2627 Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
2628 return true;
2629 }
2630 }
2631
2632 _framebufferUpdate() {
2633 if (this._FBU.rects === 0) {
2634 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
2635 this._sock.rQskipBytes(1); // Padding
2636 this._FBU.rects = this._sock.rQshift16();
2637
2638 // Make sure the previous frame is fully rendered first
2639 // to avoid building up an excessive queue
2640 if (this._display.pending()) {
2641 this._flushing = true;
2642 this._display.flush()
2643 .then(() => {
2644 this._flushing = false;
2645 // Resume processing
2646 if (!this._sock.rQwait("message", 1)) {
2647 this._handleMessage();
2648 }
2649 });
2650 return false;
2651 }
2652 }
2653
2654 while (this._FBU.rects > 0) {
2655 if (this._FBU.encoding === null) {
2656 if (this._sock.rQwait("rect header", 12)) { return false; }
2657 /* New FramebufferUpdate */
2658
2659 this._FBU.x = this._sock.rQshift16();
2660 this._FBU.y = this._sock.rQshift16();
2661 this._FBU.width = this._sock.rQshift16();
2662 this._FBU.height = this._sock.rQshift16();
2663 this._FBU.encoding = this._sock.rQshift32();
2664 /* Encodings are signed */
2665 this._FBU.encoding >>= 0;
2666 }
2667
2668 if (!this._handleRect()) {
2669 return false;
2670 }
2671
2672 this._FBU.rects--;
2673 this._FBU.encoding = null;
2674 }
2675
2676 this._display.flip();
2677
2678 return true; // We finished this FBU
2679 }
2680
2681 _handleRect() {
2682 switch (this._FBU.encoding) {
2683 case encodings.pseudoEncodingLastRect:
2684 this._FBU.rects = 1; // Will be decreased when we return
2685 return true;
2686
2687 case encodings.pseudoEncodingVMwareCursor:
2688 return this._handleVMwareCursor();
2689
2690 case encodings.pseudoEncodingCursor:
2691 return this._handleCursor();
2692
2693 case encodings.pseudoEncodingQEMUExtendedKeyEvent:
2694 this._qemuExtKeyEventSupported = true;
2695 return true;
2696
2697 case encodings.pseudoEncodingDesktopName:
2698 return this._handleDesktopName();
2699
2700 case encodings.pseudoEncodingDesktopSize:
2701 this._resize(this._FBU.width, this._FBU.height);
2702 return true;
2703
2704 case encodings.pseudoEncodingExtendedDesktopSize:
2705 return this._handleExtendedDesktopSize();
2706
2707 case encodings.pseudoEncodingExtendedMouseButtons:
2708 this._extendedPointerEventSupported = true;
2709 return true;
2710
2711 case encodings.pseudoEncodingQEMULedEvent:
2712 return this._handleLedEvent();
2713
2714 default:
2715 return this._handleDataRect();
2716 }
2717 }
2718
2719 _handleVMwareCursor() {
2720 const hotx = this._FBU.x; // hotspot-x
2721 const hoty = this._FBU.y; // hotspot-y
2722 const w = this._FBU.width;
2723 const h = this._FBU.height;
2724 if (this._sock.rQwait("VMware cursor encoding", 1)) {
2725 return false;
2726 }
2727
2728 const cursorType = this._sock.rQshift8();
2729
2730 this._sock.rQshift8(); //Padding
2731
2732 let rgba;
2733 const bytesPerPixel = 4;
2734
2735 //Classic cursor
2736 if (cursorType == 0) {
2737 //Used to filter away unimportant bits.
2738 //OR is used for correct conversion in js.
2739 const PIXEL_MASK = 0xffffff00 | 0;
2740 rgba = new Array(w * h * bytesPerPixel);
2741
2742 if (this._sock.rQwait("VMware cursor classic encoding",
2743 (w * h * bytesPerPixel) * 2, 2)) {
2744 return false;
2745 }
2746
2747 let andMask = new Array(w * h);
2748 for (let pixel = 0; pixel < (w * h); pixel++) {
2749 andMask[pixel] = this._sock.rQshift32();
2750 }
2751
2752 let xorMask = new Array(w * h);
2753 for (let pixel = 0; pixel < (w * h); pixel++) {
2754 xorMask[pixel] = this._sock.rQshift32();
2755 }
2756
2757 for (let pixel = 0; pixel < (w * h); pixel++) {
2758 if (andMask[pixel] == 0) {
2759 //Fully opaque pixel
2760 let bgr = xorMask[pixel];
2761 let r = bgr >> 8 & 0xff;
2762 let g = bgr >> 16 & 0xff;
2763 let b = bgr >> 24 & 0xff;
2764
2765 rgba[(pixel * bytesPerPixel) ] = r; //r
2766 rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
2767 rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
2768 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
2769
2770 } else if ((andMask[pixel] & PIXEL_MASK) ==
2771 PIXEL_MASK) {
2772 //Only screen value matters, no mouse colouring
2773 if (xorMask[pixel] == 0) {
2774 //Transparent pixel
2775 rgba[(pixel * bytesPerPixel) ] = 0x00;
2776 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2777 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2778 rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
2779
2780 } else if ((xorMask[pixel] & PIXEL_MASK) ==
2781 PIXEL_MASK) {
2782 //Inverted pixel, not supported in browsers.
2783 //Fully opaque instead.
2784 rgba[(pixel * bytesPerPixel) ] = 0x00;
2785 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2786 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2787 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2788
2789 } else {
2790 //Unhandled xorMask
2791 rgba[(pixel * bytesPerPixel) ] = 0x00;
2792 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2793 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2794 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2795 }
2796
2797 } else {
2798 //Unhandled andMask
2799 rgba[(pixel * bytesPerPixel) ] = 0x00;
2800 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2801 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2802 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2803 }
2804 }
2805
2806 //Alpha cursor.
2807 } else if (cursorType == 1) {
2808 if (this._sock.rQwait("VMware cursor alpha encoding",
2809 (w * h * 4), 2)) {
2810 return false;
2811 }
2812
2813 rgba = new Array(w * h * bytesPerPixel);
2814
2815 for (let pixel = 0; pixel < (w * h); pixel++) {
2816 let data = this._sock.rQshift32();
2817
2818 rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
2819 rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
2820 rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
2821 rgba[(pixel * 4) + 3 ] = data & 0xff; //a
2822 }
2823
2824 } else {
2825 Log.Warn("The given cursor type is not supported: "
2826 + cursorType + " given.");
2827 return false;
2828 }
2829
2830 this._updateCursor(rgba, hotx, hoty, w, h);
2831
2832 return true;
2833 }
2834
2835 _handleCursor() {
2836 const hotx = this._FBU.x; // hotspot-x
2837 const hoty = this._FBU.y; // hotspot-y
2838 const w = this._FBU.width;
2839 const h = this._FBU.height;
2840
2841 const pixelslength = w * h * 4;
2842 const masklength = Math.ceil(w / 8) * h;
2843
2844 let bytes = pixelslength + masklength;
2845 if (this._sock.rQwait("cursor encoding", bytes)) {
2846 return false;
2847 }
2848
2849 // Decode from BGRX pixels + bit mask to RGBA
2850 const pixels = this._sock.rQshiftBytes(pixelslength);
2851 const mask = this._sock.rQshiftBytes(masklength);
2852 let rgba = new Uint8Array(w * h * 4);
2853
2854 let pixIdx = 0;
2855 for (let y = 0; y < h; y++) {
2856 for (let x = 0; x < w; x++) {
2857 let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
2858 let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
2859 rgba[pixIdx ] = pixels[pixIdx + 2];
2860 rgba[pixIdx + 1] = pixels[pixIdx + 1];
2861 rgba[pixIdx + 2] = pixels[pixIdx];
2862 rgba[pixIdx + 3] = alpha;
2863 pixIdx += 4;
2864 }
2865 }
2866
2867 this._updateCursor(rgba, hotx, hoty, w, h);
2868
2869 return true;
2870 }
2871
2872 _handleDesktopName() {
2873 if (this._sock.rQwait("DesktopName", 4)) {
2874 return false;
2875 }
2876
2877 let length = this._sock.rQshift32();
2878
2879 if (this._sock.rQwait("DesktopName", length, 4)) {
2880 return false;
2881 }
2882
2883 let name = this._sock.rQshiftStr(length);
2884 name = decodeUTF8(name, true);
2885
2886 this._setDesktopName(name);
2887
2888 return true;
2889 }
2890
2891 _handleLedEvent() {
2892 if (this._sock.rQwait("LED status", 1)) {
2893 return false;
2894 }
2895
2896 let data = this._sock.rQshift8();
2897 // ScrollLock state can be retrieved with data & 1. This is currently not needed.
2898 let numLock = data & 2 ? true : false;
2899 let capsLock = data & 4 ? true : false;
2900 this._remoteCapsLock = capsLock;
2901 this._remoteNumLock = numLock;
2902
2903 return true;
2904 }
2905
2906 _handleExtendedDesktopSize() {
2907 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
2908 return false;
2909 }
2910
2911 const numberOfScreens = this._sock.rQpeek8();
2912
2913 let bytes = 4 + (numberOfScreens * 16);
2914 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
2915 return false;
2916 }
2917
2918 const firstUpdate = !this._supportsSetDesktopSize;
2919 this._supportsSetDesktopSize = true;
2920
2921 this._sock.rQskipBytes(1); // number-of-screens
2922 this._sock.rQskipBytes(3); // padding
2923
2924 for (let i = 0; i < numberOfScreens; i += 1) {
2925 // Save the id and flags of the first screen
2926 if (i === 0) {
2927 this._screenID = this._sock.rQshift32(); // id
2928 this._sock.rQskipBytes(2); // x-position
2929 this._sock.rQskipBytes(2); // y-position
2930 this._sock.rQskipBytes(2); // width
2931 this._sock.rQskipBytes(2); // height
2932 this._screenFlags = this._sock.rQshift32(); // flags
2933 } else {
2934 this._sock.rQskipBytes(16);
2935 }
2936 }
2937
2938 /*
2939 * The x-position indicates the reason for the change:
2940 *
2941 * 0 - server resized on its own
2942 * 1 - this client requested the resize
2943 * 2 - another client requested the resize
2944 */
2945
2946 if (this._FBU.x === 1) {
2947 this._pendingRemoteResize = false;
2948 }
2949
2950 // We need to handle errors when we requested the resize.
2951 if (this._FBU.x === 1 && this._FBU.y !== 0) {
2952 let msg = "";
2953 // The y-position indicates the status code from the server
2954 switch (this._FBU.y) {
2955 case 1:
2956 msg = "Resize is administratively prohibited";
2957 break;
2958 case 2:
2959 msg = "Out of resources";
2960 break;
2961 case 3:
2962 msg = "Invalid screen layout";
2963 break;
2964 default:
2965 msg = "Unknown reason";
2966 break;
2967 }
2968 Log.Warn("Server did not accept the resize request: "
2969 + msg);
2970 } else {
2971 this._resize(this._FBU.width, this._FBU.height);
2972 }
2973
2974 // Normally we only apply the current resize mode after a
2975 // window resize event. However there is no such trigger on the
2976 // initial connect. And we don't know if the server supports
2977 // resizing until we've gotten here.
2978 if (firstUpdate) {
2979 this._requestRemoteResize();
2980 }
2981
2982 if (this._FBU.x === 1 && this._FBU.y === 0) {
2983 // We might have resized again whilst waiting for the
2984 // previous request, so check if we are in sync
2985 this._requestRemoteResize();
2986 }
2987
2988 return true;
2989 }
2990
2991 _handleDataRect() {
2992 let decoder = this._decoders[this._FBU.encoding];
2993 if (!decoder) {
2994 this._fail("Unsupported encoding (encoding: " +
2995 this._FBU.encoding + ")");
2996 return false;
2997 }
2998
2999 try {
3000 return decoder.decodeRect(this._FBU.x, this._FBU.y,
3001 this._FBU.width, this._FBU.height,
3002 this._sock, this._display,
3003 this._fbDepth);
3004 } catch (err) {
3005 this._fail("Error decoding rect: " + err);
3006 return false;
3007 }
3008 }
3009
3010 _updateContinuousUpdates() {
3011 if (!this._enabledContinuousUpdates) { return; }
3012
3013 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
3014 this._fbWidth, this._fbHeight);
3015 }
3016
3017 // Handle resize-messages from the server
3018 _resize(width, height) {
3019 this._fbWidth = width;
3020 this._fbHeight = height;
3021
3022 this._display.resize(this._fbWidth, this._fbHeight);
3023
3024 // Adjust the visible viewport based on the new dimensions
3025 this._updateClip();
3026 this._updateScale();
3027
3028 this._updateContinuousUpdates();
3029
3030 // Keep this size until browser client size changes
3031 this._saveExpectedClientSize();
3032 }
3033
3034 _xvpOp(ver, op) {
3035 if (this._rfbXvpVer < ver) { return; }
3036 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
3037 RFB.messages.xvpOp(this._sock, ver, op);
3038 }
3039
3040 _updateCursor(rgba, hotx, hoty, w, h) {
3041 this._cursorImage = {
3042 rgbaPixels: rgba,
3043 hotx: hotx, hoty: hoty, w: w, h: h,
3044 };
3045 this._refreshCursor();
3046 }
3047
3048 _shouldShowDotCursor() {
3049 // Called when this._cursorImage is updated
3050 if (!this._showDotCursor) {
3051 // User does not want to see the dot, so...
3052 return false;
3053 }
3054
3055 // The dot should not be shown if the cursor is already visible,
3056 // i.e. contains at least one not-fully-transparent pixel.
3057 // So iterate through all alpha bytes in rgba and stop at the
3058 // first non-zero.
3059 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
3060 if (this._cursorImage.rgbaPixels[i]) {
3061 return false;
3062 }
3063 }
3064
3065 // At this point, we know that the cursor is fully transparent, and
3066 // the user wants to see the dot instead of this.
3067 return true;
3068 }
3069
3070 _refreshCursor() {
3071 if (this._rfbConnectionState !== "connecting" &&
3072 this._rfbConnectionState !== "connected") {
3073 return;
3074 }
3075 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
3076 this._cursor.change(image.rgbaPixels,
3077 image.hotx, image.hoty,
3078 image.w, image.h
3079 );
3080 }
3081
3082 static genDES(password, challenge) {
3083 const passwordChars = password.split('').map(c => c.charCodeAt(0));
3084 const key = legacyCrypto.importKey(
3085 "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
3086 return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
3087 }
3088}
3089
3090// Class Methods
3091RFB.messages = {
3092 keyEvent(sock, keysym, down) {
3093 sock.sQpush8(4); // msg-type
3094 sock.sQpush8(down);
3095
3096 sock.sQpush16(0);
3097
3098 sock.sQpush32(keysym);
3099
3100 sock.flush();
3101 },
3102
3103 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
3104 function getRFBkeycode(xtScanCode) {
3105 const upperByte = (keycode >> 8);
3106 const lowerByte = (keycode & 0x00ff);
3107 if (upperByte === 0xe0 && lowerByte < 0x7f) {
3108 return lowerByte | 0x80;
3109 }
3110 return xtScanCode;
3111 }
3112
3113 sock.sQpush8(255); // msg-type
3114 sock.sQpush8(0); // sub msg-type
3115
3116 sock.sQpush16(down);
3117
3118 sock.sQpush32(keysym);
3119
3120 const RFBkeycode = getRFBkeycode(keycode);
3121
3122 sock.sQpush32(RFBkeycode);
3123
3124 sock.flush();
3125 },
3126
3127 pointerEvent(sock, x, y, mask) {
3128 sock.sQpush8(5); // msg-type
3129
3130 // Marker bit must be set to 0, otherwise the server might
3131 // confuse the marker bit with the highest bit in a normal
3132 // PointerEvent message.
3133 mask = mask & 0x7f;
3134 sock.sQpush8(mask);
3135
3136 sock.sQpush16(x);
3137 sock.sQpush16(y);
3138
3139 sock.flush();
3140 },
3141
3142 extendedPointerEvent(sock, x, y, mask) {
3143 sock.sQpush8(5); // msg-type
3144
3145 let higherBits = (mask >> 7) & 0xff;
3146
3147 // Bits 2-7 are reserved
3148 if (higherBits & 0xfc) {
3149 throw new Error("Invalid mouse button mask: " + mask);
3150 }
3151
3152 let lowerBits = mask & 0x7f;
3153 lowerBits |= 0x80; // Set marker bit to 1
3154
3155 sock.sQpush8(lowerBits);
3156 sock.sQpush16(x);
3157 sock.sQpush16(y);
3158 sock.sQpush8(higherBits);
3159
3160 sock.flush();
3161 },
3162
3163 // Used to build Notify and Request data.
3164 _buildExtendedClipboardFlags(actions, formats) {
3165 let data = new Uint8Array(4);
3166 let formatFlag = 0x00000000;
3167 let actionFlag = 0x00000000;
3168
3169 for (let i = 0; i < actions.length; i++) {
3170 actionFlag |= actions[i];
3171 }
3172
3173 for (let i = 0; i < formats.length; i++) {
3174 formatFlag |= formats[i];
3175 }
3176
3177 data[0] = actionFlag >> 24; // Actions
3178 data[1] = 0x00; // Reserved
3179 data[2] = 0x00; // Reserved
3180 data[3] = formatFlag; // Formats
3181
3182 return data;
3183 },
3184
3185 extendedClipboardProvide(sock, formats, inData) {
3186 // Deflate incomming data and their sizes
3187 let deflator = new Deflator();
3188 let dataToDeflate = [];
3189
3190 for (let i = 0; i < formats.length; i++) {
3191 // We only support the format Text at this time
3192 if (formats[i] != extendedClipboardFormatText) {
3193 throw new Error("Unsupported extended clipboard format for Provide message.");
3194 }
3195
3196 // Change lone \r or \n into \r\n as defined in rfbproto
3197 inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
3198
3199 // Check if it already has \0
3200 let text = encodeUTF8(inData[i] + "\0");
3201
3202 dataToDeflate.push( (text.length >> 24) & 0xFF,
3203 (text.length >> 16) & 0xFF,
3204 (text.length >> 8) & 0xFF,
3205 (text.length & 0xFF));
3206
3207 for (let j = 0; j < text.length; j++) {
3208 dataToDeflate.push(text.charCodeAt(j));
3209 }
3210 }
3211
3212 let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
3213
3214 // Build data to send
3215 let data = new Uint8Array(4 + deflatedData.length);
3216 data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
3217 formats));
3218 data.set(deflatedData, 4);
3219
3220 RFB.messages.clientCutText(sock, data, true);
3221 },
3222
3223 extendedClipboardNotify(sock, formats) {
3224 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
3225 formats);
3226 RFB.messages.clientCutText(sock, flags, true);
3227 },
3228
3229 extendedClipboardRequest(sock, formats) {
3230 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
3231 formats);
3232 RFB.messages.clientCutText(sock, flags, true);
3233 },
3234
3235 extendedClipboardCaps(sock, actions, formats) {
3236 let formatKeys = Object.keys(formats);
3237 let data = new Uint8Array(4 + (4 * formatKeys.length));
3238
3239 formatKeys.map(x => parseInt(x));
3240 formatKeys.sort((a, b) => a - b);
3241
3242 data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
3243
3244 let loopOffset = 4;
3245 for (let i = 0; i < formatKeys.length; i++) {
3246 data[loopOffset] = formats[formatKeys[i]] >> 24;
3247 data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
3248 data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
3249 data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
3250
3251 loopOffset += 4;
3252 data[3] |= (1 << formatKeys[i]); // Update our format flags
3253 }
3254
3255 RFB.messages.clientCutText(sock, data, true);
3256 },
3257
3258 clientCutText(sock, data, extended = false) {
3259 sock.sQpush8(6); // msg-type
3260
3261 sock.sQpush8(0); // padding
3262 sock.sQpush8(0); // padding
3263 sock.sQpush8(0); // padding
3264
3265 let length;
3266 if (extended) {
3267 length = toUnsigned32bit(-data.length);
3268 } else {
3269 length = data.length;
3270 }
3271
3272 sock.sQpush32(length);
3273 sock.sQpushBytes(data);
3274 sock.flush();
3275 },
3276
3277 setDesktopSize(sock, width, height, id, flags) {
3278 sock.sQpush8(251); // msg-type
3279
3280 sock.sQpush8(0); // padding
3281
3282 sock.sQpush16(width);
3283 sock.sQpush16(height);
3284
3285 sock.sQpush8(1); // number-of-screens
3286
3287 sock.sQpush8(0); // padding
3288
3289 // screen array
3290 sock.sQpush32(id);
3291 sock.sQpush16(0); // x-position
3292 sock.sQpush16(0); // y-position
3293 sock.sQpush16(width);
3294 sock.sQpush16(height);
3295 sock.sQpush32(flags);
3296
3297 sock.flush();
3298 },
3299
3300 clientFence(sock, flags, payload) {
3301 sock.sQpush8(248); // msg-type
3302
3303 sock.sQpush8(0); // padding
3304 sock.sQpush8(0); // padding
3305 sock.sQpush8(0); // padding
3306
3307 sock.sQpush32(flags);
3308
3309 sock.sQpush8(payload.length);
3310 sock.sQpushString(payload);
3311
3312 sock.flush();
3313 },
3314
3315 enableContinuousUpdates(sock, enable, x, y, width, height) {
3316 sock.sQpush8(150); // msg-type
3317
3318 sock.sQpush8(enable);
3319
3320 sock.sQpush16(x);
3321 sock.sQpush16(y);
3322 sock.sQpush16(width);
3323 sock.sQpush16(height);
3324
3325 sock.flush();
3326 },
3327
3328 pixelFormat(sock, depth, trueColor) {
3329 let bpp;
3330
3331 if (depth > 16) {
3332 bpp = 32;
3333 } else if (depth > 8) {
3334 bpp = 16;
3335 } else {
3336 bpp = 8;
3337 }
3338
3339 const bits = Math.floor(depth/3);
3340
3341 sock.sQpush8(0); // msg-type
3342
3343 sock.sQpush8(0); // padding
3344 sock.sQpush8(0); // padding
3345 sock.sQpush8(0); // padding
3346
3347 sock.sQpush8(bpp);
3348 sock.sQpush8(depth);
3349 sock.sQpush8(0); // little-endian
3350 sock.sQpush8(trueColor ? 1 : 0);
3351
3352 sock.sQpush16((1 << bits) - 1); // red-max
3353 sock.sQpush16((1 << bits) - 1); // green-max
3354 sock.sQpush16((1 << bits) - 1); // blue-max
3355
3356 sock.sQpush8(bits * 0); // red-shift
3357 sock.sQpush8(bits * 1); // green-shift
3358 sock.sQpush8(bits * 2); // blue-shift
3359
3360 sock.sQpush8(0); // padding
3361 sock.sQpush8(0); // padding
3362 sock.sQpush8(0); // padding
3363
3364 sock.flush();
3365 },
3366
3367 clientEncodings(sock, encodings) {
3368 sock.sQpush8(2); // msg-type
3369
3370 sock.sQpush8(0); // padding
3371
3372 sock.sQpush16(encodings.length);
3373 for (let i = 0; i < encodings.length; i++) {
3374 sock.sQpush32(encodings[i]);
3375 }
3376
3377 sock.flush();
3378 },
3379
3380 fbUpdateRequest(sock, incremental, x, y, w, h) {
3381 if (typeof(x) === "undefined") { x = 0; }
3382 if (typeof(y) === "undefined") { y = 0; }
3383
3384 sock.sQpush8(3); // msg-type
3385
3386 sock.sQpush8(incremental ? 1 : 0);
3387
3388 sock.sQpush16(x);
3389 sock.sQpush16(y);
3390 sock.sQpush16(w);
3391 sock.sQpush16(h);
3392
3393 sock.flush();
3394 },
3395
3396 xvpOp(sock, ver, op) {
3397 sock.sQpush8(250); // msg-type
3398
3399 sock.sQpush8(0); // padding
3400
3401 sock.sQpush8(ver);
3402 sock.sQpush8(op);
3403
3404 sock.flush();
3405 }
3406};
3407
3408RFB.cursors = {
3409 none: {
3410 rgbaPixels: new Uint8Array(),
3411 w: 0, h: 0,
3412 hotx: 0, hoty: 0,
3413 },
3414
3415 dot: {
3416 /* eslint-disable indent */
3417 rgbaPixels: new Uint8Array([
3418 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
3419 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
3420 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
3421 ]),
3422 /* eslint-enable indent */
3423 w: 3, h: 3,
3424 hotx: 1, hoty: 1,
3425 }
3426};