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