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