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