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