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