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