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