]> git.proxmox.com Git - mirror_novnc.git/blob - core/rfb.js
Stop send mouse clicks while dragging in view only
[mirror_novnc.git] / core / rfb.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
10 import { toUnsigned32bit, toSigned32bit } from './util/int.js';
11 import * as Log from './util/logging.js';
12 import { encodeUTF8, decodeUTF8 } from './util/strings.js';
13 import { dragThreshold } from './util/browser.js';
14 import EventTargetMixin from './util/eventtarget.js';
15 import Display from "./display.js";
16 import Inflator from "./inflator.js";
17 import Deflator from "./deflator.js";
18 import Keyboard from "./input/keyboard.js";
19 import Mouse from "./input/mouse.js";
20 import Cursor from "./util/cursor.js";
21 import Websock from "./websock.js";
22 import DES from "./des.js";
23 import KeyTable from "./input/keysym.js";
24 import XtScancode from "./input/xtscancodes.js";
25 import { encodings } from "./encodings.js";
26 import "./util/polyfill.js";
27
28 import RawDecoder from "./decoders/raw.js";
29 import CopyRectDecoder from "./decoders/copyrect.js";
30 import RREDecoder from "./decoders/rre.js";
31 import HextileDecoder from "./decoders/hextile.js";
32 import TightDecoder from "./decoders/tight.js";
33 import TightPNGDecoder from "./decoders/tightpng.js";
34
35 // How many seconds to wait for a disconnect to finish
36 const DISCONNECT_TIMEOUT = 3;
37 const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
38
39 // Extended clipboard pseudo-encoding formats
40 const extendedClipboardFormatText = 1;
41 /*eslint-disable no-unused-vars */
42 const extendedClipboardFormatRtf = 1 << 1;
43 const extendedClipboardFormatHtml = 1 << 2;
44 const extendedClipboardFormatDib = 1 << 3;
45 const extendedClipboardFormatFiles = 1 << 4;
46 /*eslint-enable */
47
48 // Extended clipboard pseudo-encoding actions
49 const extendedClipboardActionCaps = 1 << 24;
50 const extendedClipboardActionRequest = 1 << 25;
51 const extendedClipboardActionPeek = 1 << 26;
52 const extendedClipboardActionNotify = 1 << 27;
53 const extendedClipboardActionProvide = 1 << 28;
54
55
56 export default class RFB extends EventTargetMixin {
57 constructor(target, url, options) {
58 if (!target) {
59 throw new Error("Must specify target");
60 }
61 if (!url) {
62 throw new Error("Must specify URL");
63 }
64
65 super();
66
67 this._target = target;
68 this._url = url;
69
70 // Connection details
71 options = options || {};
72 this._rfbCredentials = options.credentials || {};
73 this._shared = 'shared' in options ? !!options.shared : true;
74 this._repeaterID = options.repeaterID || '';
75 this._wsProtocols = options.wsProtocols || [];
76
77 // Internal state
78 this._rfbConnectionState = '';
79 this._rfbInitState = '';
80 this._rfbAuthScheme = -1;
81 this._rfbCleanDisconnect = true;
82
83 // Server capabilities
84 this._rfbVersion = 0;
85 this._rfbMaxVersion = 3.8;
86 this._rfbTightVNC = false;
87 this._rfbVeNCryptState = 0;
88 this._rfbXvpVer = 0;
89
90 this._fbWidth = 0;
91 this._fbHeight = 0;
92
93 this._fbName = "";
94
95 this._capabilities = { power: false };
96
97 this._supportsFence = false;
98
99 this._supportsContinuousUpdates = false;
100 this._enabledContinuousUpdates = false;
101
102 this._supportsSetDesktopSize = false;
103 this._screenID = 0;
104 this._screenFlags = 0;
105
106 this._qemuExtKeyEventSupported = false;
107
108 this._clipboardText = null;
109 this._clipboardServerCapabilitiesActions = {};
110 this._clipboardServerCapabilitiesFormats = {};
111
112 // Internal objects
113 this._sock = null; // Websock object
114 this._display = null; // Display object
115 this._flushing = false; // Display flushing state
116 this._keyboard = null; // Keyboard input handler object
117 this._mouse = null; // Mouse input handler object
118
119 // Timers
120 this._disconnTimer = null; // disconnection timer
121 this._resizeTimeout = null; // resize rate limiting
122
123 // Decoder states
124 this._decoders = {};
125
126 this._FBU = {
127 rects: 0,
128 x: 0,
129 y: 0,
130 width: 0,
131 height: 0,
132 encoding: null,
133 };
134
135 // Mouse state
136 this._mouseButtonMask = 0;
137 this._viewportDragging = false;
138 this._viewportDragPos = {};
139 this._viewportHasMoved = false;
140
141 // Bound event handlers
142 this._eventHandlers = {
143 focusCanvas: this._focusCanvas.bind(this),
144 windowResize: this._windowResize.bind(this),
145 };
146
147 // main setup
148 Log.Debug(">> RFB.constructor");
149
150 // Create DOM elements
151 this._screen = document.createElement('div');
152 this._screen.style.display = 'flex';
153 this._screen.style.width = '100%';
154 this._screen.style.height = '100%';
155 this._screen.style.overflow = 'auto';
156 this._screen.style.background = DEFAULT_BACKGROUND;
157 this._canvas = document.createElement('canvas');
158 this._canvas.style.margin = 'auto';
159 // Some browsers add an outline on focus
160 this._canvas.style.outline = 'none';
161 // IE miscalculates width without this :(
162 this._canvas.style.flexShrink = '0';
163 this._canvas.width = 0;
164 this._canvas.height = 0;
165 this._canvas.tabIndex = -1;
166 this._screen.appendChild(this._canvas);
167
168 // Cursor
169 this._cursor = new Cursor();
170
171 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
172 // it. Result: no cursor at all until a window border or an edit field
173 // is hit blindly. But there are also VNC servers that draw the cursor
174 // in the framebuffer and don't send the empty local cursor. There is
175 // no way to satisfy both sides.
176 //
177 // The spec is unclear on this "initial cursor" issue. Many other
178 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
179 // initial cursor instead.
180 this._cursorImage = RFB.cursors.none;
181
182 // populate decoder array with objects
183 this._decoders[encodings.encodingRaw] = new RawDecoder();
184 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
185 this._decoders[encodings.encodingRRE] = new RREDecoder();
186 this._decoders[encodings.encodingHextile] = new HextileDecoder();
187 this._decoders[encodings.encodingTight] = new TightDecoder();
188 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
189
190 // NB: nothing that needs explicit teardown should be done
191 // before this point, since this can throw an exception
192 try {
193 this._display = new Display(this._canvas);
194 } catch (exc) {
195 Log.Error("Display exception: " + exc);
196 throw exc;
197 }
198 this._display.onflush = this._onFlush.bind(this);
199
200 this._keyboard = new Keyboard(this._canvas);
201 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
202
203 this._mouse = new Mouse(this._canvas);
204 this._mouse.onmousebutton = this._handleMouseButton.bind(this);
205 this._mouse.onmousemove = this._handleMouseMove.bind(this);
206
207 this._sock = new Websock();
208 this._sock.on('message', () => {
209 this._handleMessage();
210 });
211 this._sock.on('open', () => {
212 if ((this._rfbConnectionState === 'connecting') &&
213 (this._rfbInitState === '')) {
214 this._rfbInitState = 'ProtocolVersion';
215 Log.Debug("Starting VNC handshake");
216 } else {
217 this._fail("Unexpected server connection while " +
218 this._rfbConnectionState);
219 }
220 });
221 this._sock.on('close', (e) => {
222 Log.Debug("WebSocket on-close event");
223 let msg = "";
224 if (e.code) {
225 msg = "(code: " + e.code;
226 if (e.reason) {
227 msg += ", reason: " + e.reason;
228 }
229 msg += ")";
230 }
231 switch (this._rfbConnectionState) {
232 case 'connecting':
233 this._fail("Connection closed " + msg);
234 break;
235 case 'connected':
236 // Handle disconnects that were initiated server-side
237 this._updateConnectionState('disconnecting');
238 this._updateConnectionState('disconnected');
239 break;
240 case 'disconnecting':
241 // Normal disconnection path
242 this._updateConnectionState('disconnected');
243 break;
244 case 'disconnected':
245 this._fail("Unexpected server disconnect " +
246 "when already disconnected " + msg);
247 break;
248 default:
249 this._fail("Unexpected server disconnect before connecting " +
250 msg);
251 break;
252 }
253 this._sock.off('close');
254 });
255 this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
256
257 // Slight delay of the actual connection so that the caller has
258 // time to set up callbacks
259 setTimeout(this._updateConnectionState.bind(this, 'connecting'));
260
261 Log.Debug("<< RFB.constructor");
262
263 // ===== PROPERTIES =====
264
265 this.dragViewport = false;
266 this.focusOnClick = true;
267
268 this._viewOnly = false;
269 this._clipViewport = false;
270 this._scaleViewport = false;
271 this._resizeSession = false;
272
273 this._showDotCursor = false;
274 if (options.showDotCursor !== undefined) {
275 Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
276 this._showDotCursor = options.showDotCursor;
277 }
278
279 this._qualityLevel = 6;
280 this._compressionLevel = 2;
281 }
282
283 // ===== PROPERTIES =====
284
285 get viewOnly() { return this._viewOnly; }
286 set viewOnly(viewOnly) {
287 this._viewOnly = viewOnly;
288
289 if (this._rfbConnectionState === "connecting" ||
290 this._rfbConnectionState === "connected") {
291 if (viewOnly) {
292 this._keyboard.ungrab();
293 this._mouse.ungrab();
294 } else {
295 this._keyboard.grab();
296 this._mouse.grab();
297 }
298 }
299 }
300
301 get capabilities() { return this._capabilities; }
302
303 get touchButton() { return this._mouse.touchButton; }
304 set touchButton(button) { this._mouse.touchButton = button; }
305
306 get clipViewport() { return this._clipViewport; }
307 set clipViewport(viewport) {
308 this._clipViewport = viewport;
309 this._updateClip();
310 }
311
312 get scaleViewport() { return this._scaleViewport; }
313 set scaleViewport(scale) {
314 this._scaleViewport = scale;
315 // Scaling trumps clipping, so we may need to adjust
316 // clipping when enabling or disabling scaling
317 if (scale && this._clipViewport) {
318 this._updateClip();
319 }
320 this._updateScale();
321 if (!scale && this._clipViewport) {
322 this._updateClip();
323 }
324 }
325
326 get resizeSession() { return this._resizeSession; }
327 set resizeSession(resize) {
328 this._resizeSession = resize;
329 if (resize) {
330 this._requestRemoteResize();
331 }
332 }
333
334 get showDotCursor() { return this._showDotCursor; }
335 set showDotCursor(show) {
336 this._showDotCursor = show;
337 this._refreshCursor();
338 }
339
340 get background() { return this._screen.style.background; }
341 set background(cssValue) { this._screen.style.background = cssValue; }
342
343 get qualityLevel() {
344 return this._qualityLevel;
345 }
346 set qualityLevel(qualityLevel) {
347 if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
348 Log.Error("qualityLevel must be an integer between 0 and 9");
349 return;
350 }
351
352 if (this._qualityLevel === qualityLevel) {
353 return;
354 }
355
356 this._qualityLevel = qualityLevel;
357
358 if (this._rfbConnectionState === 'connected') {
359 this._sendEncodings();
360 }
361 }
362
363 get compressionLevel() {
364 return this._compressionLevel;
365 }
366 set compressionLevel(compressionLevel) {
367 if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
368 Log.Error("compressionLevel must be an integer between 0 and 9");
369 return;
370 }
371
372 if (this._compressionLevel === compressionLevel) {
373 return;
374 }
375
376 this._compressionLevel = compressionLevel;
377
378 if (this._rfbConnectionState === 'connected') {
379 this._sendEncodings();
380 }
381 }
382
383 // ===== PUBLIC METHODS =====
384
385 disconnect() {
386 this._updateConnectionState('disconnecting');
387 this._sock.off('error');
388 this._sock.off('message');
389 this._sock.off('open');
390 }
391
392 sendCredentials(creds) {
393 this._rfbCredentials = creds;
394 setTimeout(this._initMsg.bind(this), 0);
395 }
396
397 sendCtrlAltDel() {
398 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
399 Log.Info("Sending Ctrl-Alt-Del");
400
401 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
402 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
403 this.sendKey(KeyTable.XK_Delete, "Delete", true);
404 this.sendKey(KeyTable.XK_Delete, "Delete", false);
405 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
406 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
407 }
408
409 machineShutdown() {
410 this._xvpOp(1, 2);
411 }
412
413 machineReboot() {
414 this._xvpOp(1, 3);
415 }
416
417 machineReset() {
418 this._xvpOp(1, 4);
419 }
420
421 // Send a key press. If 'down' is not specified then send a down key
422 // followed by an up key.
423 sendKey(keysym, code, down) {
424 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
425
426 if (down === undefined) {
427 this.sendKey(keysym, code, true);
428 this.sendKey(keysym, code, false);
429 return;
430 }
431
432 const scancode = XtScancode[code];
433
434 if (this._qemuExtKeyEventSupported && scancode) {
435 // 0 is NoSymbol
436 keysym = keysym || 0;
437
438 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
439
440 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
441 } else {
442 if (!keysym) {
443 return;
444 }
445 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
446 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
447 }
448 }
449
450 focus() {
451 this._canvas.focus();
452 }
453
454 blur() {
455 this._canvas.blur();
456 }
457
458 clipboardPasteFrom(text) {
459 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
460
461 if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
462 this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
463
464 this._clipboardText = text;
465 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
466 } else {
467 let data = new Uint8Array(text.length);
468 for (let i = 0; i < text.length; i++) {
469 // FIXME: text can have values outside of Latin1/Uint8
470 data[i] = text.charCodeAt(i);
471 }
472
473 RFB.messages.clientCutText(this._sock, data);
474 }
475 }
476
477 // ===== PRIVATE METHODS =====
478
479 _connect() {
480 Log.Debug(">> RFB.connect");
481
482 Log.Info("connecting to " + this._url);
483
484 try {
485 // WebSocket.onopen transitions to the RFB init states
486 this._sock.open(this._url, this._wsProtocols);
487 } catch (e) {
488 if (e.name === 'SyntaxError') {
489 this._fail("Invalid host or port (" + e + ")");
490 } else {
491 this._fail("Error when opening socket (" + e + ")");
492 }
493 }
494
495 // Make our elements part of the page
496 this._target.appendChild(this._screen);
497
498 this._cursor.attach(this._canvas);
499 this._refreshCursor();
500
501 // Monitor size changes of the screen
502 // FIXME: Use ResizeObserver, or hidden overflow
503 window.addEventListener('resize', this._eventHandlers.windowResize);
504
505 // Always grab focus on some kind of click event
506 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
507 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
508
509 Log.Debug("<< RFB.connect");
510 }
511
512 _disconnect() {
513 Log.Debug(">> RFB.disconnect");
514 this._cursor.detach();
515 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
516 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
517 window.removeEventListener('resize', this._eventHandlers.windowResize);
518 this._keyboard.ungrab();
519 this._mouse.ungrab();
520 this._sock.close();
521 try {
522 this._target.removeChild(this._screen);
523 } catch (e) {
524 if (e.name === 'NotFoundError') {
525 // Some cases where the initial connection fails
526 // can disconnect before the _screen is created
527 } else {
528 throw e;
529 }
530 }
531 clearTimeout(this._resizeTimeout);
532 Log.Debug("<< RFB.disconnect");
533 }
534
535 _focusCanvas(event) {
536 // Respect earlier handlers' request to not do side-effects
537 if (event.defaultPrevented) {
538 return;
539 }
540
541 if (!this.focusOnClick) {
542 return;
543 }
544
545 this.focus();
546 }
547
548 _setDesktopName(name) {
549 this._fbName = name;
550 this.dispatchEvent(new CustomEvent(
551 "desktopname",
552 { detail: { name: this._fbName } }));
553 }
554
555 _windowResize(event) {
556 // If the window resized then our screen element might have
557 // as well. Update the viewport dimensions.
558 window.requestAnimationFrame(() => {
559 this._updateClip();
560 this._updateScale();
561 });
562
563 if (this._resizeSession) {
564 // Request changing the resolution of the remote display to
565 // the size of the local browser viewport.
566
567 // In order to not send multiple requests before the browser-resize
568 // is finished we wait 0.5 seconds before sending the request.
569 clearTimeout(this._resizeTimeout);
570 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
571 }
572 }
573
574 // Update state of clipping in Display object, and make sure the
575 // configured viewport matches the current screen size
576 _updateClip() {
577 const curClip = this._display.clipViewport;
578 let newClip = this._clipViewport;
579
580 if (this._scaleViewport) {
581 // Disable viewport clipping if we are scaling
582 newClip = false;
583 }
584
585 if (curClip !== newClip) {
586 this._display.clipViewport = newClip;
587 }
588
589 if (newClip) {
590 // When clipping is enabled, the screen is limited to
591 // the size of the container.
592 const size = this._screenSize();
593 this._display.viewportChangeSize(size.w, size.h);
594 this._fixScrollbars();
595 }
596 }
597
598 _updateScale() {
599 if (!this._scaleViewport) {
600 this._display.scale = 1.0;
601 } else {
602 const size = this._screenSize();
603 this._display.autoscale(size.w, size.h);
604 }
605 this._fixScrollbars();
606 }
607
608 // Requests a change of remote desktop size. This message is an extension
609 // and may only be sent if we have received an ExtendedDesktopSize message
610 _requestRemoteResize() {
611 clearTimeout(this._resizeTimeout);
612 this._resizeTimeout = null;
613
614 if (!this._resizeSession || this._viewOnly ||
615 !this._supportsSetDesktopSize) {
616 return;
617 }
618
619 const size = this._screenSize();
620 RFB.messages.setDesktopSize(this._sock,
621 Math.floor(size.w), Math.floor(size.h),
622 this._screenID, this._screenFlags);
623
624 Log.Debug('Requested new desktop size: ' +
625 size.w + 'x' + size.h);
626 }
627
628 // Gets the the size of the available screen
629 _screenSize() {
630 let r = this._screen.getBoundingClientRect();
631 return { w: r.width, h: r.height };
632 }
633
634 _fixScrollbars() {
635 // This is a hack because Chrome screws up the calculation
636 // for when scrollbars are needed. So to fix it we temporarily
637 // toggle them off and on.
638 const orig = this._screen.style.overflow;
639 this._screen.style.overflow = 'hidden';
640 // Force Chrome to recalculate the layout by asking for
641 // an element's dimensions
642 this._screen.getBoundingClientRect();
643 this._screen.style.overflow = orig;
644 }
645
646 /*
647 * Connection states:
648 * connecting
649 * connected
650 * disconnecting
651 * disconnected - permanent state
652 */
653 _updateConnectionState(state) {
654 const oldstate = this._rfbConnectionState;
655
656 if (state === oldstate) {
657 Log.Debug("Already in state '" + state + "', ignoring");
658 return;
659 }
660
661 // The 'disconnected' state is permanent for each RFB object
662 if (oldstate === 'disconnected') {
663 Log.Error("Tried changing state of a disconnected RFB object");
664 return;
665 }
666
667 // Ensure proper transitions before doing anything
668 switch (state) {
669 case 'connected':
670 if (oldstate !== 'connecting') {
671 Log.Error("Bad transition to connected state, " +
672 "previous connection state: " + oldstate);
673 return;
674 }
675 break;
676
677 case 'disconnected':
678 if (oldstate !== 'disconnecting') {
679 Log.Error("Bad transition to disconnected state, " +
680 "previous connection state: " + oldstate);
681 return;
682 }
683 break;
684
685 case 'connecting':
686 if (oldstate !== '') {
687 Log.Error("Bad transition to connecting state, " +
688 "previous connection state: " + oldstate);
689 return;
690 }
691 break;
692
693 case 'disconnecting':
694 if (oldstate !== 'connected' && oldstate !== 'connecting') {
695 Log.Error("Bad transition to disconnecting state, " +
696 "previous connection state: " + oldstate);
697 return;
698 }
699 break;
700
701 default:
702 Log.Error("Unknown connection state: " + state);
703 return;
704 }
705
706 // State change actions
707
708 this._rfbConnectionState = state;
709
710 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
711
712 if (this._disconnTimer && state !== 'disconnecting') {
713 Log.Debug("Clearing disconnect timer");
714 clearTimeout(this._disconnTimer);
715 this._disconnTimer = null;
716
717 // make sure we don't get a double event
718 this._sock.off('close');
719 }
720
721 switch (state) {
722 case 'connecting':
723 this._connect();
724 break;
725
726 case 'connected':
727 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
728 break;
729
730 case 'disconnecting':
731 this._disconnect();
732
733 this._disconnTimer = setTimeout(() => {
734 Log.Error("Disconnection timed out.");
735 this._updateConnectionState('disconnected');
736 }, DISCONNECT_TIMEOUT * 1000);
737 break;
738
739 case 'disconnected':
740 this.dispatchEvent(new CustomEvent(
741 "disconnect", { detail:
742 { clean: this._rfbCleanDisconnect } }));
743 break;
744 }
745 }
746
747 /* Print errors and disconnect
748 *
749 * The parameter 'details' is used for information that
750 * should be logged but not sent to the user interface.
751 */
752 _fail(details) {
753 switch (this._rfbConnectionState) {
754 case 'disconnecting':
755 Log.Error("Failed when disconnecting: " + details);
756 break;
757 case 'connected':
758 Log.Error("Failed while connected: " + details);
759 break;
760 case 'connecting':
761 Log.Error("Failed when connecting: " + details);
762 break;
763 default:
764 Log.Error("RFB failure: " + details);
765 break;
766 }
767 this._rfbCleanDisconnect = false; //This is sent to the UI
768
769 // Transition to disconnected without waiting for socket to close
770 this._updateConnectionState('disconnecting');
771 this._updateConnectionState('disconnected');
772
773 return false;
774 }
775
776 _setCapability(cap, val) {
777 this._capabilities[cap] = val;
778 this.dispatchEvent(new CustomEvent("capabilities",
779 { detail: { capabilities: this._capabilities } }));
780 }
781
782 _handleMessage() {
783 if (this._sock.rQlen === 0) {
784 Log.Warn("handleMessage called on an empty receive queue");
785 return;
786 }
787
788 switch (this._rfbConnectionState) {
789 case 'disconnected':
790 Log.Error("Got data while disconnected");
791 break;
792 case 'connected':
793 while (true) {
794 if (this._flushing) {
795 break;
796 }
797 if (!this._normalMsg()) {
798 break;
799 }
800 if (this._sock.rQlen === 0) {
801 break;
802 }
803 }
804 break;
805 default:
806 this._initMsg();
807 break;
808 }
809 }
810
811 _handleKeyEvent(keysym, code, down) {
812 this.sendKey(keysym, code, down);
813 }
814
815 _handleMouseButton(x, y, down, bmask) {
816 if (down) {
817 this._mouseButtonMask |= bmask;
818 } else {
819 this._mouseButtonMask &= ~bmask;
820 }
821
822 if (this.dragViewport) {
823 if (down && !this._viewportDragging) {
824 this._viewportDragging = true;
825 this._viewportDragPos = {'x': x, 'y': y};
826 this._viewportHasMoved = false;
827
828 // Skip sending mouse events
829 return;
830 } else {
831 this._viewportDragging = false;
832
833 // If we actually performed a drag then we are done
834 // here and should not send any mouse events
835 if (this._viewportHasMoved) {
836 return;
837 }
838
839 if (this._viewOnly) { return; }
840
841 // Otherwise we treat this as a mouse click event.
842 // Send the button down event here, as the button up
843 // event is sent at the end of this function.
844 RFB.messages.pointerEvent(this._sock,
845 this._display.absX(x),
846 this._display.absY(y),
847 bmask);
848 }
849 }
850
851 if (this._viewOnly) { return; } // View only, skip mouse events
852
853 if (this._rfbConnectionState !== 'connected') { return; }
854 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouseButtonMask);
855 }
856
857 _handleMouseMove(x, y) {
858 if (this._viewportDragging) {
859 const deltaX = this._viewportDragPos.x - x;
860 const deltaY = this._viewportDragPos.y - y;
861
862 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
863 Math.abs(deltaY) > dragThreshold)) {
864 this._viewportHasMoved = true;
865
866 this._viewportDragPos = {'x': x, 'y': y};
867 this._display.viewportChangePos(deltaX, deltaY);
868 }
869
870 // Skip sending mouse events
871 return;
872 }
873
874 if (this._viewOnly) { return; } // View only, skip mouse events
875
876 if (this._rfbConnectionState !== 'connected') { return; }
877 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouseButtonMask);
878 }
879
880 // Message Handlers
881
882 _negotiateProtocolVersion() {
883 if (this._sock.rQwait("version", 12)) {
884 return false;
885 }
886
887 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
888 Log.Info("Server ProtocolVersion: " + sversion);
889 let isRepeater = 0;
890 switch (sversion) {
891 case "000.000": // UltraVNC repeater
892 isRepeater = 1;
893 break;
894 case "003.003":
895 case "003.006": // UltraVNC
896 case "003.889": // Apple Remote Desktop
897 this._rfbVersion = 3.3;
898 break;
899 case "003.007":
900 this._rfbVersion = 3.7;
901 break;
902 case "003.008":
903 case "004.000": // Intel AMT KVM
904 case "004.001": // RealVNC 4.6
905 case "005.000": // RealVNC 5.3
906 this._rfbVersion = 3.8;
907 break;
908 default:
909 return this._fail("Invalid server version " + sversion);
910 }
911
912 if (isRepeater) {
913 let repeaterID = "ID:" + this._repeaterID;
914 while (repeaterID.length < 250) {
915 repeaterID += "\0";
916 }
917 this._sock.send_string(repeaterID);
918 return true;
919 }
920
921 if (this._rfbVersion > this._rfbMaxVersion) {
922 this._rfbVersion = this._rfbMaxVersion;
923 }
924
925 const cversion = "00" + parseInt(this._rfbVersion, 10) +
926 ".00" + ((this._rfbVersion * 10) % 10);
927 this._sock.send_string("RFB " + cversion + "\n");
928 Log.Debug('Sent ProtocolVersion: ' + cversion);
929
930 this._rfbInitState = 'Security';
931 }
932
933 _negotiateSecurity() {
934 // Polyfill since IE and PhantomJS doesn't have
935 // TypedArray.includes()
936 function includes(item, array) {
937 for (let i = 0; i < array.length; i++) {
938 if (array[i] === item) {
939 return true;
940 }
941 }
942 return false;
943 }
944
945 if (this._rfbVersion >= 3.7) {
946 // Server sends supported list, client decides
947 const numTypes = this._sock.rQshift8();
948 if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
949
950 if (numTypes === 0) {
951 this._rfbInitState = "SecurityReason";
952 this._securityContext = "no security types";
953 this._securityStatus = 1;
954 return this._initMsg();
955 }
956
957 const types = this._sock.rQshiftBytes(numTypes);
958 Log.Debug("Server security types: " + types);
959
960 // Look for each auth in preferred order
961 if (includes(1, types)) {
962 this._rfbAuthScheme = 1; // None
963 } else if (includes(22, types)) {
964 this._rfbAuthScheme = 22; // XVP
965 } else if (includes(16, types)) {
966 this._rfbAuthScheme = 16; // Tight
967 } else if (includes(2, types)) {
968 this._rfbAuthScheme = 2; // VNC Auth
969 } else if (includes(19, types)) {
970 this._rfbAuthScheme = 19; // VeNCrypt Auth
971 } else {
972 return this._fail("Unsupported security types (types: " + types + ")");
973 }
974
975 this._sock.send([this._rfbAuthScheme]);
976 } else {
977 // Server decides
978 if (this._sock.rQwait("security scheme", 4)) { return false; }
979 this._rfbAuthScheme = this._sock.rQshift32();
980
981 if (this._rfbAuthScheme == 0) {
982 this._rfbInitState = "SecurityReason";
983 this._securityContext = "authentication scheme";
984 this._securityStatus = 1;
985 return this._initMsg();
986 }
987 }
988
989 this._rfbInitState = 'Authentication';
990 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
991
992 return this._initMsg(); // jump to authentication
993 }
994
995 _handleSecurityReason() {
996 if (this._sock.rQwait("reason length", 4)) {
997 return false;
998 }
999 const strlen = this._sock.rQshift32();
1000 let reason = "";
1001
1002 if (strlen > 0) {
1003 if (this._sock.rQwait("reason", strlen, 4)) { return false; }
1004 reason = this._sock.rQshiftStr(strlen);
1005 }
1006
1007 if (reason !== "") {
1008 this.dispatchEvent(new CustomEvent(
1009 "securityfailure",
1010 { detail: { status: this._securityStatus,
1011 reason: reason } }));
1012
1013 return this._fail("Security negotiation failed on " +
1014 this._securityContext +
1015 " (reason: " + reason + ")");
1016 } else {
1017 this.dispatchEvent(new CustomEvent(
1018 "securityfailure",
1019 { detail: { status: this._securityStatus } }));
1020
1021 return this._fail("Security negotiation failed on " +
1022 this._securityContext);
1023 }
1024 }
1025
1026 // authentication
1027 _negotiateXvpAuth() {
1028 if (this._rfbCredentials.username === undefined ||
1029 this._rfbCredentials.password === undefined ||
1030 this._rfbCredentials.target === undefined) {
1031 this.dispatchEvent(new CustomEvent(
1032 "credentialsrequired",
1033 { detail: { types: ["username", "password", "target"] } }));
1034 return false;
1035 }
1036
1037 const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
1038 String.fromCharCode(this._rfbCredentials.target.length) +
1039 this._rfbCredentials.username +
1040 this._rfbCredentials.target;
1041 this._sock.send_string(xvpAuthStr);
1042 this._rfbAuthScheme = 2;
1043 return this._negotiateAuthentication();
1044 }
1045
1046 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1047 _negotiateVeNCryptAuth() {
1048
1049 // waiting for VeNCrypt version
1050 if (this._rfbVeNCryptState == 0) {
1051 if (this._sock.rQwait("vencrypt version", 2)) { return false; }
1052
1053 const major = this._sock.rQshift8();
1054 const minor = this._sock.rQshift8();
1055
1056 if (!(major == 0 && minor == 2)) {
1057 return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
1058 }
1059
1060 this._sock.send([0, 2]);
1061 this._rfbVeNCryptState = 1;
1062 }
1063
1064 // waiting for ACK
1065 if (this._rfbVeNCryptState == 1) {
1066 if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
1067
1068 const res = this._sock.rQshift8();
1069
1070 if (res != 0) {
1071 return this._fail("VeNCrypt failure " + res);
1072 }
1073
1074 this._rfbVeNCryptState = 2;
1075 }
1076 // must fall through here (i.e. no "else if"), beacause we may have already received
1077 // the subtypes length and won't be called again
1078
1079 if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
1080 if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
1081
1082 const subtypesLength = this._sock.rQshift8();
1083 if (subtypesLength < 1) {
1084 return this._fail("VeNCrypt subtypes empty");
1085 }
1086
1087 this._rfbVeNCryptSubtypesLength = subtypesLength;
1088 this._rfbVeNCryptState = 3;
1089 }
1090
1091 // waiting for subtypes list
1092 if (this._rfbVeNCryptState == 3) {
1093 if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
1094
1095 const subtypes = [];
1096 for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
1097 subtypes.push(this._sock.rQshift32());
1098 }
1099
1100 // 256 = Plain subtype
1101 if (subtypes.indexOf(256) != -1) {
1102 // 0x100 = 256
1103 this._sock.send([0, 0, 1, 0]);
1104 this._rfbVeNCryptState = 4;
1105 } else {
1106 return this._fail("VeNCrypt Plain subtype not offered by server");
1107 }
1108 }
1109
1110 // negotiated Plain subtype, server waits for password
1111 if (this._rfbVeNCryptState == 4) {
1112 if (!this._rfbCredentials.username ||
1113 !this._rfbCredentials.password) {
1114 this.dispatchEvent(new CustomEvent(
1115 "credentialsrequired",
1116 { detail: { types: ["username", "password"] } }));
1117 return false;
1118 }
1119
1120 const user = encodeUTF8(this._rfbCredentials.username);
1121 const pass = encodeUTF8(this._rfbCredentials.password);
1122
1123 // XXX we assume lengths are <= 255 (should not be an issue in the real world)
1124 this._sock.send([0, 0, 0, user.length]);
1125 this._sock.send([0, 0, 0, pass.length]);
1126 this._sock.send_string(user);
1127 this._sock.send_string(pass);
1128
1129 this._rfbInitState = "SecurityResult";
1130 return true;
1131 }
1132 }
1133
1134 _negotiateStdVNCAuth() {
1135 if (this._sock.rQwait("auth challenge", 16)) { return false; }
1136
1137 if (this._rfbCredentials.password === undefined) {
1138 this.dispatchEvent(new CustomEvent(
1139 "credentialsrequired",
1140 { detail: { types: ["password"] } }));
1141 return false;
1142 }
1143
1144 // TODO(directxman12): make genDES not require an Array
1145 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
1146 const response = RFB.genDES(this._rfbCredentials.password, challenge);
1147 this._sock.send(response);
1148 this._rfbInitState = "SecurityResult";
1149 return true;
1150 }
1151
1152 _negotiateTightUnixAuth() {
1153 if (this._rfbCredentials.username === undefined ||
1154 this._rfbCredentials.password === undefined) {
1155 this.dispatchEvent(new CustomEvent(
1156 "credentialsrequired",
1157 { detail: { types: ["username", "password"] } }));
1158 return false;
1159 }
1160
1161 this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
1162 this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
1163 this._sock.send_string(this._rfbCredentials.username);
1164 this._sock.send_string(this._rfbCredentials.password);
1165 this._rfbInitState = "SecurityResult";
1166 return true;
1167 }
1168
1169 _negotiateTightTunnels(numTunnels) {
1170 const clientSupportedTunnelTypes = {
1171 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
1172 };
1173 const serverSupportedTunnelTypes = {};
1174 // receive tunnel capabilities
1175 for (let i = 0; i < numTunnels; i++) {
1176 const capCode = this._sock.rQshift32();
1177 const capVendor = this._sock.rQshiftStr(4);
1178 const capSignature = this._sock.rQshiftStr(8);
1179 serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
1180 }
1181
1182 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
1183
1184 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1185 // but forgets to advertise it. Try to detect such servers by
1186 // looking for their custom tunnel type.
1187 if (serverSupportedTunnelTypes[1] &&
1188 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
1189 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
1190 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1191 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
1192 }
1193
1194 // choose the notunnel type
1195 if (serverSupportedTunnelTypes[0]) {
1196 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
1197 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
1198 return this._fail("Client's tunnel type had the incorrect " +
1199 "vendor or signature");
1200 }
1201 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1202 this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
1203 return false; // wait until we receive the sub auth count to continue
1204 } else {
1205 return this._fail("Server wanted tunnels, but doesn't support " +
1206 "the notunnel type");
1207 }
1208 }
1209
1210 _negotiateTightAuth() {
1211 if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
1212 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1213 const numTunnels = this._sock.rQshift32();
1214 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1215
1216 this._rfbTightVNC = true;
1217
1218 if (numTunnels > 0) {
1219 this._negotiateTightTunnels(numTunnels);
1220 return false; // wait until we receive the sub auth to continue
1221 }
1222 }
1223
1224 // second pass, do the sub-auth negotiation
1225 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1226 const subAuthCount = this._sock.rQshift32();
1227 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1228 this._rfbInitState = 'SecurityResult';
1229 return true;
1230 }
1231
1232 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1233
1234 const clientSupportedTypes = {
1235 'STDVNOAUTH__': 1,
1236 'STDVVNCAUTH_': 2,
1237 'TGHTULGNAUTH': 129
1238 };
1239
1240 const serverSupportedTypes = [];
1241
1242 for (let i = 0; i < subAuthCount; i++) {
1243 this._sock.rQshift32(); // capNum
1244 const capabilities = this._sock.rQshiftStr(12);
1245 serverSupportedTypes.push(capabilities);
1246 }
1247
1248 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1249
1250 for (let authType in clientSupportedTypes) {
1251 if (serverSupportedTypes.indexOf(authType) != -1) {
1252 this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
1253 Log.Debug("Selected authentication type: " + authType);
1254
1255 switch (authType) {
1256 case 'STDVNOAUTH__': // no auth
1257 this._rfbInitState = 'SecurityResult';
1258 return true;
1259 case 'STDVVNCAUTH_': // VNC auth
1260 this._rfbAuthScheme = 2;
1261 return this._initMsg();
1262 case 'TGHTULGNAUTH': // UNIX auth
1263 this._rfbAuthScheme = 129;
1264 return this._initMsg();
1265 default:
1266 return this._fail("Unsupported tiny auth scheme " +
1267 "(scheme: " + authType + ")");
1268 }
1269 }
1270 }
1271
1272 return this._fail("No supported sub-auth types!");
1273 }
1274
1275 _negotiateAuthentication() {
1276 switch (this._rfbAuthScheme) {
1277 case 1: // no auth
1278 if (this._rfbVersion >= 3.8) {
1279 this._rfbInitState = 'SecurityResult';
1280 return true;
1281 }
1282 this._rfbInitState = 'ClientInitialisation';
1283 return this._initMsg();
1284
1285 case 22: // XVP auth
1286 return this._negotiateXvpAuth();
1287
1288 case 2: // VNC authentication
1289 return this._negotiateStdVNCAuth();
1290
1291 case 16: // TightVNC Security Type
1292 return this._negotiateTightAuth();
1293
1294 case 19: // VeNCrypt Security Type
1295 return this._negotiateVeNCryptAuth();
1296
1297 case 129: // TightVNC UNIX Security Type
1298 return this._negotiateTightUnixAuth();
1299
1300 default:
1301 return this._fail("Unsupported auth scheme (scheme: " +
1302 this._rfbAuthScheme + ")");
1303 }
1304 }
1305
1306 _handleSecurityResult() {
1307 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
1308
1309 const status = this._sock.rQshift32();
1310
1311 if (status === 0) { // OK
1312 this._rfbInitState = 'ClientInitialisation';
1313 Log.Debug('Authentication OK');
1314 return this._initMsg();
1315 } else {
1316 if (this._rfbVersion >= 3.8) {
1317 this._rfbInitState = "SecurityReason";
1318 this._securityContext = "security result";
1319 this._securityStatus = status;
1320 return this._initMsg();
1321 } else {
1322 this.dispatchEvent(new CustomEvent(
1323 "securityfailure",
1324 { detail: { status: status } }));
1325
1326 return this._fail("Security handshake failed");
1327 }
1328 }
1329 }
1330
1331 _negotiateServerInit() {
1332 if (this._sock.rQwait("server initialization", 24)) { return false; }
1333
1334 /* Screen size */
1335 const width = this._sock.rQshift16();
1336 const height = this._sock.rQshift16();
1337
1338 /* PIXEL_FORMAT */
1339 const bpp = this._sock.rQshift8();
1340 const depth = this._sock.rQshift8();
1341 const bigEndian = this._sock.rQshift8();
1342 const trueColor = this._sock.rQshift8();
1343
1344 const redMax = this._sock.rQshift16();
1345 const greenMax = this._sock.rQshift16();
1346 const blueMax = this._sock.rQshift16();
1347 const redShift = this._sock.rQshift8();
1348 const greenShift = this._sock.rQshift8();
1349 const blueShift = this._sock.rQshift8();
1350 this._sock.rQskipBytes(3); // padding
1351
1352 // NB(directxman12): we don't want to call any callbacks or print messages until
1353 // *after* we're past the point where we could backtrack
1354
1355 /* Connection name/title */
1356 const nameLength = this._sock.rQshift32();
1357 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1358 let name = this._sock.rQshiftStr(nameLength);
1359 name = decodeUTF8(name, true);
1360
1361 if (this._rfbTightVNC) {
1362 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1363 // In TightVNC mode, ServerInit message is extended
1364 const numServerMessages = this._sock.rQshift16();
1365 const numClientMessages = this._sock.rQshift16();
1366 const numEncodings = this._sock.rQshift16();
1367 this._sock.rQskipBytes(2); // padding
1368
1369 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1370 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1371
1372 // we don't actually do anything with the capability information that TIGHT sends,
1373 // so we just skip the all of this.
1374
1375 // TIGHT server message capabilities
1376 this._sock.rQskipBytes(16 * numServerMessages);
1377
1378 // TIGHT client message capabilities
1379 this._sock.rQskipBytes(16 * numClientMessages);
1380
1381 // TIGHT encoding capabilities
1382 this._sock.rQskipBytes(16 * numEncodings);
1383 }
1384
1385 // NB(directxman12): these are down here so that we don't run them multiple times
1386 // if we backtrack
1387 Log.Info("Screen: " + width + "x" + height +
1388 ", bpp: " + bpp + ", depth: " + depth +
1389 ", bigEndian: " + bigEndian +
1390 ", trueColor: " + trueColor +
1391 ", redMax: " + redMax +
1392 ", greenMax: " + greenMax +
1393 ", blueMax: " + blueMax +
1394 ", redShift: " + redShift +
1395 ", greenShift: " + greenShift +
1396 ", blueShift: " + blueShift);
1397
1398 // we're past the point where we could backtrack, so it's safe to call this
1399 this._setDesktopName(name);
1400 this._resize(width, height);
1401
1402 if (!this._viewOnly) { this._keyboard.grab(); }
1403 if (!this._viewOnly) { this._mouse.grab(); }
1404
1405 this._fbDepth = 24;
1406
1407 if (this._fbName === "Intel(r) AMT KVM") {
1408 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1409 this._fbDepth = 8;
1410 }
1411
1412 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1413 this._sendEncodings();
1414 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1415
1416 this._updateConnectionState('connected');
1417 return true;
1418 }
1419
1420 _sendEncodings() {
1421 const encs = [];
1422
1423 // In preference order
1424 encs.push(encodings.encodingCopyRect);
1425 // Only supported with full depth support
1426 if (this._fbDepth == 24) {
1427 encs.push(encodings.encodingTight);
1428 encs.push(encodings.encodingTightPNG);
1429 encs.push(encodings.encodingHextile);
1430 encs.push(encodings.encodingRRE);
1431 }
1432 encs.push(encodings.encodingRaw);
1433
1434 // Psuedo-encoding settings
1435 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1436 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1437
1438 encs.push(encodings.pseudoEncodingDesktopSize);
1439 encs.push(encodings.pseudoEncodingLastRect);
1440 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1441 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1442 encs.push(encodings.pseudoEncodingXvp);
1443 encs.push(encodings.pseudoEncodingFence);
1444 encs.push(encodings.pseudoEncodingContinuousUpdates);
1445 encs.push(encodings.pseudoEncodingDesktopName);
1446 encs.push(encodings.pseudoEncodingExtendedClipboard);
1447
1448 if (this._fbDepth == 24) {
1449 encs.push(encodings.pseudoEncodingVMwareCursor);
1450 encs.push(encodings.pseudoEncodingCursor);
1451 }
1452
1453 RFB.messages.clientEncodings(this._sock, encs);
1454 }
1455
1456 /* RFB protocol initialization states:
1457 * ProtocolVersion
1458 * Security
1459 * Authentication
1460 * SecurityResult
1461 * ClientInitialization - not triggered by server message
1462 * ServerInitialization
1463 */
1464 _initMsg() {
1465 switch (this._rfbInitState) {
1466 case 'ProtocolVersion':
1467 return this._negotiateProtocolVersion();
1468
1469 case 'Security':
1470 return this._negotiateSecurity();
1471
1472 case 'Authentication':
1473 return this._negotiateAuthentication();
1474
1475 case 'SecurityResult':
1476 return this._handleSecurityResult();
1477
1478 case 'SecurityReason':
1479 return this._handleSecurityReason();
1480
1481 case 'ClientInitialisation':
1482 this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
1483 this._rfbInitState = 'ServerInitialisation';
1484 return true;
1485
1486 case 'ServerInitialisation':
1487 return this._negotiateServerInit();
1488
1489 default:
1490 return this._fail("Unknown init state (state: " +
1491 this._rfbInitState + ")");
1492 }
1493 }
1494
1495 _handleSetColourMapMsg() {
1496 Log.Debug("SetColorMapEntries");
1497
1498 return this._fail("Unexpected SetColorMapEntries message");
1499 }
1500
1501 _handleServerCutText() {
1502 Log.Debug("ServerCutText");
1503
1504 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
1505
1506 this._sock.rQskipBytes(3); // Padding
1507
1508 let length = this._sock.rQshift32();
1509 length = toSigned32bit(length);
1510
1511 if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
1512
1513 if (length >= 0) {
1514 //Standard msg
1515 const text = this._sock.rQshiftStr(length);
1516 if (this._viewOnly) {
1517 return true;
1518 }
1519
1520 this.dispatchEvent(new CustomEvent(
1521 "clipboard",
1522 { detail: { text: text } }));
1523
1524 } else {
1525 //Extended msg.
1526 length = Math.abs(length);
1527 const flags = this._sock.rQshift32();
1528 let formats = flags & 0x0000FFFF;
1529 let actions = flags & 0xFF000000;
1530
1531 let isCaps = (!!(actions & extendedClipboardActionCaps));
1532 if (isCaps) {
1533 this._clipboardServerCapabilitiesFormats = {};
1534 this._clipboardServerCapabilitiesActions = {};
1535
1536 // Update our server capabilities for Formats
1537 for (let i = 0; i <= 15; i++) {
1538 let index = 1 << i;
1539
1540 // Check if format flag is set.
1541 if ((formats & index)) {
1542 this._clipboardServerCapabilitiesFormats[index] = true;
1543 // We don't send unsolicited clipboard, so we
1544 // ignore the size
1545 this._sock.rQshift32();
1546 }
1547 }
1548
1549 // Update our server capabilities for Actions
1550 for (let i = 24; i <= 31; i++) {
1551 let index = 1 << i;
1552 this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
1553 }
1554
1555 /* Caps handling done, send caps with the clients
1556 capabilities set as a response */
1557 let clientActions = [
1558 extendedClipboardActionCaps,
1559 extendedClipboardActionRequest,
1560 extendedClipboardActionPeek,
1561 extendedClipboardActionNotify,
1562 extendedClipboardActionProvide
1563 ];
1564 RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
1565
1566 } else if (actions === extendedClipboardActionRequest) {
1567 if (this._viewOnly) {
1568 return true;
1569 }
1570
1571 // Check if server has told us it can handle Provide and there is clipboard data to send.
1572 if (this._clipboardText != null &&
1573 this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
1574
1575 if (formats & extendedClipboardFormatText) {
1576 RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
1577 }
1578 }
1579
1580 } else if (actions === extendedClipboardActionPeek) {
1581 if (this._viewOnly) {
1582 return true;
1583 }
1584
1585 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
1586
1587 if (this._clipboardText != null) {
1588 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
1589 } else {
1590 RFB.messages.extendedClipboardNotify(this._sock, []);
1591 }
1592 }
1593
1594 } else if (actions === extendedClipboardActionNotify) {
1595 if (this._viewOnly) {
1596 return true;
1597 }
1598
1599 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
1600
1601 if (formats & extendedClipboardFormatText) {
1602 RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
1603 }
1604 }
1605
1606 } else if (actions === extendedClipboardActionProvide) {
1607 if (this._viewOnly) {
1608 return true;
1609 }
1610
1611 if (!(formats & extendedClipboardFormatText)) {
1612 return true;
1613 }
1614 // Ignore what we had in our clipboard client side.
1615 this._clipboardText = null;
1616
1617 // FIXME: Should probably verify that this data was actually requested
1618 let zlibStream = this._sock.rQshiftBytes(length - 4);
1619 let streamInflator = new Inflator();
1620 let textData = null;
1621
1622 streamInflator.setInput(zlibStream);
1623 for (let i = 0; i <= 15; i++) {
1624 let format = 1 << i;
1625
1626 if (formats & format) {
1627
1628 let size = 0x00;
1629 let sizeArray = streamInflator.inflate(4);
1630
1631 size |= (sizeArray[0] << 24);
1632 size |= (sizeArray[1] << 16);
1633 size |= (sizeArray[2] << 8);
1634 size |= (sizeArray[3]);
1635 let chunk = streamInflator.inflate(size);
1636
1637 if (format === extendedClipboardFormatText) {
1638 textData = chunk;
1639 }
1640 }
1641 }
1642 streamInflator.setInput(null);
1643
1644 if (textData !== null) {
1645 let tmpText = "";
1646 for (let i = 0; i < textData.length; i++) {
1647 tmpText += String.fromCharCode(textData[i]);
1648 }
1649 textData = tmpText;
1650
1651 textData = decodeUTF8(textData);
1652 if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
1653 textData = textData.slice(0, -1);
1654 }
1655
1656 textData = textData.replace("\r\n", "\n");
1657
1658 this.dispatchEvent(new CustomEvent(
1659 "clipboard",
1660 { detail: { text: textData } }));
1661 }
1662 } else {
1663 return this._fail("Unexpected action in extended clipboard message: " + actions);
1664 }
1665 }
1666 return true;
1667 }
1668
1669 _handleServerFenceMsg() {
1670 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
1671 this._sock.rQskipBytes(3); // Padding
1672 let flags = this._sock.rQshift32();
1673 let length = this._sock.rQshift8();
1674
1675 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
1676
1677 if (length > 64) {
1678 Log.Warn("Bad payload length (" + length + ") in fence response");
1679 length = 64;
1680 }
1681
1682 const payload = this._sock.rQshiftStr(length);
1683
1684 this._supportsFence = true;
1685
1686 /*
1687 * Fence flags
1688 *
1689 * (1<<0) - BlockBefore
1690 * (1<<1) - BlockAfter
1691 * (1<<2) - SyncNext
1692 * (1<<31) - Request
1693 */
1694
1695 if (!(flags & (1<<31))) {
1696 return this._fail("Unexpected fence response");
1697 }
1698
1699 // Filter out unsupported flags
1700 // FIXME: support syncNext
1701 flags &= (1<<0) | (1<<1);
1702
1703 // BlockBefore and BlockAfter are automatically handled by
1704 // the fact that we process each incoming message
1705 // synchronuosly.
1706 RFB.messages.clientFence(this._sock, flags, payload);
1707
1708 return true;
1709 }
1710
1711 _handleXvpMsg() {
1712 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
1713 this._sock.rQskipBytes(1); // Padding
1714 const xvpVer = this._sock.rQshift8();
1715 const xvpMsg = this._sock.rQshift8();
1716
1717 switch (xvpMsg) {
1718 case 0: // XVP_FAIL
1719 Log.Error("XVP Operation Failed");
1720 break;
1721 case 1: // XVP_INIT
1722 this._rfbXvpVer = xvpVer;
1723 Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
1724 this._setCapability("power", true);
1725 break;
1726 default:
1727 this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
1728 break;
1729 }
1730
1731 return true;
1732 }
1733
1734 _normalMsg() {
1735 let msgType;
1736 if (this._FBU.rects > 0) {
1737 msgType = 0;
1738 } else {
1739 msgType = this._sock.rQshift8();
1740 }
1741
1742 let first, ret;
1743 switch (msgType) {
1744 case 0: // FramebufferUpdate
1745 ret = this._framebufferUpdate();
1746 if (ret && !this._enabledContinuousUpdates) {
1747 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
1748 this._fbWidth, this._fbHeight);
1749 }
1750 return ret;
1751
1752 case 1: // SetColorMapEntries
1753 return this._handleSetColourMapMsg();
1754
1755 case 2: // Bell
1756 Log.Debug("Bell");
1757 this.dispatchEvent(new CustomEvent(
1758 "bell",
1759 { detail: {} }));
1760 return true;
1761
1762 case 3: // ServerCutText
1763 return this._handleServerCutText();
1764
1765 case 150: // EndOfContinuousUpdates
1766 first = !this._supportsContinuousUpdates;
1767 this._supportsContinuousUpdates = true;
1768 this._enabledContinuousUpdates = false;
1769 if (first) {
1770 this._enabledContinuousUpdates = true;
1771 this._updateContinuousUpdates();
1772 Log.Info("Enabling continuous updates.");
1773 } else {
1774 // FIXME: We need to send a framebufferupdaterequest here
1775 // if we add support for turning off continuous updates
1776 }
1777 return true;
1778
1779 case 248: // ServerFence
1780 return this._handleServerFenceMsg();
1781
1782 case 250: // XVP
1783 return this._handleXvpMsg();
1784
1785 default:
1786 this._fail("Unexpected server message (type " + msgType + ")");
1787 Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
1788 return true;
1789 }
1790 }
1791
1792 _onFlush() {
1793 this._flushing = false;
1794 // Resume processing
1795 if (this._sock.rQlen > 0) {
1796 this._handleMessage();
1797 }
1798 }
1799
1800 _framebufferUpdate() {
1801 if (this._FBU.rects === 0) {
1802 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
1803 this._sock.rQskipBytes(1); // Padding
1804 this._FBU.rects = this._sock.rQshift16();
1805
1806 // Make sure the previous frame is fully rendered first
1807 // to avoid building up an excessive queue
1808 if (this._display.pending()) {
1809 this._flushing = true;
1810 this._display.flush();
1811 return false;
1812 }
1813 }
1814
1815 while (this._FBU.rects > 0) {
1816 if (this._FBU.encoding === null) {
1817 if (this._sock.rQwait("rect header", 12)) { return false; }
1818 /* New FramebufferUpdate */
1819
1820 const hdr = this._sock.rQshiftBytes(12);
1821 this._FBU.x = (hdr[0] << 8) + hdr[1];
1822 this._FBU.y = (hdr[2] << 8) + hdr[3];
1823 this._FBU.width = (hdr[4] << 8) + hdr[5];
1824 this._FBU.height = (hdr[6] << 8) + hdr[7];
1825 this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
1826 (hdr[10] << 8) + hdr[11], 10);
1827 }
1828
1829 if (!this._handleRect()) {
1830 return false;
1831 }
1832
1833 this._FBU.rects--;
1834 this._FBU.encoding = null;
1835 }
1836
1837 this._display.flip();
1838
1839 return true; // We finished this FBU
1840 }
1841
1842 _handleRect() {
1843 switch (this._FBU.encoding) {
1844 case encodings.pseudoEncodingLastRect:
1845 this._FBU.rects = 1; // Will be decreased when we return
1846 return true;
1847
1848 case encodings.pseudoEncodingVMwareCursor:
1849 return this._handleVMwareCursor();
1850
1851 case encodings.pseudoEncodingCursor:
1852 return this._handleCursor();
1853
1854 case encodings.pseudoEncodingQEMUExtendedKeyEvent:
1855 // Old Safari doesn't support creating keyboard events
1856 try {
1857 const keyboardEvent = document.createEvent("keyboardEvent");
1858 if (keyboardEvent.code !== undefined) {
1859 this._qemuExtKeyEventSupported = true;
1860 }
1861 } catch (err) {
1862 // Do nothing
1863 }
1864 return true;
1865
1866 case encodings.pseudoEncodingDesktopName:
1867 return this._handleDesktopName();
1868
1869 case encodings.pseudoEncodingDesktopSize:
1870 this._resize(this._FBU.width, this._FBU.height);
1871 return true;
1872
1873 case encodings.pseudoEncodingExtendedDesktopSize:
1874 return this._handleExtendedDesktopSize();
1875
1876 default:
1877 return this._handleDataRect();
1878 }
1879 }
1880
1881 _handleVMwareCursor() {
1882 const hotx = this._FBU.x; // hotspot-x
1883 const hoty = this._FBU.y; // hotspot-y
1884 const w = this._FBU.width;
1885 const h = this._FBU.height;
1886 if (this._sock.rQwait("VMware cursor encoding", 1)) {
1887 return false;
1888 }
1889
1890 const cursorType = this._sock.rQshift8();
1891
1892 this._sock.rQshift8(); //Padding
1893
1894 let rgba;
1895 const bytesPerPixel = 4;
1896
1897 //Classic cursor
1898 if (cursorType == 0) {
1899 //Used to filter away unimportant bits.
1900 //OR is used for correct conversion in js.
1901 const PIXEL_MASK = 0xffffff00 | 0;
1902 rgba = new Array(w * h * bytesPerPixel);
1903
1904 if (this._sock.rQwait("VMware cursor classic encoding",
1905 (w * h * bytesPerPixel) * 2, 2)) {
1906 return false;
1907 }
1908
1909 let andMask = new Array(w * h);
1910 for (let pixel = 0; pixel < (w * h); pixel++) {
1911 andMask[pixel] = this._sock.rQshift32();
1912 }
1913
1914 let xorMask = new Array(w * h);
1915 for (let pixel = 0; pixel < (w * h); pixel++) {
1916 xorMask[pixel] = this._sock.rQshift32();
1917 }
1918
1919 for (let pixel = 0; pixel < (w * h); pixel++) {
1920 if (andMask[pixel] == 0) {
1921 //Fully opaque pixel
1922 let bgr = xorMask[pixel];
1923 let r = bgr >> 8 & 0xff;
1924 let g = bgr >> 16 & 0xff;
1925 let b = bgr >> 24 & 0xff;
1926
1927 rgba[(pixel * bytesPerPixel) ] = r; //r
1928 rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
1929 rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
1930 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
1931
1932 } else if ((andMask[pixel] & PIXEL_MASK) ==
1933 PIXEL_MASK) {
1934 //Only screen value matters, no mouse colouring
1935 if (xorMask[pixel] == 0) {
1936 //Transparent pixel
1937 rgba[(pixel * bytesPerPixel) ] = 0x00;
1938 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1939 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1940 rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
1941
1942 } else if ((xorMask[pixel] & PIXEL_MASK) ==
1943 PIXEL_MASK) {
1944 //Inverted pixel, not supported in browsers.
1945 //Fully opaque instead.
1946 rgba[(pixel * bytesPerPixel) ] = 0x00;
1947 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1948 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1949 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1950
1951 } else {
1952 //Unhandled xorMask
1953 rgba[(pixel * bytesPerPixel) ] = 0x00;
1954 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1955 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1956 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1957 }
1958
1959 } else {
1960 //Unhandled andMask
1961 rgba[(pixel * bytesPerPixel) ] = 0x00;
1962 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1963 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1964 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1965 }
1966 }
1967
1968 //Alpha cursor.
1969 } else if (cursorType == 1) {
1970 if (this._sock.rQwait("VMware cursor alpha encoding",
1971 (w * h * 4), 2)) {
1972 return false;
1973 }
1974
1975 rgba = new Array(w * h * bytesPerPixel);
1976
1977 for (let pixel = 0; pixel < (w * h); pixel++) {
1978 let data = this._sock.rQshift32();
1979
1980 rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
1981 rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
1982 rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
1983 rgba[(pixel * 4) + 3 ] = data & 0xff; //a
1984 }
1985
1986 } else {
1987 Log.Warn("The given cursor type is not supported: "
1988 + cursorType + " given.");
1989 return false;
1990 }
1991
1992 this._updateCursor(rgba, hotx, hoty, w, h);
1993
1994 return true;
1995 }
1996
1997 _handleCursor() {
1998 const hotx = this._FBU.x; // hotspot-x
1999 const hoty = this._FBU.y; // hotspot-y
2000 const w = this._FBU.width;
2001 const h = this._FBU.height;
2002
2003 const pixelslength = w * h * 4;
2004 const masklength = Math.ceil(w / 8) * h;
2005
2006 let bytes = pixelslength + masklength;
2007 if (this._sock.rQwait("cursor encoding", bytes)) {
2008 return false;
2009 }
2010
2011 // Decode from BGRX pixels + bit mask to RGBA
2012 const pixels = this._sock.rQshiftBytes(pixelslength);
2013 const mask = this._sock.rQshiftBytes(masklength);
2014 let rgba = new Uint8Array(w * h * 4);
2015
2016 let pixIdx = 0;
2017 for (let y = 0; y < h; y++) {
2018 for (let x = 0; x < w; x++) {
2019 let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
2020 let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
2021 rgba[pixIdx ] = pixels[pixIdx + 2];
2022 rgba[pixIdx + 1] = pixels[pixIdx + 1];
2023 rgba[pixIdx + 2] = pixels[pixIdx];
2024 rgba[pixIdx + 3] = alpha;
2025 pixIdx += 4;
2026 }
2027 }
2028
2029 this._updateCursor(rgba, hotx, hoty, w, h);
2030
2031 return true;
2032 }
2033
2034 _handleDesktopName() {
2035 if (this._sock.rQwait("DesktopName", 4)) {
2036 return false;
2037 }
2038
2039 let length = this._sock.rQshift32();
2040
2041 if (this._sock.rQwait("DesktopName", length, 4)) {
2042 return false;
2043 }
2044
2045 let name = this._sock.rQshiftStr(length);
2046 name = decodeUTF8(name, true);
2047
2048 this._setDesktopName(name);
2049
2050 return true;
2051 }
2052
2053 _handleExtendedDesktopSize() {
2054 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
2055 return false;
2056 }
2057
2058 const numberOfScreens = this._sock.rQpeek8();
2059
2060 let bytes = 4 + (numberOfScreens * 16);
2061 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
2062 return false;
2063 }
2064
2065 const firstUpdate = !this._supportsSetDesktopSize;
2066 this._supportsSetDesktopSize = true;
2067
2068 // Normally we only apply the current resize mode after a
2069 // window resize event. However there is no such trigger on the
2070 // initial connect. And we don't know if the server supports
2071 // resizing until we've gotten here.
2072 if (firstUpdate) {
2073 this._requestRemoteResize();
2074 }
2075
2076 this._sock.rQskipBytes(1); // number-of-screens
2077 this._sock.rQskipBytes(3); // padding
2078
2079 for (let i = 0; i < numberOfScreens; i += 1) {
2080 // Save the id and flags of the first screen
2081 if (i === 0) {
2082 this._screenID = this._sock.rQshiftBytes(4); // id
2083 this._sock.rQskipBytes(2); // x-position
2084 this._sock.rQskipBytes(2); // y-position
2085 this._sock.rQskipBytes(2); // width
2086 this._sock.rQskipBytes(2); // height
2087 this._screenFlags = this._sock.rQshiftBytes(4); // flags
2088 } else {
2089 this._sock.rQskipBytes(16);
2090 }
2091 }
2092
2093 /*
2094 * The x-position indicates the reason for the change:
2095 *
2096 * 0 - server resized on its own
2097 * 1 - this client requested the resize
2098 * 2 - another client requested the resize
2099 */
2100
2101 // We need to handle errors when we requested the resize.
2102 if (this._FBU.x === 1 && this._FBU.y !== 0) {
2103 let msg = "";
2104 // The y-position indicates the status code from the server
2105 switch (this._FBU.y) {
2106 case 1:
2107 msg = "Resize is administratively prohibited";
2108 break;
2109 case 2:
2110 msg = "Out of resources";
2111 break;
2112 case 3:
2113 msg = "Invalid screen layout";
2114 break;
2115 default:
2116 msg = "Unknown reason";
2117 break;
2118 }
2119 Log.Warn("Server did not accept the resize request: "
2120 + msg);
2121 } else {
2122 this._resize(this._FBU.width, this._FBU.height);
2123 }
2124
2125 return true;
2126 }
2127
2128 _handleDataRect() {
2129 let decoder = this._decoders[this._FBU.encoding];
2130 if (!decoder) {
2131 this._fail("Unsupported encoding (encoding: " +
2132 this._FBU.encoding + ")");
2133 return false;
2134 }
2135
2136 try {
2137 return decoder.decodeRect(this._FBU.x, this._FBU.y,
2138 this._FBU.width, this._FBU.height,
2139 this._sock, this._display,
2140 this._fbDepth);
2141 } catch (err) {
2142 this._fail("Error decoding rect: " + err);
2143 return false;
2144 }
2145 }
2146
2147 _updateContinuousUpdates() {
2148 if (!this._enabledContinuousUpdates) { return; }
2149
2150 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
2151 this._fbWidth, this._fbHeight);
2152 }
2153
2154 _resize(width, height) {
2155 this._fbWidth = width;
2156 this._fbHeight = height;
2157
2158 this._display.resize(this._fbWidth, this._fbHeight);
2159
2160 // Adjust the visible viewport based on the new dimensions
2161 this._updateClip();
2162 this._updateScale();
2163
2164 this._updateContinuousUpdates();
2165 }
2166
2167 _xvpOp(ver, op) {
2168 if (this._rfbXvpVer < ver) { return; }
2169 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
2170 RFB.messages.xvpOp(this._sock, ver, op);
2171 }
2172
2173 _updateCursor(rgba, hotx, hoty, w, h) {
2174 this._cursorImage = {
2175 rgbaPixels: rgba,
2176 hotx: hotx, hoty: hoty, w: w, h: h,
2177 };
2178 this._refreshCursor();
2179 }
2180
2181 _shouldShowDotCursor() {
2182 // Called when this._cursorImage is updated
2183 if (!this._showDotCursor) {
2184 // User does not want to see the dot, so...
2185 return false;
2186 }
2187
2188 // The dot should not be shown if the cursor is already visible,
2189 // i.e. contains at least one not-fully-transparent pixel.
2190 // So iterate through all alpha bytes in rgba and stop at the
2191 // first non-zero.
2192 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
2193 if (this._cursorImage.rgbaPixels[i]) {
2194 return false;
2195 }
2196 }
2197
2198 // At this point, we know that the cursor is fully transparent, and
2199 // the user wants to see the dot instead of this.
2200 return true;
2201 }
2202
2203 _refreshCursor() {
2204 if (this._rfbConnectionState !== "connecting" &&
2205 this._rfbConnectionState !== "connected") {
2206 return;
2207 }
2208 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
2209 this._cursor.change(image.rgbaPixels,
2210 image.hotx, image.hoty,
2211 image.w, image.h
2212 );
2213 }
2214
2215 static genDES(password, challenge) {
2216 const passwordChars = password.split('').map(c => c.charCodeAt(0));
2217 return (new DES(passwordChars)).encrypt(challenge);
2218 }
2219 }
2220
2221 // Class Methods
2222 RFB.messages = {
2223 keyEvent(sock, keysym, down) {
2224 const buff = sock._sQ;
2225 const offset = sock._sQlen;
2226
2227 buff[offset] = 4; // msg-type
2228 buff[offset + 1] = down;
2229
2230 buff[offset + 2] = 0;
2231 buff[offset + 3] = 0;
2232
2233 buff[offset + 4] = (keysym >> 24);
2234 buff[offset + 5] = (keysym >> 16);
2235 buff[offset + 6] = (keysym >> 8);
2236 buff[offset + 7] = keysym;
2237
2238 sock._sQlen += 8;
2239 sock.flush();
2240 },
2241
2242 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
2243 function getRFBkeycode(xtScanCode) {
2244 const upperByte = (keycode >> 8);
2245 const lowerByte = (keycode & 0x00ff);
2246 if (upperByte === 0xe0 && lowerByte < 0x7f) {
2247 return lowerByte | 0x80;
2248 }
2249 return xtScanCode;
2250 }
2251
2252 const buff = sock._sQ;
2253 const offset = sock._sQlen;
2254
2255 buff[offset] = 255; // msg-type
2256 buff[offset + 1] = 0; // sub msg-type
2257
2258 buff[offset + 2] = (down >> 8);
2259 buff[offset + 3] = down;
2260
2261 buff[offset + 4] = (keysym >> 24);
2262 buff[offset + 5] = (keysym >> 16);
2263 buff[offset + 6] = (keysym >> 8);
2264 buff[offset + 7] = keysym;
2265
2266 const RFBkeycode = getRFBkeycode(keycode);
2267
2268 buff[offset + 8] = (RFBkeycode >> 24);
2269 buff[offset + 9] = (RFBkeycode >> 16);
2270 buff[offset + 10] = (RFBkeycode >> 8);
2271 buff[offset + 11] = RFBkeycode;
2272
2273 sock._sQlen += 12;
2274 sock.flush();
2275 },
2276
2277 pointerEvent(sock, x, y, mask) {
2278 const buff = sock._sQ;
2279 const offset = sock._sQlen;
2280
2281 buff[offset] = 5; // msg-type
2282
2283 buff[offset + 1] = mask;
2284
2285 buff[offset + 2] = x >> 8;
2286 buff[offset + 3] = x;
2287
2288 buff[offset + 4] = y >> 8;
2289 buff[offset + 5] = y;
2290
2291 sock._sQlen += 6;
2292 sock.flush();
2293 },
2294
2295 // Used to build Notify and Request data.
2296 _buildExtendedClipboardFlags(actions, formats) {
2297 let data = new Uint8Array(4);
2298 let formatFlag = 0x00000000;
2299 let actionFlag = 0x00000000;
2300
2301 for (let i = 0; i < actions.length; i++) {
2302 actionFlag |= actions[i];
2303 }
2304
2305 for (let i = 0; i < formats.length; i++) {
2306 formatFlag |= formats[i];
2307 }
2308
2309 data[0] = actionFlag >> 24; // Actions
2310 data[1] = 0x00; // Reserved
2311 data[2] = 0x00; // Reserved
2312 data[3] = formatFlag; // Formats
2313
2314 return data;
2315 },
2316
2317 extendedClipboardProvide(sock, formats, inData) {
2318 // Deflate incomming data and their sizes
2319 let deflator = new Deflator();
2320 let dataToDeflate = [];
2321
2322 for (let i = 0; i < formats.length; i++) {
2323 // We only support the format Text at this time
2324 if (formats[i] != extendedClipboardFormatText) {
2325 throw new Error("Unsupported extended clipboard format for Provide message.");
2326 }
2327
2328 // Change lone \r or \n into \r\n as defined in rfbproto
2329 inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
2330
2331 // Check if it already has \0
2332 let text = encodeUTF8(inData[i] + "\0");
2333
2334 dataToDeflate.push( (text.length >> 24) & 0xFF,
2335 (text.length >> 16) & 0xFF,
2336 (text.length >> 8) & 0xFF,
2337 (text.length & 0xFF));
2338
2339 for (let j = 0; j < text.length; j++) {
2340 dataToDeflate.push(text.charCodeAt(j));
2341 }
2342 }
2343
2344 let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
2345
2346 // Build data to send
2347 let data = new Uint8Array(4 + deflatedData.length);
2348 data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
2349 formats));
2350 data.set(deflatedData, 4);
2351
2352 RFB.messages.clientCutText(sock, data, true);
2353 },
2354
2355 extendedClipboardNotify(sock, formats) {
2356 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
2357 formats);
2358 RFB.messages.clientCutText(sock, flags, true);
2359 },
2360
2361 extendedClipboardRequest(sock, formats) {
2362 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
2363 formats);
2364 RFB.messages.clientCutText(sock, flags, true);
2365 },
2366
2367 extendedClipboardCaps(sock, actions, formats) {
2368 let formatKeys = Object.keys(formats);
2369 let data = new Uint8Array(4 + (4 * formatKeys.length));
2370
2371 formatKeys.map(x => parseInt(x));
2372 formatKeys.sort((a, b) => a - b);
2373
2374 data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
2375
2376 let loopOffset = 4;
2377 for (let i = 0; i < formatKeys.length; i++) {
2378 data[loopOffset] = formats[formatKeys[i]] >> 24;
2379 data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
2380 data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
2381 data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
2382
2383 loopOffset += 4;
2384 data[3] |= (1 << formatKeys[i]); // Update our format flags
2385 }
2386
2387 RFB.messages.clientCutText(sock, data, true);
2388 },
2389
2390 clientCutText(sock, data, extended = false) {
2391 const buff = sock._sQ;
2392 const offset = sock._sQlen;
2393
2394 buff[offset] = 6; // msg-type
2395
2396 buff[offset + 1] = 0; // padding
2397 buff[offset + 2] = 0; // padding
2398 buff[offset + 3] = 0; // padding
2399
2400 let length;
2401 if (extended) {
2402 length = toUnsigned32bit(-data.length);
2403 } else {
2404 length = data.length;
2405 }
2406
2407 buff[offset + 4] = length >> 24;
2408 buff[offset + 5] = length >> 16;
2409 buff[offset + 6] = length >> 8;
2410 buff[offset + 7] = length;
2411
2412 sock._sQlen += 8;
2413
2414 // We have to keep track of from where in the data we begin creating the
2415 // buffer for the flush in the next iteration.
2416 let dataOffset = 0;
2417
2418 let remaining = data.length;
2419 while (remaining > 0) {
2420
2421 let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
2422 for (let i = 0; i < flushSize; i++) {
2423 buff[sock._sQlen + i] = data[dataOffset + i];
2424 }
2425
2426 sock._sQlen += flushSize;
2427 sock.flush();
2428
2429 remaining -= flushSize;
2430 dataOffset += flushSize;
2431 }
2432
2433 },
2434
2435 setDesktopSize(sock, width, height, id, flags) {
2436 const buff = sock._sQ;
2437 const offset = sock._sQlen;
2438
2439 buff[offset] = 251; // msg-type
2440 buff[offset + 1] = 0; // padding
2441 buff[offset + 2] = width >> 8; // width
2442 buff[offset + 3] = width;
2443 buff[offset + 4] = height >> 8; // height
2444 buff[offset + 5] = height;
2445
2446 buff[offset + 6] = 1; // number-of-screens
2447 buff[offset + 7] = 0; // padding
2448
2449 // screen array
2450 buff[offset + 8] = id >> 24; // id
2451 buff[offset + 9] = id >> 16;
2452 buff[offset + 10] = id >> 8;
2453 buff[offset + 11] = id;
2454 buff[offset + 12] = 0; // x-position
2455 buff[offset + 13] = 0;
2456 buff[offset + 14] = 0; // y-position
2457 buff[offset + 15] = 0;
2458 buff[offset + 16] = width >> 8; // width
2459 buff[offset + 17] = width;
2460 buff[offset + 18] = height >> 8; // height
2461 buff[offset + 19] = height;
2462 buff[offset + 20] = flags >> 24; // flags
2463 buff[offset + 21] = flags >> 16;
2464 buff[offset + 22] = flags >> 8;
2465 buff[offset + 23] = flags;
2466
2467 sock._sQlen += 24;
2468 sock.flush();
2469 },
2470
2471 clientFence(sock, flags, payload) {
2472 const buff = sock._sQ;
2473 const offset = sock._sQlen;
2474
2475 buff[offset] = 248; // msg-type
2476
2477 buff[offset + 1] = 0; // padding
2478 buff[offset + 2] = 0; // padding
2479 buff[offset + 3] = 0; // padding
2480
2481 buff[offset + 4] = flags >> 24; // flags
2482 buff[offset + 5] = flags >> 16;
2483 buff[offset + 6] = flags >> 8;
2484 buff[offset + 7] = flags;
2485
2486 const n = payload.length;
2487
2488 buff[offset + 8] = n; // length
2489
2490 for (let i = 0; i < n; i++) {
2491 buff[offset + 9 + i] = payload.charCodeAt(i);
2492 }
2493
2494 sock._sQlen += 9 + n;
2495 sock.flush();
2496 },
2497
2498 enableContinuousUpdates(sock, enable, x, y, width, height) {
2499 const buff = sock._sQ;
2500 const offset = sock._sQlen;
2501
2502 buff[offset] = 150; // msg-type
2503 buff[offset + 1] = enable; // enable-flag
2504
2505 buff[offset + 2] = x >> 8; // x
2506 buff[offset + 3] = x;
2507 buff[offset + 4] = y >> 8; // y
2508 buff[offset + 5] = y;
2509 buff[offset + 6] = width >> 8; // width
2510 buff[offset + 7] = width;
2511 buff[offset + 8] = height >> 8; // height
2512 buff[offset + 9] = height;
2513
2514 sock._sQlen += 10;
2515 sock.flush();
2516 },
2517
2518 pixelFormat(sock, depth, trueColor) {
2519 const buff = sock._sQ;
2520 const offset = sock._sQlen;
2521
2522 let bpp;
2523
2524 if (depth > 16) {
2525 bpp = 32;
2526 } else if (depth > 8) {
2527 bpp = 16;
2528 } else {
2529 bpp = 8;
2530 }
2531
2532 const bits = Math.floor(depth/3);
2533
2534 buff[offset] = 0; // msg-type
2535
2536 buff[offset + 1] = 0; // padding
2537 buff[offset + 2] = 0; // padding
2538 buff[offset + 3] = 0; // padding
2539
2540 buff[offset + 4] = bpp; // bits-per-pixel
2541 buff[offset + 5] = depth; // depth
2542 buff[offset + 6] = 0; // little-endian
2543 buff[offset + 7] = trueColor ? 1 : 0; // true-color
2544
2545 buff[offset + 8] = 0; // red-max
2546 buff[offset + 9] = (1 << bits) - 1; // red-max
2547
2548 buff[offset + 10] = 0; // green-max
2549 buff[offset + 11] = (1 << bits) - 1; // green-max
2550
2551 buff[offset + 12] = 0; // blue-max
2552 buff[offset + 13] = (1 << bits) - 1; // blue-max
2553
2554 buff[offset + 14] = bits * 2; // red-shift
2555 buff[offset + 15] = bits * 1; // green-shift
2556 buff[offset + 16] = bits * 0; // blue-shift
2557
2558 buff[offset + 17] = 0; // padding
2559 buff[offset + 18] = 0; // padding
2560 buff[offset + 19] = 0; // padding
2561
2562 sock._sQlen += 20;
2563 sock.flush();
2564 },
2565
2566 clientEncodings(sock, encodings) {
2567 const buff = sock._sQ;
2568 const offset = sock._sQlen;
2569
2570 buff[offset] = 2; // msg-type
2571 buff[offset + 1] = 0; // padding
2572
2573 buff[offset + 2] = encodings.length >> 8;
2574 buff[offset + 3] = encodings.length;
2575
2576 let j = offset + 4;
2577 for (let i = 0; i < encodings.length; i++) {
2578 const enc = encodings[i];
2579 buff[j] = enc >> 24;
2580 buff[j + 1] = enc >> 16;
2581 buff[j + 2] = enc >> 8;
2582 buff[j + 3] = enc;
2583
2584 j += 4;
2585 }
2586
2587 sock._sQlen += j - offset;
2588 sock.flush();
2589 },
2590
2591 fbUpdateRequest(sock, incremental, x, y, w, h) {
2592 const buff = sock._sQ;
2593 const offset = sock._sQlen;
2594
2595 if (typeof(x) === "undefined") { x = 0; }
2596 if (typeof(y) === "undefined") { y = 0; }
2597
2598 buff[offset] = 3; // msg-type
2599 buff[offset + 1] = incremental ? 1 : 0;
2600
2601 buff[offset + 2] = (x >> 8) & 0xFF;
2602 buff[offset + 3] = x & 0xFF;
2603
2604 buff[offset + 4] = (y >> 8) & 0xFF;
2605 buff[offset + 5] = y & 0xFF;
2606
2607 buff[offset + 6] = (w >> 8) & 0xFF;
2608 buff[offset + 7] = w & 0xFF;
2609
2610 buff[offset + 8] = (h >> 8) & 0xFF;
2611 buff[offset + 9] = h & 0xFF;
2612
2613 sock._sQlen += 10;
2614 sock.flush();
2615 },
2616
2617 xvpOp(sock, ver, op) {
2618 const buff = sock._sQ;
2619 const offset = sock._sQlen;
2620
2621 buff[offset] = 250; // msg-type
2622 buff[offset + 1] = 0; // padding
2623
2624 buff[offset + 2] = ver;
2625 buff[offset + 3] = op;
2626
2627 sock._sQlen += 4;
2628 sock.flush();
2629 }
2630 };
2631
2632 RFB.cursors = {
2633 none: {
2634 rgbaPixels: new Uint8Array(),
2635 w: 0, h: 0,
2636 hotx: 0, hoty: 0,
2637 },
2638
2639 dot: {
2640 /* eslint-disable indent */
2641 rgbaPixels: new Uint8Array([
2642 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2643 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2644 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2645 ]),
2646 /* eslint-enable indent */
2647 w: 3, h: 3,
2648 hotx: 1, hoty: 1,
2649 }
2650 };