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