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