]> git.proxmox.com Git - mirror_novnc.git/blob - core/rfb.js
Standardize on camelCase for variables in RFB
[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._handle_message();
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._init_msg.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 _handle_message() {
783 if (this._sock.rQlen === 0) {
784 Log.Warn("handle_message 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._normal_msg()) {
798 break;
799 }
800 if (this._sock.rQlen === 0) {
801 break;
802 }
803 }
804 break;
805 default:
806 this._init_msg();
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 // Otherwise we treat this as a mouse click event.
840 // Send the button down event here, as the button up
841 // event is sent at the end of this function.
842 RFB.messages.pointerEvent(this._sock,
843 this._display.absX(x),
844 this._display.absY(y),
845 bmask);
846 }
847 }
848
849 if (this._viewOnly) { return; } // View only, skip mouse events
850
851 if (this._rfbConnectionState !== 'connected') { return; }
852 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouseButtonMask);
853 }
854
855 _handleMouseMove(x, y) {
856 if (this._viewportDragging) {
857 const deltaX = this._viewportDragPos.x - x;
858 const deltaY = this._viewportDragPos.y - y;
859
860 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
861 Math.abs(deltaY) > dragThreshold)) {
862 this._viewportHasMoved = true;
863
864 this._viewportDragPos = {'x': x, 'y': y};
865 this._display.viewportChangePos(deltaX, deltaY);
866 }
867
868 // Skip sending mouse events
869 return;
870 }
871
872 if (this._viewOnly) { return; } // View only, skip mouse events
873
874 if (this._rfbConnectionState !== 'connected') { return; }
875 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouseButtonMask);
876 }
877
878 // Message Handlers
879
880 _negotiate_protocol_version() {
881 if (this._sock.rQwait("version", 12)) {
882 return false;
883 }
884
885 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
886 Log.Info("Server ProtocolVersion: " + sversion);
887 let isRepeater = 0;
888 switch (sversion) {
889 case "000.000": // UltraVNC repeater
890 isRepeater = 1;
891 break;
892 case "003.003":
893 case "003.006": // UltraVNC
894 case "003.889": // Apple Remote Desktop
895 this._rfbVersion = 3.3;
896 break;
897 case "003.007":
898 this._rfbVersion = 3.7;
899 break;
900 case "003.008":
901 case "004.000": // Intel AMT KVM
902 case "004.001": // RealVNC 4.6
903 case "005.000": // RealVNC 5.3
904 this._rfbVersion = 3.8;
905 break;
906 default:
907 return this._fail("Invalid server version " + sversion);
908 }
909
910 if (isRepeater) {
911 let repeaterID = "ID:" + this._repeaterID;
912 while (repeaterID.length < 250) {
913 repeaterID += "\0";
914 }
915 this._sock.send_string(repeaterID);
916 return true;
917 }
918
919 if (this._rfbVersion > this._rfbMaxVersion) {
920 this._rfbVersion = this._rfbMaxVersion;
921 }
922
923 const cversion = "00" + parseInt(this._rfbVersion, 10) +
924 ".00" + ((this._rfbVersion * 10) % 10);
925 this._sock.send_string("RFB " + cversion + "\n");
926 Log.Debug('Sent ProtocolVersion: ' + cversion);
927
928 this._rfbInitState = 'Security';
929 }
930
931 _negotiate_security() {
932 // Polyfill since IE and PhantomJS doesn't have
933 // TypedArray.includes()
934 function includes(item, array) {
935 for (let i = 0; i < array.length; i++) {
936 if (array[i] === item) {
937 return true;
938 }
939 }
940 return false;
941 }
942
943 if (this._rfbVersion >= 3.7) {
944 // Server sends supported list, client decides
945 const numTypes = this._sock.rQshift8();
946 if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
947
948 if (numTypes === 0) {
949 this._rfbInitState = "SecurityReason";
950 this._securityContext = "no security types";
951 this._securityStatus = 1;
952 return this._init_msg();
953 }
954
955 const types = this._sock.rQshiftBytes(numTypes);
956 Log.Debug("Server security types: " + types);
957
958 // Look for each auth in preferred order
959 if (includes(1, types)) {
960 this._rfbAuthScheme = 1; // None
961 } else if (includes(22, types)) {
962 this._rfbAuthScheme = 22; // XVP
963 } else if (includes(16, types)) {
964 this._rfbAuthScheme = 16; // Tight
965 } else if (includes(2, types)) {
966 this._rfbAuthScheme = 2; // VNC Auth
967 } else if (includes(19, types)) {
968 this._rfbAuthScheme = 19; // VeNCrypt Auth
969 } else {
970 return this._fail("Unsupported security types (types: " + types + ")");
971 }
972
973 this._sock.send([this._rfbAuthScheme]);
974 } else {
975 // Server decides
976 if (this._sock.rQwait("security scheme", 4)) { return false; }
977 this._rfbAuthScheme = this._sock.rQshift32();
978
979 if (this._rfbAuthScheme == 0) {
980 this._rfbInitState = "SecurityReason";
981 this._securityContext = "authentication scheme";
982 this._securityStatus = 1;
983 return this._init_msg();
984 }
985 }
986
987 this._rfbInitState = 'Authentication';
988 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
989
990 return this._init_msg(); // jump to authentication
991 }
992
993 _handle_security_reason() {
994 if (this._sock.rQwait("reason length", 4)) {
995 return false;
996 }
997 const strlen = this._sock.rQshift32();
998 let reason = "";
999
1000 if (strlen > 0) {
1001 if (this._sock.rQwait("reason", strlen, 4)) { return false; }
1002 reason = this._sock.rQshiftStr(strlen);
1003 }
1004
1005 if (reason !== "") {
1006 this.dispatchEvent(new CustomEvent(
1007 "securityfailure",
1008 { detail: { status: this._securityStatus,
1009 reason: reason } }));
1010
1011 return this._fail("Security negotiation failed on " +
1012 this._securityContext +
1013 " (reason: " + reason + ")");
1014 } else {
1015 this.dispatchEvent(new CustomEvent(
1016 "securityfailure",
1017 { detail: { status: this._securityStatus } }));
1018
1019 return this._fail("Security negotiation failed on " +
1020 this._securityContext);
1021 }
1022 }
1023
1024 // authentication
1025 _negotiate_xvp_auth() {
1026 if (this._rfbCredentials.username === undefined ||
1027 this._rfbCredentials.password === undefined ||
1028 this._rfbCredentials.target === undefined) {
1029 this.dispatchEvent(new CustomEvent(
1030 "credentialsrequired",
1031 { detail: { types: ["username", "password", "target"] } }));
1032 return false;
1033 }
1034
1035 const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
1036 String.fromCharCode(this._rfbCredentials.target.length) +
1037 this._rfbCredentials.username +
1038 this._rfbCredentials.target;
1039 this._sock.send_string(xvpAuthStr);
1040 this._rfbAuthScheme = 2;
1041 return this._negotiate_authentication();
1042 }
1043
1044 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1045 _negotiate_vencrypt_auth() {
1046
1047 // waiting for VeNCrypt version
1048 if (this._rfbVeNCryptState == 0) {
1049 if (this._sock.rQwait("vencrypt version", 2)) { return false; }
1050
1051 const major = this._sock.rQshift8();
1052 const minor = this._sock.rQshift8();
1053
1054 if (!(major == 0 && minor == 2)) {
1055 return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
1056 }
1057
1058 this._sock.send([0, 2]);
1059 this._rfbVeNCryptState = 1;
1060 }
1061
1062 // waiting for ACK
1063 if (this._rfbVeNCryptState == 1) {
1064 if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
1065
1066 const res = this._sock.rQshift8();
1067
1068 if (res != 0) {
1069 return this._fail("VeNCrypt failure " + res);
1070 }
1071
1072 this._rfbVeNCryptState = 2;
1073 }
1074 // must fall through here (i.e. no "else if"), beacause we may have already received
1075 // the subtypes length and won't be called again
1076
1077 if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
1078 if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
1079
1080 const subtypesLength = this._sock.rQshift8();
1081 if (subtypesLength < 1) {
1082 return this._fail("VeNCrypt subtypes empty");
1083 }
1084
1085 this._rfbVeNCryptSubtypesLength = subtypesLength;
1086 this._rfbVeNCryptState = 3;
1087 }
1088
1089 // waiting for subtypes list
1090 if (this._rfbVeNCryptState == 3) {
1091 if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
1092
1093 const subtypes = [];
1094 for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
1095 subtypes.push(this._sock.rQshift32());
1096 }
1097
1098 // 256 = Plain subtype
1099 if (subtypes.indexOf(256) != -1) {
1100 // 0x100 = 256
1101 this._sock.send([0, 0, 1, 0]);
1102 this._rfbVeNCryptState = 4;
1103 } else {
1104 return this._fail("VeNCrypt Plain subtype not offered by server");
1105 }
1106 }
1107
1108 // negotiated Plain subtype, server waits for password
1109 if (this._rfbVeNCryptState == 4) {
1110 if (!this._rfbCredentials.username ||
1111 !this._rfbCredentials.password) {
1112 this.dispatchEvent(new CustomEvent(
1113 "credentialsrequired",
1114 { detail: { types: ["username", "password"] } }));
1115 return false;
1116 }
1117
1118 const user = encodeUTF8(this._rfbCredentials.username);
1119 const pass = encodeUTF8(this._rfbCredentials.password);
1120
1121 // XXX we assume lengths are <= 255 (should not be an issue in the real world)
1122 this._sock.send([0, 0, 0, user.length]);
1123 this._sock.send([0, 0, 0, pass.length]);
1124 this._sock.send_string(user);
1125 this._sock.send_string(pass);
1126
1127 this._rfbInitState = "SecurityResult";
1128 return true;
1129 }
1130 }
1131
1132 _negotiate_std_vnc_auth() {
1133 if (this._sock.rQwait("auth challenge", 16)) { return false; }
1134
1135 if (this._rfbCredentials.password === undefined) {
1136 this.dispatchEvent(new CustomEvent(
1137 "credentialsrequired",
1138 { detail: { types: ["password"] } }));
1139 return false;
1140 }
1141
1142 // TODO(directxman12): make genDES not require an Array
1143 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
1144 const response = RFB.genDES(this._rfbCredentials.password, challenge);
1145 this._sock.send(response);
1146 this._rfbInitState = "SecurityResult";
1147 return true;
1148 }
1149
1150 _negotiate_tight_unix_auth() {
1151 if (this._rfbCredentials.username === undefined ||
1152 this._rfbCredentials.password === undefined) {
1153 this.dispatchEvent(new CustomEvent(
1154 "credentialsrequired",
1155 { detail: { types: ["username", "password"] } }));
1156 return false;
1157 }
1158
1159 this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
1160 this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
1161 this._sock.send_string(this._rfbCredentials.username);
1162 this._sock.send_string(this._rfbCredentials.password);
1163 this._rfbInitState = "SecurityResult";
1164 return true;
1165 }
1166
1167 _negotiate_tight_tunnels(numTunnels) {
1168 const clientSupportedTunnelTypes = {
1169 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
1170 };
1171 const serverSupportedTunnelTypes = {};
1172 // receive tunnel capabilities
1173 for (let i = 0; i < numTunnels; i++) {
1174 const capCode = this._sock.rQshift32();
1175 const capVendor = this._sock.rQshiftStr(4);
1176 const capSignature = this._sock.rQshiftStr(8);
1177 serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
1178 }
1179
1180 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
1181
1182 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1183 // but forgets to advertise it. Try to detect such servers by
1184 // looking for their custom tunnel type.
1185 if (serverSupportedTunnelTypes[1] &&
1186 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
1187 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
1188 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1189 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
1190 }
1191
1192 // choose the notunnel type
1193 if (serverSupportedTunnelTypes[0]) {
1194 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
1195 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
1196 return this._fail("Client's tunnel type had the incorrect " +
1197 "vendor or signature");
1198 }
1199 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1200 this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
1201 return false; // wait until we receive the sub auth count to continue
1202 } else {
1203 return this._fail("Server wanted tunnels, but doesn't support " +
1204 "the notunnel type");
1205 }
1206 }
1207
1208 _negotiate_tight_auth() {
1209 if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
1210 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1211 const numTunnels = this._sock.rQshift32();
1212 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1213
1214 this._rfbTightVNC = true;
1215
1216 if (numTunnels > 0) {
1217 this._negotiate_tight_tunnels(numTunnels);
1218 return false; // wait until we receive the sub auth to continue
1219 }
1220 }
1221
1222 // second pass, do the sub-auth negotiation
1223 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1224 const subAuthCount = this._sock.rQshift32();
1225 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1226 this._rfbInitState = 'SecurityResult';
1227 return true;
1228 }
1229
1230 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1231
1232 const clientSupportedTypes = {
1233 'STDVNOAUTH__': 1,
1234 'STDVVNCAUTH_': 2,
1235 'TGHTULGNAUTH': 129
1236 };
1237
1238 const serverSupportedTypes = [];
1239
1240 for (let i = 0; i < subAuthCount; i++) {
1241 this._sock.rQshift32(); // capNum
1242 const capabilities = this._sock.rQshiftStr(12);
1243 serverSupportedTypes.push(capabilities);
1244 }
1245
1246 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1247
1248 for (let authType in clientSupportedTypes) {
1249 if (serverSupportedTypes.indexOf(authType) != -1) {
1250 this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
1251 Log.Debug("Selected authentication type: " + authType);
1252
1253 switch (authType) {
1254 case 'STDVNOAUTH__': // no auth
1255 this._rfbInitState = 'SecurityResult';
1256 return true;
1257 case 'STDVVNCAUTH_': // VNC auth
1258 this._rfbAuthScheme = 2;
1259 return this._init_msg();
1260 case 'TGHTULGNAUTH': // UNIX auth
1261 this._rfbAuthScheme = 129;
1262 return this._init_msg();
1263 default:
1264 return this._fail("Unsupported tiny auth scheme " +
1265 "(scheme: " + authType + ")");
1266 }
1267 }
1268 }
1269
1270 return this._fail("No supported sub-auth types!");
1271 }
1272
1273 _negotiate_authentication() {
1274 switch (this._rfbAuthScheme) {
1275 case 1: // no auth
1276 if (this._rfbVersion >= 3.8) {
1277 this._rfbInitState = 'SecurityResult';
1278 return true;
1279 }
1280 this._rfbInitState = 'ClientInitialisation';
1281 return this._init_msg();
1282
1283 case 22: // XVP auth
1284 return this._negotiate_xvp_auth();
1285
1286 case 2: // VNC authentication
1287 return this._negotiate_std_vnc_auth();
1288
1289 case 16: // TightVNC Security Type
1290 return this._negotiate_tight_auth();
1291
1292 case 19: // VeNCrypt Security Type
1293 return this._negotiate_vencrypt_auth();
1294
1295 case 129: // TightVNC UNIX Security Type
1296 return this._negotiate_tight_unix_auth();
1297
1298 default:
1299 return this._fail("Unsupported auth scheme (scheme: " +
1300 this._rfbAuthScheme + ")");
1301 }
1302 }
1303
1304 _handle_security_result() {
1305 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
1306
1307 const status = this._sock.rQshift32();
1308
1309 if (status === 0) { // OK
1310 this._rfbInitState = 'ClientInitialisation';
1311 Log.Debug('Authentication OK');
1312 return this._init_msg();
1313 } else {
1314 if (this._rfbVersion >= 3.8) {
1315 this._rfbInitState = "SecurityReason";
1316 this._securityContext = "security result";
1317 this._securityStatus = status;
1318 return this._init_msg();
1319 } else {
1320 this.dispatchEvent(new CustomEvent(
1321 "securityfailure",
1322 { detail: { status: status } }));
1323
1324 return this._fail("Security handshake failed");
1325 }
1326 }
1327 }
1328
1329 _negotiate_server_init() {
1330 if (this._sock.rQwait("server initialization", 24)) { return false; }
1331
1332 /* Screen size */
1333 const width = this._sock.rQshift16();
1334 const height = this._sock.rQshift16();
1335
1336 /* PIXEL_FORMAT */
1337 const bpp = this._sock.rQshift8();
1338 const depth = this._sock.rQshift8();
1339 const bigEndian = this._sock.rQshift8();
1340 const trueColor = this._sock.rQshift8();
1341
1342 const redMax = this._sock.rQshift16();
1343 const greenMax = this._sock.rQshift16();
1344 const blueMax = this._sock.rQshift16();
1345 const redShift = this._sock.rQshift8();
1346 const greenShift = this._sock.rQshift8();
1347 const blueShift = this._sock.rQshift8();
1348 this._sock.rQskipBytes(3); // padding
1349
1350 // NB(directxman12): we don't want to call any callbacks or print messages until
1351 // *after* we're past the point where we could backtrack
1352
1353 /* Connection name/title */
1354 const nameLength = this._sock.rQshift32();
1355 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1356 let name = this._sock.rQshiftStr(nameLength);
1357 name = decodeUTF8(name, true);
1358
1359 if (this._rfbTightVNC) {
1360 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1361 // In TightVNC mode, ServerInit message is extended
1362 const numServerMessages = this._sock.rQshift16();
1363 const numClientMessages = this._sock.rQshift16();
1364 const numEncodings = this._sock.rQshift16();
1365 this._sock.rQskipBytes(2); // padding
1366
1367 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1368 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1369
1370 // we don't actually do anything with the capability information that TIGHT sends,
1371 // so we just skip the all of this.
1372
1373 // TIGHT server message capabilities
1374 this._sock.rQskipBytes(16 * numServerMessages);
1375
1376 // TIGHT client message capabilities
1377 this._sock.rQskipBytes(16 * numClientMessages);
1378
1379 // TIGHT encoding capabilities
1380 this._sock.rQskipBytes(16 * numEncodings);
1381 }
1382
1383 // NB(directxman12): these are down here so that we don't run them multiple times
1384 // if we backtrack
1385 Log.Info("Screen: " + width + "x" + height +
1386 ", bpp: " + bpp + ", depth: " + depth +
1387 ", bigEndian: " + bigEndian +
1388 ", trueColor: " + trueColor +
1389 ", redMax: " + redMax +
1390 ", greenMax: " + greenMax +
1391 ", blueMax: " + blueMax +
1392 ", redShift: " + redShift +
1393 ", greenShift: " + greenShift +
1394 ", blueShift: " + blueShift);
1395
1396 // we're past the point where we could backtrack, so it's safe to call this
1397 this._setDesktopName(name);
1398 this._resize(width, height);
1399
1400 if (!this._viewOnly) { this._keyboard.grab(); }
1401 if (!this._viewOnly) { this._mouse.grab(); }
1402
1403 this._fbDepth = 24;
1404
1405 if (this._fbName === "Intel(r) AMT KVM") {
1406 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1407 this._fbDepth = 8;
1408 }
1409
1410 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1411 this._sendEncodings();
1412 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1413
1414 this._updateConnectionState('connected');
1415 return true;
1416 }
1417
1418 _sendEncodings() {
1419 const encs = [];
1420
1421 // In preference order
1422 encs.push(encodings.encodingCopyRect);
1423 // Only supported with full depth support
1424 if (this._fbDepth == 24) {
1425 encs.push(encodings.encodingTight);
1426 encs.push(encodings.encodingTightPNG);
1427 encs.push(encodings.encodingHextile);
1428 encs.push(encodings.encodingRRE);
1429 }
1430 encs.push(encodings.encodingRaw);
1431
1432 // Psuedo-encoding settings
1433 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1434 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1435
1436 encs.push(encodings.pseudoEncodingDesktopSize);
1437 encs.push(encodings.pseudoEncodingLastRect);
1438 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1439 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1440 encs.push(encodings.pseudoEncodingXvp);
1441 encs.push(encodings.pseudoEncodingFence);
1442 encs.push(encodings.pseudoEncodingContinuousUpdates);
1443 encs.push(encodings.pseudoEncodingDesktopName);
1444 encs.push(encodings.pseudoEncodingExtendedClipboard);
1445
1446 if (this._fbDepth == 24) {
1447 encs.push(encodings.pseudoEncodingVMwareCursor);
1448 encs.push(encodings.pseudoEncodingCursor);
1449 }
1450
1451 RFB.messages.clientEncodings(this._sock, encs);
1452 }
1453
1454 /* RFB protocol initialization states:
1455 * ProtocolVersion
1456 * Security
1457 * Authentication
1458 * SecurityResult
1459 * ClientInitialization - not triggered by server message
1460 * ServerInitialization
1461 */
1462 _init_msg() {
1463 switch (this._rfbInitState) {
1464 case 'ProtocolVersion':
1465 return this._negotiate_protocol_version();
1466
1467 case 'Security':
1468 return this._negotiate_security();
1469
1470 case 'Authentication':
1471 return this._negotiate_authentication();
1472
1473 case 'SecurityResult':
1474 return this._handle_security_result();
1475
1476 case 'SecurityReason':
1477 return this._handle_security_reason();
1478
1479 case 'ClientInitialisation':
1480 this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
1481 this._rfbInitState = 'ServerInitialisation';
1482 return true;
1483
1484 case 'ServerInitialisation':
1485 return this._negotiate_server_init();
1486
1487 default:
1488 return this._fail("Unknown init state (state: " +
1489 this._rfbInitState + ")");
1490 }
1491 }
1492
1493 _handle_set_colour_map_msg() {
1494 Log.Debug("SetColorMapEntries");
1495
1496 return this._fail("Unexpected SetColorMapEntries message");
1497 }
1498
1499 _handle_server_cut_text() {
1500 Log.Debug("ServerCutText");
1501
1502 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
1503
1504 this._sock.rQskipBytes(3); // Padding
1505
1506 let length = this._sock.rQshift32();
1507 length = toSigned32bit(length);
1508
1509 if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
1510
1511 if (length >= 0) {
1512 //Standard msg
1513 const text = this._sock.rQshiftStr(length);
1514 if (this._viewOnly) {
1515 return true;
1516 }
1517
1518 this.dispatchEvent(new CustomEvent(
1519 "clipboard",
1520 { detail: { text: text } }));
1521
1522 } else {
1523 //Extended msg.
1524 length = Math.abs(length);
1525 const flags = this._sock.rQshift32();
1526 let formats = flags & 0x0000FFFF;
1527 let actions = flags & 0xFF000000;
1528
1529 let isCaps = (!!(actions & extendedClipboardActionCaps));
1530 if (isCaps) {
1531 this._clipboardServerCapabilitiesFormats = {};
1532 this._clipboardServerCapabilitiesActions = {};
1533
1534 // Update our server capabilities for Formats
1535 for (let i = 0; i <= 15; i++) {
1536 let index = 1 << i;
1537
1538 // Check if format flag is set.
1539 if ((formats & index)) {
1540 this._clipboardServerCapabilitiesFormats[index] = true;
1541 // We don't send unsolicited clipboard, so we
1542 // ignore the size
1543 this._sock.rQshift32();
1544 }
1545 }
1546
1547 // Update our server capabilities for Actions
1548 for (let i = 24; i <= 31; i++) {
1549 let index = 1 << i;
1550 this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
1551 }
1552
1553 /* Caps handling done, send caps with the clients
1554 capabilities set as a response */
1555 let clientActions = [
1556 extendedClipboardActionCaps,
1557 extendedClipboardActionRequest,
1558 extendedClipboardActionPeek,
1559 extendedClipboardActionNotify,
1560 extendedClipboardActionProvide
1561 ];
1562 RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
1563
1564 } else if (actions === extendedClipboardActionRequest) {
1565 if (this._viewOnly) {
1566 return true;
1567 }
1568
1569 // Check if server has told us it can handle Provide and there is clipboard data to send.
1570 if (this._clipboardText != null &&
1571 this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
1572
1573 if (formats & extendedClipboardFormatText) {
1574 RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
1575 }
1576 }
1577
1578 } else if (actions === extendedClipboardActionPeek) {
1579 if (this._viewOnly) {
1580 return true;
1581 }
1582
1583 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
1584
1585 if (this._clipboardText != null) {
1586 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
1587 } else {
1588 RFB.messages.extendedClipboardNotify(this._sock, []);
1589 }
1590 }
1591
1592 } else if (actions === extendedClipboardActionNotify) {
1593 if (this._viewOnly) {
1594 return true;
1595 }
1596
1597 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
1598
1599 if (formats & extendedClipboardFormatText) {
1600 RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
1601 }
1602 }
1603
1604 } else if (actions === extendedClipboardActionProvide) {
1605 if (this._viewOnly) {
1606 return true;
1607 }
1608
1609 if (!(formats & extendedClipboardFormatText)) {
1610 return true;
1611 }
1612 // Ignore what we had in our clipboard client side.
1613 this._clipboardText = null;
1614
1615 // FIXME: Should probably verify that this data was actually requested
1616 let zlibStream = this._sock.rQshiftBytes(length - 4);
1617 let streamInflator = new Inflator();
1618 let textData = null;
1619
1620 streamInflator.setInput(zlibStream);
1621 for (let i = 0; i <= 15; i++) {
1622 let format = 1 << i;
1623
1624 if (formats & format) {
1625
1626 let size = 0x00;
1627 let sizeArray = streamInflator.inflate(4);
1628
1629 size |= (sizeArray[0] << 24);
1630 size |= (sizeArray[1] << 16);
1631 size |= (sizeArray[2] << 8);
1632 size |= (sizeArray[3]);
1633 let chunk = streamInflator.inflate(size);
1634
1635 if (format === extendedClipboardFormatText) {
1636 textData = chunk;
1637 }
1638 }
1639 }
1640 streamInflator.setInput(null);
1641
1642 if (textData !== null) {
1643 let tmpText = "";
1644 for (let i = 0; i < textData.length; i++) {
1645 tmpText += String.fromCharCode(textData[i]);
1646 }
1647 textData = tmpText;
1648
1649 textData = decodeUTF8(textData);
1650 if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
1651 textData = textData.slice(0, -1);
1652 }
1653
1654 textData = textData.replace("\r\n", "\n");
1655
1656 this.dispatchEvent(new CustomEvent(
1657 "clipboard",
1658 { detail: { text: textData } }));
1659 }
1660 } else {
1661 return this._fail("Unexpected action in extended clipboard message: " + actions);
1662 }
1663 }
1664 return true;
1665 }
1666
1667 _handle_server_fence_msg() {
1668 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
1669 this._sock.rQskipBytes(3); // Padding
1670 let flags = this._sock.rQshift32();
1671 let length = this._sock.rQshift8();
1672
1673 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
1674
1675 if (length > 64) {
1676 Log.Warn("Bad payload length (" + length + ") in fence response");
1677 length = 64;
1678 }
1679
1680 const payload = this._sock.rQshiftStr(length);
1681
1682 this._supportsFence = true;
1683
1684 /*
1685 * Fence flags
1686 *
1687 * (1<<0) - BlockBefore
1688 * (1<<1) - BlockAfter
1689 * (1<<2) - SyncNext
1690 * (1<<31) - Request
1691 */
1692
1693 if (!(flags & (1<<31))) {
1694 return this._fail("Unexpected fence response");
1695 }
1696
1697 // Filter out unsupported flags
1698 // FIXME: support syncNext
1699 flags &= (1<<0) | (1<<1);
1700
1701 // BlockBefore and BlockAfter are automatically handled by
1702 // the fact that we process each incoming message
1703 // synchronuosly.
1704 RFB.messages.clientFence(this._sock, flags, payload);
1705
1706 return true;
1707 }
1708
1709 _handle_xvp_msg() {
1710 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
1711 this._sock.rQskipBytes(1); // Padding
1712 const xvpVer = this._sock.rQshift8();
1713 const xvpMsg = this._sock.rQshift8();
1714
1715 switch (xvpMsg) {
1716 case 0: // XVP_FAIL
1717 Log.Error("XVP Operation Failed");
1718 break;
1719 case 1: // XVP_INIT
1720 this._rfbXvpVer = xvpVer;
1721 Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
1722 this._setCapability("power", true);
1723 break;
1724 default:
1725 this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
1726 break;
1727 }
1728
1729 return true;
1730 }
1731
1732 _normal_msg() {
1733 let msgType;
1734 if (this._FBU.rects > 0) {
1735 msgType = 0;
1736 } else {
1737 msgType = this._sock.rQshift8();
1738 }
1739
1740 let first, ret;
1741 switch (msgType) {
1742 case 0: // FramebufferUpdate
1743 ret = this._framebufferUpdate();
1744 if (ret && !this._enabledContinuousUpdates) {
1745 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
1746 this._fbWidth, this._fbHeight);
1747 }
1748 return ret;
1749
1750 case 1: // SetColorMapEntries
1751 return this._handle_set_colour_map_msg();
1752
1753 case 2: // Bell
1754 Log.Debug("Bell");
1755 this.dispatchEvent(new CustomEvent(
1756 "bell",
1757 { detail: {} }));
1758 return true;
1759
1760 case 3: // ServerCutText
1761 return this._handle_server_cut_text();
1762
1763 case 150: // EndOfContinuousUpdates
1764 first = !this._supportsContinuousUpdates;
1765 this._supportsContinuousUpdates = true;
1766 this._enabledContinuousUpdates = false;
1767 if (first) {
1768 this._enabledContinuousUpdates = true;
1769 this._updateContinuousUpdates();
1770 Log.Info("Enabling continuous updates.");
1771 } else {
1772 // FIXME: We need to send a framebufferupdaterequest here
1773 // if we add support for turning off continuous updates
1774 }
1775 return true;
1776
1777 case 248: // ServerFence
1778 return this._handle_server_fence_msg();
1779
1780 case 250: // XVP
1781 return this._handle_xvp_msg();
1782
1783 default:
1784 this._fail("Unexpected server message (type " + msgType + ")");
1785 Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
1786 return true;
1787 }
1788 }
1789
1790 _onFlush() {
1791 this._flushing = false;
1792 // Resume processing
1793 if (this._sock.rQlen > 0) {
1794 this._handle_message();
1795 }
1796 }
1797
1798 _framebufferUpdate() {
1799 if (this._FBU.rects === 0) {
1800 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
1801 this._sock.rQskipBytes(1); // Padding
1802 this._FBU.rects = this._sock.rQshift16();
1803
1804 // Make sure the previous frame is fully rendered first
1805 // to avoid building up an excessive queue
1806 if (this._display.pending()) {
1807 this._flushing = true;
1808 this._display.flush();
1809 return false;
1810 }
1811 }
1812
1813 while (this._FBU.rects > 0) {
1814 if (this._FBU.encoding === null) {
1815 if (this._sock.rQwait("rect header", 12)) { return false; }
1816 /* New FramebufferUpdate */
1817
1818 const hdr = this._sock.rQshiftBytes(12);
1819 this._FBU.x = (hdr[0] << 8) + hdr[1];
1820 this._FBU.y = (hdr[2] << 8) + hdr[3];
1821 this._FBU.width = (hdr[4] << 8) + hdr[5];
1822 this._FBU.height = (hdr[6] << 8) + hdr[7];
1823 this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
1824 (hdr[10] << 8) + hdr[11], 10);
1825 }
1826
1827 if (!this._handleRect()) {
1828 return false;
1829 }
1830
1831 this._FBU.rects--;
1832 this._FBU.encoding = null;
1833 }
1834
1835 this._display.flip();
1836
1837 return true; // We finished this FBU
1838 }
1839
1840 _handleRect() {
1841 switch (this._FBU.encoding) {
1842 case encodings.pseudoEncodingLastRect:
1843 this._FBU.rects = 1; // Will be decreased when we return
1844 return true;
1845
1846 case encodings.pseudoEncodingVMwareCursor:
1847 return this._handleVMwareCursor();
1848
1849 case encodings.pseudoEncodingCursor:
1850 return this._handleCursor();
1851
1852 case encodings.pseudoEncodingQEMUExtendedKeyEvent:
1853 // Old Safari doesn't support creating keyboard events
1854 try {
1855 const keyboardEvent = document.createEvent("keyboardEvent");
1856 if (keyboardEvent.code !== undefined) {
1857 this._qemuExtKeyEventSupported = true;
1858 }
1859 } catch (err) {
1860 // Do nothing
1861 }
1862 return true;
1863
1864 case encodings.pseudoEncodingDesktopName:
1865 return this._handleDesktopName();
1866
1867 case encodings.pseudoEncodingDesktopSize:
1868 this._resize(this._FBU.width, this._FBU.height);
1869 return true;
1870
1871 case encodings.pseudoEncodingExtendedDesktopSize:
1872 return this._handleExtendedDesktopSize();
1873
1874 default:
1875 return this._handleDataRect();
1876 }
1877 }
1878
1879 _handleVMwareCursor() {
1880 const hotx = this._FBU.x; // hotspot-x
1881 const hoty = this._FBU.y; // hotspot-y
1882 const w = this._FBU.width;
1883 const h = this._FBU.height;
1884 if (this._sock.rQwait("VMware cursor encoding", 1)) {
1885 return false;
1886 }
1887
1888 const cursorType = this._sock.rQshift8();
1889
1890 this._sock.rQshift8(); //Padding
1891
1892 let rgba;
1893 const bytesPerPixel = 4;
1894
1895 //Classic cursor
1896 if (cursorType == 0) {
1897 //Used to filter away unimportant bits.
1898 //OR is used for correct conversion in js.
1899 const PIXEL_MASK = 0xffffff00 | 0;
1900 rgba = new Array(w * h * bytesPerPixel);
1901
1902 if (this._sock.rQwait("VMware cursor classic encoding",
1903 (w * h * bytesPerPixel) * 2, 2)) {
1904 return false;
1905 }
1906
1907 let andMask = new Array(w * h);
1908 for (let pixel = 0; pixel < (w * h); pixel++) {
1909 andMask[pixel] = this._sock.rQshift32();
1910 }
1911
1912 let xorMask = new Array(w * h);
1913 for (let pixel = 0; pixel < (w * h); pixel++) {
1914 xorMask[pixel] = this._sock.rQshift32();
1915 }
1916
1917 for (let pixel = 0; pixel < (w * h); pixel++) {
1918 if (andMask[pixel] == 0) {
1919 //Fully opaque pixel
1920 let bgr = xorMask[pixel];
1921 let r = bgr >> 8 & 0xff;
1922 let g = bgr >> 16 & 0xff;
1923 let b = bgr >> 24 & 0xff;
1924
1925 rgba[(pixel * bytesPerPixel) ] = r; //r
1926 rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
1927 rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
1928 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
1929
1930 } else if ((andMask[pixel] & PIXEL_MASK) ==
1931 PIXEL_MASK) {
1932 //Only screen value matters, no mouse colouring
1933 if (xorMask[pixel] == 0) {
1934 //Transparent pixel
1935 rgba[(pixel * bytesPerPixel) ] = 0x00;
1936 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1937 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1938 rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
1939
1940 } else if ((xorMask[pixel] & PIXEL_MASK) ==
1941 PIXEL_MASK) {
1942 //Inverted pixel, not supported in browsers.
1943 //Fully opaque instead.
1944 rgba[(pixel * bytesPerPixel) ] = 0x00;
1945 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1946 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1947 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1948
1949 } else {
1950 //Unhandled xorMask
1951 rgba[(pixel * bytesPerPixel) ] = 0x00;
1952 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1953 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1954 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1955 }
1956
1957 } else {
1958 //Unhandled andMask
1959 rgba[(pixel * bytesPerPixel) ] = 0x00;
1960 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
1961 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
1962 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
1963 }
1964 }
1965
1966 //Alpha cursor.
1967 } else if (cursorType == 1) {
1968 if (this._sock.rQwait("VMware cursor alpha encoding",
1969 (w * h * 4), 2)) {
1970 return false;
1971 }
1972
1973 rgba = new Array(w * h * bytesPerPixel);
1974
1975 for (let pixel = 0; pixel < (w * h); pixel++) {
1976 let data = this._sock.rQshift32();
1977
1978 rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
1979 rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
1980 rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
1981 rgba[(pixel * 4) + 3 ] = data & 0xff; //a
1982 }
1983
1984 } else {
1985 Log.Warn("The given cursor type is not supported: "
1986 + cursorType + " given.");
1987 return false;
1988 }
1989
1990 this._updateCursor(rgba, hotx, hoty, w, h);
1991
1992 return true;
1993 }
1994
1995 _handleCursor() {
1996 const hotx = this._FBU.x; // hotspot-x
1997 const hoty = this._FBU.y; // hotspot-y
1998 const w = this._FBU.width;
1999 const h = this._FBU.height;
2000
2001 const pixelslength = w * h * 4;
2002 const masklength = Math.ceil(w / 8) * h;
2003
2004 let bytes = pixelslength + masklength;
2005 if (this._sock.rQwait("cursor encoding", bytes)) {
2006 return false;
2007 }
2008
2009 // Decode from BGRX pixels + bit mask to RGBA
2010 const pixels = this._sock.rQshiftBytes(pixelslength);
2011 const mask = this._sock.rQshiftBytes(masklength);
2012 let rgba = new Uint8Array(w * h * 4);
2013
2014 let pixIdx = 0;
2015 for (let y = 0; y < h; y++) {
2016 for (let x = 0; x < w; x++) {
2017 let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
2018 let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
2019 rgba[pixIdx ] = pixels[pixIdx + 2];
2020 rgba[pixIdx + 1] = pixels[pixIdx + 1];
2021 rgba[pixIdx + 2] = pixels[pixIdx];
2022 rgba[pixIdx + 3] = alpha;
2023 pixIdx += 4;
2024 }
2025 }
2026
2027 this._updateCursor(rgba, hotx, hoty, w, h);
2028
2029 return true;
2030 }
2031
2032 _handleDesktopName() {
2033 if (this._sock.rQwait("DesktopName", 4)) {
2034 return false;
2035 }
2036
2037 let length = this._sock.rQshift32();
2038
2039 if (this._sock.rQwait("DesktopName", length, 4)) {
2040 return false;
2041 }
2042
2043 let name = this._sock.rQshiftStr(length);
2044 name = decodeUTF8(name, true);
2045
2046 this._setDesktopName(name);
2047
2048 return true;
2049 }
2050
2051 _handleExtendedDesktopSize() {
2052 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
2053 return false;
2054 }
2055
2056 const numberOfScreens = this._sock.rQpeek8();
2057
2058 let bytes = 4 + (numberOfScreens * 16);
2059 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
2060 return false;
2061 }
2062
2063 const firstUpdate = !this._supportsSetDesktopSize;
2064 this._supportsSetDesktopSize = true;
2065
2066 // Normally we only apply the current resize mode after a
2067 // window resize event. However there is no such trigger on the
2068 // initial connect. And we don't know if the server supports
2069 // resizing until we've gotten here.
2070 if (firstUpdate) {
2071 this._requestRemoteResize();
2072 }
2073
2074 this._sock.rQskipBytes(1); // number-of-screens
2075 this._sock.rQskipBytes(3); // padding
2076
2077 for (let i = 0; i < numberOfScreens; i += 1) {
2078 // Save the id and flags of the first screen
2079 if (i === 0) {
2080 this._screenID = this._sock.rQshiftBytes(4); // id
2081 this._sock.rQskipBytes(2); // x-position
2082 this._sock.rQskipBytes(2); // y-position
2083 this._sock.rQskipBytes(2); // width
2084 this._sock.rQskipBytes(2); // height
2085 this._screenFlags = this._sock.rQshiftBytes(4); // flags
2086 } else {
2087 this._sock.rQskipBytes(16);
2088 }
2089 }
2090
2091 /*
2092 * The x-position indicates the reason for the change:
2093 *
2094 * 0 - server resized on its own
2095 * 1 - this client requested the resize
2096 * 2 - another client requested the resize
2097 */
2098
2099 // We need to handle errors when we requested the resize.
2100 if (this._FBU.x === 1 && this._FBU.y !== 0) {
2101 let msg = "";
2102 // The y-position indicates the status code from the server
2103 switch (this._FBU.y) {
2104 case 1:
2105 msg = "Resize is administratively prohibited";
2106 break;
2107 case 2:
2108 msg = "Out of resources";
2109 break;
2110 case 3:
2111 msg = "Invalid screen layout";
2112 break;
2113 default:
2114 msg = "Unknown reason";
2115 break;
2116 }
2117 Log.Warn("Server did not accept the resize request: "
2118 + msg);
2119 } else {
2120 this._resize(this._FBU.width, this._FBU.height);
2121 }
2122
2123 return true;
2124 }
2125
2126 _handleDataRect() {
2127 let decoder = this._decoders[this._FBU.encoding];
2128 if (!decoder) {
2129 this._fail("Unsupported encoding (encoding: " +
2130 this._FBU.encoding + ")");
2131 return false;
2132 }
2133
2134 try {
2135 return decoder.decodeRect(this._FBU.x, this._FBU.y,
2136 this._FBU.width, this._FBU.height,
2137 this._sock, this._display,
2138 this._fbDepth);
2139 } catch (err) {
2140 this._fail("Error decoding rect: " + err);
2141 return false;
2142 }
2143 }
2144
2145 _updateContinuousUpdates() {
2146 if (!this._enabledContinuousUpdates) { return; }
2147
2148 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
2149 this._fbWidth, this._fbHeight);
2150 }
2151
2152 _resize(width, height) {
2153 this._fbWidth = width;
2154 this._fbHeight = height;
2155
2156 this._display.resize(this._fbWidth, this._fbHeight);
2157
2158 // Adjust the visible viewport based on the new dimensions
2159 this._updateClip();
2160 this._updateScale();
2161
2162 this._updateContinuousUpdates();
2163 }
2164
2165 _xvpOp(ver, op) {
2166 if (this._rfbXvpVer < ver) { return; }
2167 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
2168 RFB.messages.xvpOp(this._sock, ver, op);
2169 }
2170
2171 _updateCursor(rgba, hotx, hoty, w, h) {
2172 this._cursorImage = {
2173 rgbaPixels: rgba,
2174 hotx: hotx, hoty: hoty, w: w, h: h,
2175 };
2176 this._refreshCursor();
2177 }
2178
2179 _shouldShowDotCursor() {
2180 // Called when this._cursorImage is updated
2181 if (!this._showDotCursor) {
2182 // User does not want to see the dot, so...
2183 return false;
2184 }
2185
2186 // The dot should not be shown if the cursor is already visible,
2187 // i.e. contains at least one not-fully-transparent pixel.
2188 // So iterate through all alpha bytes in rgba and stop at the
2189 // first non-zero.
2190 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
2191 if (this._cursorImage.rgbaPixels[i]) {
2192 return false;
2193 }
2194 }
2195
2196 // At this point, we know that the cursor is fully transparent, and
2197 // the user wants to see the dot instead of this.
2198 return true;
2199 }
2200
2201 _refreshCursor() {
2202 if (this._rfbConnectionState !== "connecting" &&
2203 this._rfbConnectionState !== "connected") {
2204 return;
2205 }
2206 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
2207 this._cursor.change(image.rgbaPixels,
2208 image.hotx, image.hoty,
2209 image.w, image.h
2210 );
2211 }
2212
2213 static genDES(password, challenge) {
2214 const passwordChars = password.split('').map(c => c.charCodeAt(0));
2215 return (new DES(passwordChars)).encrypt(challenge);
2216 }
2217 }
2218
2219 // Class Methods
2220 RFB.messages = {
2221 keyEvent(sock, keysym, down) {
2222 const buff = sock._sQ;
2223 const offset = sock._sQlen;
2224
2225 buff[offset] = 4; // msg-type
2226 buff[offset + 1] = down;
2227
2228 buff[offset + 2] = 0;
2229 buff[offset + 3] = 0;
2230
2231 buff[offset + 4] = (keysym >> 24);
2232 buff[offset + 5] = (keysym >> 16);
2233 buff[offset + 6] = (keysym >> 8);
2234 buff[offset + 7] = keysym;
2235
2236 sock._sQlen += 8;
2237 sock.flush();
2238 },
2239
2240 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
2241 function getRFBkeycode(xtScanCode) {
2242 const upperByte = (keycode >> 8);
2243 const lowerByte = (keycode & 0x00ff);
2244 if (upperByte === 0xe0 && lowerByte < 0x7f) {
2245 return lowerByte | 0x80;
2246 }
2247 return xtScanCode;
2248 }
2249
2250 const buff = sock._sQ;
2251 const offset = sock._sQlen;
2252
2253 buff[offset] = 255; // msg-type
2254 buff[offset + 1] = 0; // sub msg-type
2255
2256 buff[offset + 2] = (down >> 8);
2257 buff[offset + 3] = down;
2258
2259 buff[offset + 4] = (keysym >> 24);
2260 buff[offset + 5] = (keysym >> 16);
2261 buff[offset + 6] = (keysym >> 8);
2262 buff[offset + 7] = keysym;
2263
2264 const RFBkeycode = getRFBkeycode(keycode);
2265
2266 buff[offset + 8] = (RFBkeycode >> 24);
2267 buff[offset + 9] = (RFBkeycode >> 16);
2268 buff[offset + 10] = (RFBkeycode >> 8);
2269 buff[offset + 11] = RFBkeycode;
2270
2271 sock._sQlen += 12;
2272 sock.flush();
2273 },
2274
2275 pointerEvent(sock, x, y, mask) {
2276 const buff = sock._sQ;
2277 const offset = sock._sQlen;
2278
2279 buff[offset] = 5; // msg-type
2280
2281 buff[offset + 1] = mask;
2282
2283 buff[offset + 2] = x >> 8;
2284 buff[offset + 3] = x;
2285
2286 buff[offset + 4] = y >> 8;
2287 buff[offset + 5] = y;
2288
2289 sock._sQlen += 6;
2290 sock.flush();
2291 },
2292
2293 // Used to build Notify and Request data.
2294 _buildExtendedClipboardFlags(actions, formats) {
2295 let data = new Uint8Array(4);
2296 let formatFlag = 0x00000000;
2297 let actionFlag = 0x00000000;
2298
2299 for (let i = 0; i < actions.length; i++) {
2300 actionFlag |= actions[i];
2301 }
2302
2303 for (let i = 0; i < formats.length; i++) {
2304 formatFlag |= formats[i];
2305 }
2306
2307 data[0] = actionFlag >> 24; // Actions
2308 data[1] = 0x00; // Reserved
2309 data[2] = 0x00; // Reserved
2310 data[3] = formatFlag; // Formats
2311
2312 return data;
2313 },
2314
2315 extendedClipboardProvide(sock, formats, inData) {
2316 // Deflate incomming data and their sizes
2317 let deflator = new Deflator();
2318 let dataToDeflate = [];
2319
2320 for (let i = 0; i < formats.length; i++) {
2321 // We only support the format Text at this time
2322 if (formats[i] != extendedClipboardFormatText) {
2323 throw new Error("Unsupported extended clipboard format for Provide message.");
2324 }
2325
2326 // Change lone \r or \n into \r\n as defined in rfbproto
2327 inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
2328
2329 // Check if it already has \0
2330 let text = encodeUTF8(inData[i] + "\0");
2331
2332 dataToDeflate.push( (text.length >> 24) & 0xFF,
2333 (text.length >> 16) & 0xFF,
2334 (text.length >> 8) & 0xFF,
2335 (text.length & 0xFF));
2336
2337 for (let j = 0; j < text.length; j++) {
2338 dataToDeflate.push(text.charCodeAt(j));
2339 }
2340 }
2341
2342 let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
2343
2344 // Build data to send
2345 let data = new Uint8Array(4 + deflatedData.length);
2346 data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
2347 formats));
2348 data.set(deflatedData, 4);
2349
2350 RFB.messages.clientCutText(sock, data, true);
2351 },
2352
2353 extendedClipboardNotify(sock, formats) {
2354 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
2355 formats);
2356 RFB.messages.clientCutText(sock, flags, true);
2357 },
2358
2359 extendedClipboardRequest(sock, formats) {
2360 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
2361 formats);
2362 RFB.messages.clientCutText(sock, flags, true);
2363 },
2364
2365 extendedClipboardCaps(sock, actions, formats) {
2366 let formatKeys = Object.keys(formats);
2367 let data = new Uint8Array(4 + (4 * formatKeys.length));
2368
2369 formatKeys.map(x => parseInt(x));
2370 formatKeys.sort((a, b) => a - b);
2371
2372 data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
2373
2374 let loopOffset = 4;
2375 for (let i = 0; i < formatKeys.length; i++) {
2376 data[loopOffset] = formats[formatKeys[i]] >> 24;
2377 data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
2378 data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
2379 data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
2380
2381 loopOffset += 4;
2382 data[3] |= (1 << formatKeys[i]); // Update our format flags
2383 }
2384
2385 RFB.messages.clientCutText(sock, data, true);
2386 },
2387
2388 clientCutText(sock, data, extended = false) {
2389 const buff = sock._sQ;
2390 const offset = sock._sQlen;
2391
2392 buff[offset] = 6; // msg-type
2393
2394 buff[offset + 1] = 0; // padding
2395 buff[offset + 2] = 0; // padding
2396 buff[offset + 3] = 0; // padding
2397
2398 let length;
2399 if (extended) {
2400 length = toUnsigned32bit(-data.length);
2401 } else {
2402 length = data.length;
2403 }
2404
2405 buff[offset + 4] = length >> 24;
2406 buff[offset + 5] = length >> 16;
2407 buff[offset + 6] = length >> 8;
2408 buff[offset + 7] = length;
2409
2410 sock._sQlen += 8;
2411
2412 // We have to keep track of from where in the data we begin creating the
2413 // buffer for the flush in the next iteration.
2414 let dataOffset = 0;
2415
2416 let remaining = data.length;
2417 while (remaining > 0) {
2418
2419 let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
2420 for (let i = 0; i < flushSize; i++) {
2421 buff[sock._sQlen + i] = data[dataOffset + i];
2422 }
2423
2424 sock._sQlen += flushSize;
2425 sock.flush();
2426
2427 remaining -= flushSize;
2428 dataOffset += flushSize;
2429 }
2430
2431 },
2432
2433 setDesktopSize(sock, width, height, id, flags) {
2434 const buff = sock._sQ;
2435 const offset = sock._sQlen;
2436
2437 buff[offset] = 251; // msg-type
2438 buff[offset + 1] = 0; // padding
2439 buff[offset + 2] = width >> 8; // width
2440 buff[offset + 3] = width;
2441 buff[offset + 4] = height >> 8; // height
2442 buff[offset + 5] = height;
2443
2444 buff[offset + 6] = 1; // number-of-screens
2445 buff[offset + 7] = 0; // padding
2446
2447 // screen array
2448 buff[offset + 8] = id >> 24; // id
2449 buff[offset + 9] = id >> 16;
2450 buff[offset + 10] = id >> 8;
2451 buff[offset + 11] = id;
2452 buff[offset + 12] = 0; // x-position
2453 buff[offset + 13] = 0;
2454 buff[offset + 14] = 0; // y-position
2455 buff[offset + 15] = 0;
2456 buff[offset + 16] = width >> 8; // width
2457 buff[offset + 17] = width;
2458 buff[offset + 18] = height >> 8; // height
2459 buff[offset + 19] = height;
2460 buff[offset + 20] = flags >> 24; // flags
2461 buff[offset + 21] = flags >> 16;
2462 buff[offset + 22] = flags >> 8;
2463 buff[offset + 23] = flags;
2464
2465 sock._sQlen += 24;
2466 sock.flush();
2467 },
2468
2469 clientFence(sock, flags, payload) {
2470 const buff = sock._sQ;
2471 const offset = sock._sQlen;
2472
2473 buff[offset] = 248; // msg-type
2474
2475 buff[offset + 1] = 0; // padding
2476 buff[offset + 2] = 0; // padding
2477 buff[offset + 3] = 0; // padding
2478
2479 buff[offset + 4] = flags >> 24; // flags
2480 buff[offset + 5] = flags >> 16;
2481 buff[offset + 6] = flags >> 8;
2482 buff[offset + 7] = flags;
2483
2484 const n = payload.length;
2485
2486 buff[offset + 8] = n; // length
2487
2488 for (let i = 0; i < n; i++) {
2489 buff[offset + 9 + i] = payload.charCodeAt(i);
2490 }
2491
2492 sock._sQlen += 9 + n;
2493 sock.flush();
2494 },
2495
2496 enableContinuousUpdates(sock, enable, x, y, width, height) {
2497 const buff = sock._sQ;
2498 const offset = sock._sQlen;
2499
2500 buff[offset] = 150; // msg-type
2501 buff[offset + 1] = enable; // enable-flag
2502
2503 buff[offset + 2] = x >> 8; // x
2504 buff[offset + 3] = x;
2505 buff[offset + 4] = y >> 8; // y
2506 buff[offset + 5] = y;
2507 buff[offset + 6] = width >> 8; // width
2508 buff[offset + 7] = width;
2509 buff[offset + 8] = height >> 8; // height
2510 buff[offset + 9] = height;
2511
2512 sock._sQlen += 10;
2513 sock.flush();
2514 },
2515
2516 pixelFormat(sock, depth, trueColor) {
2517 const buff = sock._sQ;
2518 const offset = sock._sQlen;
2519
2520 let bpp;
2521
2522 if (depth > 16) {
2523 bpp = 32;
2524 } else if (depth > 8) {
2525 bpp = 16;
2526 } else {
2527 bpp = 8;
2528 }
2529
2530 const bits = Math.floor(depth/3);
2531
2532 buff[offset] = 0; // msg-type
2533
2534 buff[offset + 1] = 0; // padding
2535 buff[offset + 2] = 0; // padding
2536 buff[offset + 3] = 0; // padding
2537
2538 buff[offset + 4] = bpp; // bits-per-pixel
2539 buff[offset + 5] = depth; // depth
2540 buff[offset + 6] = 0; // little-endian
2541 buff[offset + 7] = trueColor ? 1 : 0; // true-color
2542
2543 buff[offset + 8] = 0; // red-max
2544 buff[offset + 9] = (1 << bits) - 1; // red-max
2545
2546 buff[offset + 10] = 0; // green-max
2547 buff[offset + 11] = (1 << bits) - 1; // green-max
2548
2549 buff[offset + 12] = 0; // blue-max
2550 buff[offset + 13] = (1 << bits) - 1; // blue-max
2551
2552 buff[offset + 14] = bits * 2; // red-shift
2553 buff[offset + 15] = bits * 1; // green-shift
2554 buff[offset + 16] = bits * 0; // blue-shift
2555
2556 buff[offset + 17] = 0; // padding
2557 buff[offset + 18] = 0; // padding
2558 buff[offset + 19] = 0; // padding
2559
2560 sock._sQlen += 20;
2561 sock.flush();
2562 },
2563
2564 clientEncodings(sock, encodings) {
2565 const buff = sock._sQ;
2566 const offset = sock._sQlen;
2567
2568 buff[offset] = 2; // msg-type
2569 buff[offset + 1] = 0; // padding
2570
2571 buff[offset + 2] = encodings.length >> 8;
2572 buff[offset + 3] = encodings.length;
2573
2574 let j = offset + 4;
2575 for (let i = 0; i < encodings.length; i++) {
2576 const enc = encodings[i];
2577 buff[j] = enc >> 24;
2578 buff[j + 1] = enc >> 16;
2579 buff[j + 2] = enc >> 8;
2580 buff[j + 3] = enc;
2581
2582 j += 4;
2583 }
2584
2585 sock._sQlen += j - offset;
2586 sock.flush();
2587 },
2588
2589 fbUpdateRequest(sock, incremental, x, y, w, h) {
2590 const buff = sock._sQ;
2591 const offset = sock._sQlen;
2592
2593 if (typeof(x) === "undefined") { x = 0; }
2594 if (typeof(y) === "undefined") { y = 0; }
2595
2596 buff[offset] = 3; // msg-type
2597 buff[offset + 1] = incremental ? 1 : 0;
2598
2599 buff[offset + 2] = (x >> 8) & 0xFF;
2600 buff[offset + 3] = x & 0xFF;
2601
2602 buff[offset + 4] = (y >> 8) & 0xFF;
2603 buff[offset + 5] = y & 0xFF;
2604
2605 buff[offset + 6] = (w >> 8) & 0xFF;
2606 buff[offset + 7] = w & 0xFF;
2607
2608 buff[offset + 8] = (h >> 8) & 0xFF;
2609 buff[offset + 9] = h & 0xFF;
2610
2611 sock._sQlen += 10;
2612 sock.flush();
2613 },
2614
2615 xvpOp(sock, ver, op) {
2616 const buff = sock._sQ;
2617 const offset = sock._sQlen;
2618
2619 buff[offset] = 250; // msg-type
2620 buff[offset + 1] = 0; // padding
2621
2622 buff[offset + 2] = ver;
2623 buff[offset + 3] = op;
2624
2625 sock._sQlen += 4;
2626 sock.flush();
2627 }
2628 };
2629
2630 RFB.cursors = {
2631 none: {
2632 rgbaPixels: new Uint8Array(),
2633 w: 0, h: 0,
2634 hotx: 0, hoty: 0,
2635 },
2636
2637 dot: {
2638 /* eslint-disable indent */
2639 rgbaPixels: new Uint8Array([
2640 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2641 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2642 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2643 ]),
2644 /* eslint-enable indent */
2645 w: 3, h: 3,
2646 hotx: 1, hoty: 1,
2647 }
2648 };