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