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