| /* |
| * noVNC: HTML5 VNC client |
| * Copyright (C) 2012 Joel Martin |
| * Copyright (C) 2013 Samuel Mannehed for Cendio AB |
| * Licensed under MPL 2.0 (see LICENSE.txt) |
| * |
| * See README.md for usage and integration instructions. |
| * |
| * TIGHT decoder portion: |
| * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) |
| */ |
| |
| /*jslint white: false, browser: true */ |
| /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ |
| |
| var RFB; |
| |
| (function () { |
| "use strict"; |
| RFB = function (defaults) { |
| if (!defaults) { |
| defaults = {}; |
| } |
| |
| this._rfb_host = ''; |
| this._rfb_port = 5900; |
| this._rfb_password = ''; |
| this._rfb_path = ''; |
| |
| this._rfb_state = 'disconnected'; |
| this._rfb_version = 0; |
| this._rfb_max_version = 3.8; |
| this._rfb_auth_scheme = ''; |
| |
| this._rfb_tightvnc = false; |
| this._rfb_xvp_ver = 0; |
| |
| // In preference order |
| this._encodings = [ |
| ['COPYRECT', 0x01 ], |
| ['TIGHT', 0x07 ], |
| ['TIGHT_PNG', -260 ], |
| ['HEXTILE', 0x05 ], |
| ['RRE', 0x02 ], |
| ['RAW', 0x00 ], |
| ['DesktopSize', -223 ], |
| ['Cursor', -239 ], |
| |
| // Psuedo-encoding settings |
| //['JPEG_quality_lo', -32 ], |
| ['JPEG_quality_med', -26 ], |
| //['JPEG_quality_hi', -23 ], |
| //['compress_lo', -255 ], |
| ['compress_hi', -247 ], |
| ['last_rect', -224 ], |
| ['xvp', -309 ] |
| ]; |
| |
| this._encHandlers = {}; |
| this._encNames = {}; |
| this._encStats = {}; |
| |
| this._sock = null; // Websock object |
| this._display = null; // Display object |
| this._keyboard = null; // Keyboard input handler object |
| this._mouse = null; // Mouse input handler object |
| this._sendTimer = null; // Send Queue check timer |
| this._disconnTimer = null; // disconnection timer |
| this._msgTimer = null; // queued handle_msg timer |
| |
| // Frame buffer update state |
| this._FBU = { |
| rects: 0, |
| subrects: 0, // RRE |
| lines: 0, // RAW |
| tiles: 0, // HEXTILE |
| bytes: 0, |
| x: 0, |
| y: 0, |
| width: 0, |
| height: 0, |
| encoding: 0, |
| subencoding: -1, |
| background: null, |
| zlib: [] // TIGHT zlib streams |
| }; |
| |
| this._fb_Bpp = 4; |
| this._fb_depth = 3; |
| this._fb_width = 0; |
| this._fb_height = 0; |
| this._fb_name = ""; |
| |
| this._rre_chunk_sz = 100; |
| |
| this._timing = { |
| last_fbu: 0, |
| fbu_total: 0, |
| fbu_total_cnt: 0, |
| full_fbu_total: 0, |
| full_fbu_cnt: 0, |
| |
| fbu_rt_start: 0, |
| fbu_rt_total: 0, |
| fbu_rt_cnt: 0, |
| pixels: 0 |
| }; |
| |
| // Mouse state |
| this._mouse_buttonMask = 0; |
| this._mouse_arr = []; |
| this._viewportDragging = false; |
| this._viewportDragPos = {}; |
| |
| // set the default value on user-facing properties |
| Util.set_defaults(this, defaults, { |
| 'target': 'null', // VNC display rendering Canvas object |
| 'focusContainer': document, // DOM element that captures keyboard input |
| 'encrypt': false, // Use TLS/SSL/wss encryption |
| 'true_color': true, // Request true color pixel data |
| 'local_cursor': false, // Request locally rendered cursor |
| 'shared': true, // Request shared mode |
| 'view_only': false, // Disable client mouse/keyboard |
| 'xvp_password_sep': '@', // Separator for XVP password fields |
| 'disconnectTimeout': 3, // Time (s) to wait for disconnection |
| 'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection |
| 'repeaterID': '', // [UltraVNC] RepeaterID to connect to |
| 'viewportDrag': false, // Move the viewport on mouse drags |
| |
| // Callback functions |
| 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change |
| 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required |
| 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received |
| 'onBell': function () { }, // onBell(rfb): RFB Bell message received |
| 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed |
| 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed |
| 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized |
| 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received |
| 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection |
| }); |
| |
| // main setup |
| Util.Debug(">> RFB.constructor"); |
| |
| // populate encHandlers with bound versions |
| Object.keys(RFB.encodingHandlers).forEach(function (encName) { |
| this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this); |
| }.bind(this)); |
| |
| // Create lookup tables based on encoding number |
| for (var i = 0; i < this._encodings.length; i++) { |
| this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]]; |
| this._encNames[this._encodings[i][1]] = this._encodings[i][0]; |
| this._encStats[this._encodings[i][1]] = [0, 0]; |
| } |
| |
| try { |
| this._display = new Display({target: this._target}); |
| } catch (exc) { |
| Util.Error("Display exception: " + exc); |
| this._updateState('fatal', "No working Display"); |
| } |
| |
| this._keyboard = new Keyboard({target: this._focusContainer, |
| onKeyPress: this._handleKeyPress.bind(this)}); |
| |
| this._mouse = new Mouse({target: this._target, |
| onMouseButton: this._handleMouseButton.bind(this), |
| onMouseMove: this._handleMouseMove.bind(this), |
| notify: this._keyboard.sync.bind(this._keyboard)}); |
| |
| this._sock = new Websock(); |
| this._sock.on('message', this._handle_message.bind(this)); |
| this._sock.on('open', function () { |
| if (this._rfb_state === 'connect') { |
| this._updateState('ProtocolVersion', "Starting VNC handshake"); |
| } else { |
| this._fail("Got unexpected WebSocket connection"); |
| } |
| }.bind(this)); |
| this._sock.on('close', function (e) { |
| Util.Warn("WebSocket on-close event"); |
| var msg = ""; |
| if (e.code) { |
| msg = " (code: " + e.code; |
| if (e.reason) { |
| msg += ", reason: " + e.reason; |
| } |
| msg += ")"; |
| } |
| if (this._rfb_state === 'disconnect') { |
| this._updateState('disconnected', 'VNC disconnected' + msg); |
| } else if (this._rfb_state === 'ProtocolVersion') { |
| this._fail('Failed to connect to server' + msg); |
| } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { |
| Util.Error("Received onclose while disconnected" + msg); |
| } else { |
| this._fail("Server disconnected" + msg); |
| } |
| }.bind(this)); |
| this._sock.on('error', function (e) { |
| Util.Warn("WebSocket on-error event"); |
| }); |
| |
| this._init_vars(); |
| |
| var rmode = this._display.get_render_mode(); |
| if (Websock_native) { |
| Util.Info("Using native WebSockets"); |
| this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); |
| } else { |
| Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); |
| if (!Util.Flash || Util.Flash.version < 9) { |
| this._updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> is required"); |
| } else if (document.location.href.substr(0, 7) === 'file://') { |
| this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); |
| } else { |
| this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); |
| } |
| } |
| |
| Util.Debug("<< RFB.constructor"); |
| }; |
| |
| RFB.prototype = { |
| // Public methods |
| connect: function (host, port, password, path) { |
| this._rfb_host = host; |
| this._rfb_port = port; |
| this._rfb_password = (password !== undefined) ? password : ""; |
| this._rfb_path = (path !== undefined) ? path : ""; |
| |
| if (!this._rfb_host || !this._rfb_port) { |
| return this._fail("Must set host and port"); |
| } |
| |
| this._updateState('connect'); |
| }, |
| |
| disconnect: function () { |
| this._updateState('disconnect', 'Disconnecting'); |
| }, |
| |
| sendPassword: function (passwd) { |
| this._rfb_password = passwd; |
| this._rfb_state = 'Authentication'; |
| setTimeout(this._init_msg.bind(this), 1); |
| }, |
| |
| sendCtrlAltDel: function () { |
| if (this._rfb_state !== 'normal' || this._view_only) { return false; } |
| Util.Info("Sending Ctrl-Alt-Del"); |
| |
| var arr = []; |
| arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 1)); // Control |
| arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 1)); // Alt |
| arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 1)); // Delete |
| arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 0)); // Delete |
| arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 0)); // Alt |
| arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 0)); // Control |
| this._sock.send(arr); |
| }, |
| |
| xvpOp: function (ver, op) { |
| if (this._rfb_xvp_ver < ver) { return false; } |
| Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); |
| this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); |
| return true; |
| }, |
| |
| xvpShutdown: function () { |
| return this.xvpOp(1, 2); |
| }, |
| |
| xvpReboot: function () { |
| return this.xvpOp(1, 3); |
| }, |
| |
| xvpReset: function () { |
| return this.xvpOp(1, 4); |
| }, |
| |
| // Send a key press. If 'down' is not specified then send a down key |
| // followed by an up key. |
| sendKey: function (code, down) { |
| if (this._rfb_state !== "normal" || this._view_only) { return false; } |
| var arr = []; |
| if (typeof down !== 'undefined') { |
| Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); |
| arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0)); |
| } else { |
| Util.Info("Sending key code (down + up): " + code); |
| arr = arr.concat(RFB.messages.keyEvent(code, 1)); |
| arr = arr.concat(RFB.messages.keyEvent(code, 0)); |
| } |
| this._sock.send(arr); |
| }, |
| |
| clipboardPasteFrom: function (text) { |
| if (this._rfb_state !== 'normal') { return; } |
| this._sock.send(RFB.messages.clientCutText(text)); |
| }, |
| |
| // Private methods |
| |
| _connect: function () { |
| Util.Debug(">> RFB.connect"); |
| |
| var uri; |
| if (typeof UsingSocketIO !== 'undefined') { |
| uri = 'http'; |
| } else { |
| uri = this._encrypt ? 'wss' : 'ws'; |
| } |
| |
| uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; |
| Util.Info("connecting to " + uri); |
| |
| this._sock.open(uri, this._wsProtocols); |
| |
| Util.Debug("<< RFB.connect"); |
| }, |
| |
| _init_vars: function () { |
| // reset state |
| this._sock.init(); |
| |
| this._FBU.rects = 0; |
| this._FBU.subrects = 0; // RRE and HEXTILE |
| this._FBU.lines = 0; // RAW |
| this._FBU.tiles = 0; // HEXTILE |
| this._FBU.zlibs = []; // TIGHT zlib encoders |
| this._mouse_buttonMask = 0; |
| this._mouse_arr = []; |
| this._rfb_tightvnc = false; |
| |
| // Clear the per connection encoding stats |
| var i; |
| for (i = 0; i < this._encodings.length; i++) { |
| this._encStats[this._encodings[i][1]][0] = 0; |
| } |
| |
| for (i = 0; i < 4; i++) { |
| this._FBU.zlibs[i] = new TINF(); |
| this._FBU.zlibs[i].init(); |
| } |
| }, |
| |
| _print_stats: function () { |
| Util.Info("Encoding stats for this connection:"); |
| var i, s; |
| for (i = 0; i < this._encodings.length; i++) { |
| s = this._encStats[this._encodings[i][1]]; |
| if (s[0] + s[1] > 0) { |
| Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); |
| } |
| } |
| |
| Util.Info("Encoding stats since page load:"); |
| for (i = 0; i < this._encodings.length; i++) { |
| s = this._encStats[this._encodings[i][1]]; |
| Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); |
| } |
| }, |
| |
| |
| /* |
| * Page states: |
| * loaded - page load, equivalent to disconnected |
| * disconnected - idle state |
| * connect - starting to connect (to ProtocolVersion) |
| * normal - connected |
| * disconnect - starting to disconnect |
| * failed - abnormal disconnect |
| * fatal - failed to load page, or fatal error |
| * |
| * RFB protocol initialization states: |
| * ProtocolVersion |
| * Security |
| * Authentication |
| * password - waiting for password, not part of RFB |
| * SecurityResult |
| * ClientInitialization - not triggered by server message |
| * ServerInitialization (to normal) |
| */ |
| _updateState: function (state, statusMsg) { |
| var oldstate = this._rfb_state; |
| |
| if (state === oldstate) { |
| // Already here, ignore |
| Util.Debug("Already in state '" + state + "', ignoring"); |
| } |
| |
| /* |
| * These are disconnected states. A previous connect may |
| * asynchronously cause a connection so make sure we are closed. |
| */ |
| if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, |
| 'disconnect': 1, 'failed': 1, 'fatal': 1}) { |
| |
| if (this._sendTimer) { |
| clearInterval(this._sendTimer); |
| this._sendTimer = null; |
| } |
| |
| if (this._msgTimer) { |
| clearInterval(this._msgTimer); |
| this._msgTimer = null; |
| } |
| |
| if (this._display && this._display.get_context()) { |
| this._keyboard.ungrab(); |
| this._mouse.ungrab(); |
| this._display.defaultCursor(); |
| if (Util.get_logging() !== 'debug' || state === 'loaded') { |
| // Show noVNC logo on load and when disconnected, unless in |
| // debug mode |
| this._display.clear(); |
| } |
| } |
| |
| this._sock.close(); |
| } |
| |
| if (oldstate === 'fatal') { |
| Util.Error('Fatal error, cannot continue'); |
| } |
| |
| var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; |
| var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; |
| if (state === 'failed' || state === 'fatal') { |
| Util.Error(cmsg); |
| } else { |
| Util.Warn(cmsg); |
| } |
| |
| if (oldstate === 'failed' && state === 'disconnected') { |
| // do disconnect action, but stay in failed state |
| this._rfb_state = 'failed'; |
| } else { |
| this._rfb_state = state; |
| } |
| |
| if (this._disconnTimer && this._rfb_state !== 'disconnect') { |
| Util.Debug("Clearing disconnect timer"); |
| clearTimeout(this._disconnTimer); |
| this._disconnTimer = null; |
| } |
| |
| switch (state) { |
| case 'normal': |
| if (oldstate === 'disconnected' || oldstate === 'failed') { |
| Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); |
| } |
| break; |
| |
| case 'connect': |
| this._init_vars(); |
| this._connect(); |
| // WebSocket.onopen transitions to 'ProtocolVersion' |
| break; |
| |
| case 'disconnect': |
| this._disconnTimer = setTimeout(function () { |
| this._fail("Disconnect timeout"); |
| }.bind(this), this._disconnectTimeout * 1000); |
| |
| this._print_stats(); |
| |
| // WebSocket.onclose transitions to 'disconnected' |
| break; |
| |
| case 'failed': |
| if (oldstate === 'disconnected') { |
| Util.Error("Invalid transition from 'disconnected' to 'failed'"); |
| } else if (oldstate === 'normal') { |
| Util.Error("Error while connected."); |
| } else if (oldstate === 'init') { |
| Util.Error("Error while initializing."); |
| } |
| |
| // Make sure we transition to disconnected |
| setTimeout(function () { |
| this._updateState('disconnected'); |
| }.bind(this), 50); |
| |
| break; |
| |
| default: |
| // No state change action to take |
| } |
| |
| if (oldstate === 'failed' && state === 'disconnected') { |
| this._onUpdateState(this, state, oldstate); |
| } else { |
| this._onUpdateState(this, state, oldstate, statusMsg); |
| } |
| }, |
| |
| _fail: function (msg) { |
| this._updateState('failed', msg); |
| return false; |
| }, |
| |
| _handle_message: function () { |
| if (this._sock.rQlen() === 0) { |
| Util.Warn("handle_message called on an empty receive queue"); |
| return; |
| } |
| |
| switch (this._rfb_state) { |
| case 'disconnected': |
| case 'failed': |
| Util.Error("Got data while disconnected"); |
| break; |
| case 'normal': |
| if (this._normal_msg() && this._sock.rQlen() > 0) { |
| // true means we can continue processing |
| // Give other events a chance to run |
| if (this._msgTimer === null) { |
| Util.Debug("More data to process, creating timer"); |
| this._msgTimer = setTimeout(function () { |
| this._msgTimer = null; |
| this._handle_message(); |
| }.bind(this), 10); |
| } else { |
| Util.Debug("More data to process, existing timer"); |
| } |
| } |
| break; |
| default: |
| this._init_msg(); |
| break; |
| } |
| }, |
| |
| _checkEvents: function () { |
| if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) { |
| this._sock.send(this._mouse_arr); |
| this._mouse_arr = []; |
| } |
| }, |
| |
| _handleKeyPress: function (keysym, down) { |
| if (this._view_only) { return; } // View only, skip keyboard, events |
| this._sock.send(RFB.messages.keyEvent(keysym, down)); |
| }, |
| |
| _handleMouseButton: function (x, y, down, bmask) { |
| if (down) { |
| this._mouse_buttonMask |= bmask; |
| } else { |
| this._mouse_buttonMask ^= bmask; |
| } |
| |
| if (this._viewportDrag) { |
| if (down && !this._viewportDragging) { |
| this._viewportDragging = true; |
| this._viewportDragPos = {'x': x, 'y': y}; |
| |
| // Skip sending mouse events |
| return; |
| } else { |
| this._viewportDragging = false; |
| } |
| } |
| |
| if (this._view_only) { return; } // View only, skip mouse events |
| |
| this._mouse_arr = this._mouse_arr.concat( |
| RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); |
| this._sock.send(this._mouse_arr); |
| this._mouse_arr = []; |
| }, |
| |
| _handleMouseMove: function (x, y) { |
| if (this._viewportDragging) { |
| var deltaX = this._viewportDragPos.x - x; |
| var deltaY = this._viewportDragPos.y - y; |
| this._viewportDragPos = {'x': x, 'y': y}; |
| |
| this._display.viewportChange(deltaX, deltaY); |
| |
| // Skip sending mouse events |
| return; |
| } |
| |
| if (this._view_only) { return; } // View only, skip mouse events |
| |
| this._mouse_arr = this._mouse_arr.concat( |
| RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); |
| |
| this._checkEvents(); |
| }, |
| |
| // Message Handlers |
| |
| _negotiate_protocol_version: function () { |
| if (this._sock.rQlen() < 12) { |
| return this._fail("Incomplete protocol version"); |
| } |
| |
| var sversion = this._sock.rQshiftStr(12).substr(4, 7); |
| Util.Info("Server ProtocolVersion: " + sversion); |
| var is_repeater = 0; |
| switch (sversion) { |
| case "000.000": // UltraVNC repeater |
| is_repeater = 1; |
| break; |
| case "003.003": |
| case "003.006": // UltraVNC |
| case "003.889": // Apple Remote Desktop |
| this._rfb_version = 3.3; |
| break; |
| case "003.007": |
| this._rfb_version = 3.7; |
| break; |
| case "003.008": |
| case "004.000": // Intel AMT KVM |
| case "004.001": // RealVNC 4.6 |
| this._rfb_version = 3.8; |
| break; |
| default: |
| return this._fail("Invalid server version " + sversion); |
| } |
| |
| if (is_repeater) { |
| var repeaterID = this._repeaterID; |
| while (repeaterID.length < 250) { |
| repeaterID += "\0"; |
| } |
| this._sock.send_string(repeaterID); |
| return true; |
| } |
| |
| if (this._rfb_version > this._rfb_max_version) { |
| this._rfb_version = this._rfb_max_version; |
| } |
| |
| // Send updates either at a rate of 1 update per 50ms, or |
| // whatever slower rate the network can handle |
| this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); |
| |
| var cversion = "00" + parseInt(this._rfb_version, 10) + |
| ".00" + ((this._rfb_version * 10) % 10); |
| this._sock.send_string("RFB " + cversion + "\n"); |
| this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); |
| }, |
| |
| _negotiate_security: function () { |
| if (this._rfb_version >= 3.7) { |
| // Server sends supported list, client decides |
| var num_types = this._sock.rQshift8(); |
| if (this._sock.rQwait("security type", num_types, 1)) { return false; } |
| |
| if (num_types === 0) { |
| var strlen = this._sock.rQshift32(); |
| var reason = this._sock.rQshiftStr(strlen); |
| return this._fail("Security failure: " + reason); |
| } |
| |
| this._rfb_auth_scheme = 0; |
| var types = this._sock.rQshiftBytes(num_types); |
| Util.Debug("Server security types: " + types); |
| for (var i = 0; i < types.length; i++) { |
| if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { |
| this._rfb_auth_scheme = types[i]; |
| } |
| } |
| |
| if (this._rfb_auth_scheme === 0) { |
| return this._fail("Unsupported security types: " + types); |
| } |
| |
| this._sock.send([this._rfb_auth_scheme]); |
| } else { |
| // Server decides |
| if (this._sock.rQwait("security scheme", 4)) { return false; } |
| this._rfb_auth_scheme = this._sock.rQshift32(); |
| } |
| |
| this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); |
| return this._init_msg(); // jump to authentication |
| }, |
| |
| // authentication |
| _negotiate_xvp_auth: function () { |
| var xvp_sep = this._xvp_password_sep; |
| var xvp_auth = this._rfb_password.split(xvp_sep); |
| if (xvp_auth.length < 3) { |
| this._updateState('password', 'XVP credentials required (user' + xvp_sep + |
| 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); |
| this._onPasswordRequired(this); |
| return false; |
| } |
| |
| var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + |
| String.fromCharCode(xvp_auth[1].length) + |
| xvp_auth[0] + |
| xvp_auth[1]; |
| this._sock.send_string(xvp_auth_str); |
| this._rfb_password = xvp_auth.slice(2).join(xvp_sep); |
| this._rfb_auth_scheme = 2; |
| return this._negotiate_authentication(); |
| }, |
| |
| _negotiate_std_vnc_auth: function () { |
| if (this._rfb_password.length === 0) { |
| // Notify via both callbacks since it's kind of |
| // an RFB state change and a UI interface issue |
| this._updateState('password', "Password Required"); |
| this._onPasswordRequired(this); |
| } |
| |
| if (this._sock.rQwait("auth challenge", 16)) { return false; } |
| |
| var challenge = this._sock.rQshiftBytes(16); |
| var response = RFB.genDES(this._rfb_password, challenge); |
| this._sock.send(response); |
| this._updateState("SecurityResult"); |
| return true; |
| }, |
| |
| _negotiate_tight_tunnels: function (numTunnels) { |
| var clientSupportedTunnelTypes = { |
| 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } |
| }; |
| var serverSupportedTunnelTypes = {}; |
| // receive tunnel capabilities |
| for (var i = 0; i < numTunnels; i++) { |
| var cap_code = this._sock.rQshift32(); |
| var cap_vendor = this._sock.rQshiftStr(4); |
| var cap_signature = this._sock.rQshiftStr(8); |
| serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; |
| } |
| |
| // choose the notunnel type |
| if (serverSupportedTunnelTypes[0]) { |
| if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || |
| serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { |
| return this._fail("Client's tunnel type had the incorrect vendor or signature"); |
| } |
| this._sock.send([0, 0, 0, 0]); // use NOTUNNEL |
| return false; // wait until we receive the sub auth count to continue |
| } else { |
| return this._fail("Server wanted tunnels, but doesn't support the notunnel type"); |
| } |
| }, |
| |
| _negotiate_tight_auth: function () { |
| if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation |
| if (this._sock.rQwait("num tunnels", 4)) { return false; } |
| var numTunnels = this._sock.rQshift32(); |
| if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } |
| |
| this._rfb_tightvnc = true; |
| |
| if (numTunnels > 0) { |
| this._negotiate_tight_tunnels(numTunnels); |
| return false; // wait until we receive the sub auth to continue |
| } |
| } |
| |
| // second pass, do the sub-auth negotiation |
| if (this._sock.rQwait("sub auth count", 4)) { return false; } |
| var subAuthCount = this._sock.rQshift32(); |
| if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } |
| |
| var clientSupportedTypes = { |
| 'STDVNOAUTH__': 1, |
| 'STDVVNCAUTH_': 2 |
| }; |
| |
| var serverSupportedTypes = []; |
| |
| for (var i = 0; i < subAuthCount; i++) { |
| var capNum = this._sock.rQshift32(); |
| var capabilities = this._sock.rQshiftStr(12); |
| serverSupportedTypes.push(capabilities); |
| } |
| |
| for (var authType in clientSupportedTypes) { |
| if (serverSupportedTypes.indexOf(authType) != -1) { |
| this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); |
| |
| switch (authType) { |
| case 'STDVNOAUTH__': // no auth |
| this._updateState('SecurityResult'); |
| return true; |
| case 'STDVVNCAUTH_': // VNC auth |
| this._rfb_auth_scheme = 2; |
| return this._init_msg(); |
| default: |
| return this._fail("Unsupported tiny auth scheme: " + authType); |
| } |
| } |
| } |
| |
| this._fail("No supported sub-auth types!"); |
| }, |
| |
| _negotiate_authentication: function () { |
| switch (this._rfb_auth_scheme) { |
| case 0: // connection failed |
| if (this._sock.rQwait("auth reason", 4)) { return false; } |
| var strlen = this._sock.rQshift32(); |
| var reason = this._sock.rQshiftStr(strlen); |
| return this._fail("Auth failure: " + reason); |
| |
| case 1: // no auth |
| if (this._rfb_version >= 3.8) { |
| this._updateState('SecurityResult'); |
| return true; |
| } |
| this._updateState('ClientInitialisation', "No auth required"); |
| return this._init_msg(); |
| |
| case 22: // XVP auth |
| return this._negotiate_xvp_auth(); |
| |
| case 2: // VNC authentication |
| return this._negotiate_std_vnc_auth(); |
| |
| case 16: // TightVNC Security Type |
| return this._negotiate_tight_auth(); |
| |
| default: |
| return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); |
| } |
| }, |
| |
| _handle_security_result: function () { |
| if (this._sock.rQwait('VNC auth response ', 4)) { return false; } |
| switch (this._sock.rQshift32()) { |
| case 0: // OK |
| this._updateState('ClientInitialisation', 'Authentication OK'); |
| return this._init_msg(); |
| case 1: // failed |
| if (this._rfb_version >= 3.8) { |
| var length = this._sock.rQshift32(); |
| if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } |
| var reason = this._sock.rQshiftStr(length); |
| return this._fail(reason); |
| } else { |
| return this._fail("Authentication failure"); |
| } |
| return false; |
| case 2: |
| return this._fail("Too many auth attempts"); |
| } |
| }, |
| |
| _negotiate_server_init: function () { |
| if (this._sock.rQwait("server initialization", 24)) { return false; } |
| |
| /* Screen size */ |
| this._fb_width = this._sock.rQshift16(); |
| this._fb_height = this._sock.rQshift16(); |
| |
| /* PIXEL_FORMAT */ |
| var bpp = this._sock.rQshift8(); |
| var depth = this._sock.rQshift8(); |
| var big_endian = this._sock.rQshift8(); |
| var true_color = this._sock.rQshift8(); |
| |
| var red_max = this._sock.rQshift16(); |
| var green_max = this._sock.rQshift16(); |
| var blue_max = this._sock.rQshift16(); |
| var red_shift = this._sock.rQshift8(); |
| var green_shift = this._sock.rQshift8(); |
| var blue_shift = this._sock.rQshift8(); |
| this._sock.rQskipBytes(3); // padding |
| |
| // NB(directxman12): we don't want to call any callbacks or print messages until |
| // *after* we're past the point where we could backtrack |
| |
| /* Connection name/title */ |
| var name_length = this._sock.rQshift32(); |
| if (this._sock.rQwait('server init name', name_length, 24)) { return false; } |
| this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); |
| |
| if (this._rfb_tightvnc) { |
| if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } |
| // In TightVNC mode, ServerInit message is extended |
| var numServerMessages = this._sock.rQshift16(); |
| var numClientMessages = this._sock.rQshift16(); |
| var numEncodings = this._sock.rQshift16(); |
| this._sock.rQskipBytes(2); // padding |
| |
| var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; |
| if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } |
| |
| var i; |
| for (i = 0; i < numServerMessages; i++) { |
| var srvMsg = this._sock.rQshiftStr(16); |
| } |
| |
| for (i = 0; i < numClientMessages; i++) { |
| var clientMsg = this._sock.rQshiftStr(16); |
| } |
| |
| for (i = 0; i < numEncodings; i++) { |
| var encoding = this._sock.rQshiftStr(16); |
| } |
| } |
| |
| // NB(directxman12): these are down here so that we don't run them multiple times |
| // if we backtrack |
| Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + |
| ", bpp: " + bpp + ", depth: " + depth + |
| ", big_endian: " + big_endian + |
| ", true_color: " + true_color + |
| ", red_max: " + red_max + |
| ", green_max: " + green_max + |
| ", blue_max: " + blue_max + |
| ", red_shift: " + red_shift + |
| ", green_shift: " + green_shift + |
| ", blue_shift: " + blue_shift); |
| |
| if (big_endian !== 0) { |
| Util.Warn("Server native endian is not little endian"); |
| } |
| |
| if (red_shift !== 16) { |
| Util.Warn("Server native red-shift is not 16"); |
| } |
| |
| if (blue_shift !== 0) { |
| Util.Warn("Server native blue-shift is not 0"); |
| } |
| |
| // we're past the point where we could backtrack, so it's safe to call this |
| this._onDesktopName(this, this._fb_name); |
| |
| if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { |
| Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); |
| this._true_color = false; |
| } |
| |
| this._display.set_true_color(this._true_color); |
| this._onFBResize(this, this._fb_width, this._fb_height); |
| this._display.resize(this._fb_width, this._fb_height); |
| this._keyboard.grab(); |
| this._mouse.grab(); |
| |
| if (this._true_color) { |
| this._fb_Bpp = 4; |
| this._fb_depth = 3; |
| } else { |
| this._fb_Bpp = 1; |
| this._fb_depth = 1; |
| } |
| |
| var response = RFB.messages.pixelFormat(this._fb_Bpp, this._fb_depth, this._true_color); |
| response = response.concat( |
| RFB.messages.clientEncodings(this._encodings, this._local_cursor, this._true_color)); |
| response = response.concat( |
| RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), |
| this._fb_width, this._fb_height)); |
| |
| this._timing.fbu_rt_start = (new Date()).getTime(); |
| this._timing.pixels = 0; |
| this._sock.send(response); |
| |
| this._checkEvents(); |
| |
| if (this._encrypt) { |
| this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); |
| } else { |
| this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); |
| } |
| }, |
| |
| _init_msg: function () { |
| switch (this._rfb_state) { |
| case 'ProtocolVersion': |
| return this._negotiate_protocol_version(); |
| |
| case 'Security': |
| return this._negotiate_security(); |
| |
| case 'Authentication': |
| return this._negotiate_authentication(); |
| |
| case 'SecurityResult': |
| return this._handle_security_result(); |
| |
| case 'ClientInitialisation': |
| this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation |
| this._updateState('ServerInitialisation', "Authentication OK"); |
| return true; |
| |
| case 'ServerInitialisation': |
| return this._negotiate_server_init(); |
| } |
| }, |
| |
| _handle_set_colour_map_msg: function () { |
| Util.Debug("SetColorMapEntries"); |
| this._sock.rQskip8(); // Padding |
| |
| var first_colour = this._sock.rQshift16(); |
| var num_colours = this._sock.rQshift16(); |
| if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } |
| |
| for (var c = 0; c < num_colours; c++) { |
| var red = parseInt(this._sock.rQshift16() / 256, 10); |
| var green = parseInt(this._sock.rQshift16() / 256, 10); |
| var blue = parseInt(this._sock.rQshift16() / 256, 10); |
| this._display.set_colourMap([blue, green, red], first_colour + c); |
| } |
| Util.Debug("colourMap: " + this._display.get_colourMap()); |
| Util.Info("Registered " + num_colours + " colourMap entries"); |
| |
| return true; |
| }, |
| |
| _handle_server_cut_text: function () { |
| Util.Debug("ServerCutText"); |
| if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } |
| this._sock.rQskipBytes(3); // Padding |
| var length = this._sock.rQshift32(); |
| if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } |
| |
| var text = this._sock.rQshiftStr(length); |
| this._onClipboard(this, text); |
| |
| return true; |
| }, |
| |
| _handle_xvp_msg: function () { |
| if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } |
| this._sock.rQskip8(); // Padding |
| var xvp_ver = this._sock.rQshift8(); |
| var xvp_msg = this._sock.rQshift8(); |
| |
| switch (xvp_msg) { |
| case 0: // XVP_FAIL |
| this._updateState(this._rfb_state, "Operation Failed"); |
| break; |
| case 1: // XVP_INIT |
| this._rfb_xvp_ver = xvp_ver; |
| Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); |
| this._onXvpInit(this._rfb_xvp_ver); |
| break; |
| default: |
| this._fail("Disconnected: illegal server XVP message " + xvp_msg); |
| break; |
| } |
| |
| return true; |
| }, |
| |
| _normal_msg: function () { |
| var msg_type; |
| |
| if (this._FBU.rects > 0) { |
| msg_type = 0; |
| } else { |
| msg_type = this._sock.rQshift8(); |
| } |
| |
| switch (msg_type) { |
| case 0: // FramebufferUpdate |
| var ret = this._framebufferUpdate(); |
| if (ret) { |
| this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), |
| this._fb_width, this._fb_height)); |
| } |
| return ret; |
| |
| case 1: // SetColorMapEntries |
| return this._handle_set_colour_map_msg(); |
| |
| case 2: // Bell |
| Util.Debug("Bell"); |
| this._onBell(this); |
| return true; |
| |
| case 3: // ServerCutText |
| return this._handle_server_cut_text(); |
| |
| case 250: // XVP |
| return this._handle_xvp_msg(); |
| |
| default: |
| this._fail("Disconnected: illegal server message type " + msg_type); |
| Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); |
| return true; |
| } |
| }, |
| |
| _framebufferUpdate: function () { |
| var ret = true; |
| var now; |
| |
| if (this._FBU.rects === 0) { |
| if (this._sock.rQwait("FBU header", 3, 1)) { return false; } |
| this._sock.rQskip8(); // Padding |
| this._FBU.rects = this._sock.rQshift16(); |
| this._FBU.bytes = 0; |
| this._timing.cur_fbu = 0; |
| if (this._timing.fbu_rt_start > 0) { |
| now = (new Date()).getTime(); |
| Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); |
| } |
| } |
| |
| while (this._FBU.rects > 0) { |
| if (this._rfb_state !== "normal") { return false; } |
| |
| if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } |
| if (this._FBU.bytes === 0) { |
| if (this._sock.rQwait("rect header", 12)) { return false; } |
| /* New FramebufferUpdate */ |
| |
| var hdr = this._sock.rQshiftBytes(12); |
| this._FBU.x = (hdr[0] << 8) + hdr[1]; |
| this._FBU.y = (hdr[2] << 8) + hdr[3]; |
| this._FBU.width = (hdr[4] << 8) + hdr[5]; |
| this._FBU.height = (hdr[6] << 8) + hdr[7]; |
| this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + |
| (hdr[10] << 8) + hdr[11], 10); |
| |
| this._onFBUReceive(this, |
| {'x': this._FBU.x, 'y': this._FBU.y, |
| 'width': this._FBU.width, 'height': this._FBU.height, |
| 'encoding': this._FBU.encoding, |
| 'encodingName': this._encNames[this._FBU.encoding]}); |
| |
| if (!this._encNames[this._FBU.encoding]) { |
| this._fail("Disconnected: unsupported encoding " + |
| this._FBU.encoding); |
| return false; |
| } |
| } |
| |
| this._timing.last_fbu = (new Date()).getTime(); |
| |
| ret = this._encHandlers[this._FBU.encoding](); |
| |
| now = (new Date()).getTime(); |
| this._timing.cur_fbu += (now - this._timing.last_fbu); |
| |
| if (ret) { |
| this._encStats[this._FBU.encoding][0]++; |
| this._encStats[this._FBU.encoding][1]++; |
| this._timing.pixels += this._FBU.width * this._FBU.height; |
| } |
| |
| if (this._timing.pixels >= (this._fb_width * this._fb_height)) { |
| if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || |
| this._timing.fbu_rt_start > 0) { |
| this._timing.full_fbu_total += this._timing.cur_fbu; |
| this._timing.full_fbu_cnt++; |
| Util.Info("Timing of full FBU, curr: " + |
| this._timing.cur_fbu + ", total: " + |
| this._timing.full_fbu_total + ", cnt: " + |
| this._timing.full_fbu_cnt + ", avg: " + |
| (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); |
| } |
| |
| if (this._timing.fbu_rt_start > 0) { |
| var fbu_rt_diff = now - this._timing.fbu_rt_start; |
| this._timing.fbu_rt_total += fbu_rt_diff; |
| this._timing.fbu_rt_cnt++; |
| Util.Info("full FBU round-trip, cur: " + |
| fbu_rt_diff + ", total: " + |
| this._timing.fbu_rt_total + ", cnt: " + |
| this._timing.fbu_rt_cnt + ", avg: " + |
| (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); |
| this._timing.fbu_rt_start = 0; |
| } |
| } |
| |
| if (!ret) { return ret; } // need more data |
| } |
| |
| this._onFBUComplete(this, |
| {'x': this._FBU.x, 'y': this._FBU.y, |
| 'width': this._FBU.width, 'height': this._FBU.height, |
| 'encoding': this._FBU.encoding, |
| 'encodingName': this._encNames[this._FBU.encoding]}); |
| |
| return true; // We finished this FBU |
| }, |
| }; |
| |
| Util.make_properties(RFB, [ |
| ['target', 'wo', 'dom'], // VNC display rendering Canvas object |
| ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input |
| ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption |
| ['true_color', 'rw', 'bool'], // Request true color pixel data |
| ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor |
| ['shared', 'rw', 'bool'], // Request shared mode |
| ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard |
| ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields |
| ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection |
| ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection |
| ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to |
| ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags |
| |
| // Callback functions |
| ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change |
| ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required |
| ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received |
| ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received |
| ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed |
| ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed |
| ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized |
| ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received |
| ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection |
| ]); |
| |
| RFB.prototype.set_local_cursor = function (cursor) { |
| if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { |
| this._local_cursor = false; |
| } else { |
| if (this._display.get_cursor_uri()) { |
| this._local_cursor = true; |
| } else { |
| Util.Warn("Browser does not support local cursor"); |
| } |
| } |
| }; |
| |
| RFB.prototype.get_display = function () { return this._display; }; |
| RFB.prototype.get_keyboard = function () { return this._keyboard; }; |
| RFB.prototype.get_mouse = function () { return this._mouse; }; |
| |
| // Class Methods |
| RFB.messages = { |
| keyEvent: function (keysym, down) { |
| var arr = [4]; |
| arr.push8(down); |
| arr.push16(0); |
| arr.push32(keysym); |
| return arr; |
| }, |
| |
| pointerEvent: function (x, y, mask) { |
| var arr = [5]; // msg-type |
| arr.push8(mask); |
| arr.push16(x); |
| arr.push16(y); |
| return arr; |
| }, |
| |
| // TODO(directxman12): make this unicode compatible? |
| clientCutText: function (text) { |
| var arr = [6]; // msg-type |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| arr.push32(text.length); |
| var n = text.length; |
| for (var i = 0; i < n; i++) { |
| arr.push(text.charCodeAt(i)); |
| } |
| |
| return arr; |
| }, |
| |
| pixelFormat: function (bpp, depth, true_color) { |
| var arr = [0]; // msg-type |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| |
| arr.push8(bpp * 8); // bits-per-pixel |
| arr.push8(depth * 8); // depth |
| arr.push8(0); // little-endian |
| arr.push8(true_color ? 1 : 0); // true-color |
| |
| arr.push16(255); // red-max |
| arr.push16(255); // green-max |
| arr.push16(255); // blue-max |
| arr.push8(16); // red-shift |
| arr.push8(8); // green-shift |
| arr.push8(0); // blue-shift |
| |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| arr.push8(0); // padding |
| return arr; |
| }, |
| |
| clientEncodings: function (encodings, local_cursor, true_color) { |
| var i, encList = []; |
| |
| for (i = 0; i < encodings.length; i++) { |
| if (encodings[i][0] === "Cursor" && !local_cursor) { |
| Util.Debug("Skipping Cursor pseudo-encoding"); |
| } else if (encodings[i][0] === "TIGHT" && !true_color) { |
| // TODO: remove this when we have tight+non-true-color |
| Util.Warn("Skipping tight as it is only supported with true color"); |
| } else { |
| encList.push(encodings[i][1]); |
| } |
| } |
| |
| var arr = [2]; // msg-type |
| arr.push8(0); // padding |
| |
| arr.push16(encList.length); // encoding count |
| for (i = 0; i < encList.length; i++) { |
| arr.push32(encList[i]); |
| } |
| |
| return arr; |
| }, |
| |
| fbUpdateRequests: function (cleanDirty, fb_width, fb_height) { |
| var arr = []; |
| |
| var cb = cleanDirty.cleanBox; |
| var w, h; |
| if (cb.w > 0 && cb.h > 0) { |
| w = typeof cb.w === "undefined" ? fb_width : cb.w; |
| h = typeof cb.h === "undefined" ? fb_height : cb.h; |
| // Request incremental for clean box |
| arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h)); |
| } |
| |
| for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { |
| var db = cleanDirty.dirtyBoxes[i]; |
| // Force all (non-incremental) for dirty box |
| w = typeof db.w === "undefined" ? fb_width : db.w; |
| h = typeof db.h === "undefined" ? fb_height : db.h; |
| arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h)); |
| } |
| |
| return arr; |
| }, |
| |
| fbUpdateRequest: function (incremental, x, y, w, h) { |
| if (typeof(x) === "undefined") { x = 0; } |
| if (typeof(y) === "undefined") { y = 0; } |
| |
| var arr = [3]; // msg-type |
| arr.push8(incremental); |
| arr.push16(x); |
| arr.push16(y); |
| arr.push16(w); |
| arr.push16(h); |
| |
| return arr; |
| } |
| }; |
| |
| RFB.genDES = function (password, challenge) { |
| var passwd = []; |
| for (var i = 0; i < password.length; i++) { |
| passwd.push(password.charCodeAt(i)); |
| } |
| return (new DES(passwd)).encrypt(challenge); |
| }; |
| |
| RFB.extract_data_uri = function (arr) { |
| return ";base64," + Base64.encode(arr); |
| }; |
| |
| RFB.encodingHandlers = { |
| RAW: function () { |
| if (this._FBU.lines === 0) { |
| this._FBU.lines = this._FBU.height; |
| } |
| |
| this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line |
| if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } |
| var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); |
| var curr_height = Math.min(this._FBU.lines, |
| Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); |
| this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, |
| curr_height, this._sock.get_rQ(), |
| this._sock.get_rQi()); |
| this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); |
| this._FBU.lines -= curr_height; |
| |
| if (this._FBU.lines > 0) { |
| this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line |
| } else { |
| this._FBU.rects--; |
| this._FBU.bytes = 0; |
| } |
| |
| return true; |
| }, |
| |
| COPYRECT: function () { |
| this._FBU.bytes = 4; |
| if (this._sock.rQwait("COPYRECT", 4)) { return false; } |
| this._display.renderQ_push({ |
| 'type': 'copy', |
| 'old_x': this._sock.rQshift16(), |
| 'old_y': this._sock.rQshift16(), |
| 'x': this._FBU.x, |
| 'y': this._FBU.y, |
| 'width': this._FBU.width, |
| 'height': this._FBU.height |
| }); |
| this._FBU.rects--; |
| this._FBU.bytes = 0; |
| return true; |
| }, |
| |
| RRE: function () { |
| var color; |
| if (this._FBU.subrects === 0) { |
| this._FBU.bytes = 4 + this._fb_Bpp; |
| if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } |
| this._FBU.subrects = this._sock.rQshift32(); |
| color = this._sock.rQshiftBytes(this._fb_Bpp); // Background |
| this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); |
| } |
| |
| while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { |
| color = this._sock.rQshiftBytes(this._fb_Bpp); |
| var x = this._sock.rQshift16(); |
| var y = this._sock.rQshift16(); |
| var width = this._sock.rQshift16(); |
| var height = this._sock.rQshift16(); |
| this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); |
| this._FBU.subrects--; |
| } |
| |
| if (this._FBU.subrects > 0) { |
| var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); |
| this._FBU.bytes = (this._fb_Bpp + 8) * chunk; |
| } else { |
| this._FBU.rects--; |
| this._FBU.bytes = 0; |
| } |
| |
| return true; |
| }, |
| |
| HEXTILE: function () { |
| var rQ = this._sock.get_rQ(); |
| var rQi = this._sock.get_rQi(); |
| |
| if (this._FBU.tiles === 0) { |
| this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); |
| this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); |
| this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; |
| this._FBU.tiles = this._FBU.total_tiles; |
| } |
| |
| while (this._FBU.tiles > 0) { |
| this._FBU.bytes = 1; |
| if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } |
| var subencoding = rQ[rQi]; // Peek |
| if (subencoding > 30) { // Raw |
| this._fail("Disconnected: illegal hextile subencoding " + subencoding); |
| return false; |
| } |
| |
| var subrects = 0; |
| var curr_tile = this._FBU.total_tiles - this._FBU.tiles; |
| var tile_x = curr_tile % this._FBU.tiles_x; |
| var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); |
| var x = this._FBU.x + tile_x * 16; |
| var y = this._FBU.y + tile_y * 16; |
| var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); |
| var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); |
| |
| // Figure out how much we are expecting |
| if (subencoding & 0x01) { // Raw |
| this._FBU.bytes += w * h * this._fb_Bpp; |
| } else { |
| if (subencoding & 0x02) { // Background |
| this._FBU.bytes += this._fb_Bpp; |
| } |
| if (subencoding & 0x04) { // Foreground |
| this._FBU.bytes += this._fb_Bpp; |
| } |
| if (subencoding & 0x08) { // AnySubrects |
| this._FBU.bytes++; // Since we aren't shifting it off |
| if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } |
| subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek |
| if (subencoding & 0x10) { // SubrectsColoured |
| this._FBU.bytes += subrects * (this._fb_Bpp + 2); |
| } else { |
| this._FBU.bytes += subrects * 2; |
| } |
| } |
| } |
| |
| if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } |
| |
| // We know the encoding and have a whole tile |
| this._FBU.subencoding = rQ[rQi]; |
| rQi++; |
| if (this._FBU.subencoding === 0) { |
| if (this._FBU.lastsubencoding & 0x01) { |
| // Weird: ignore blanks are RAW |
| Util.Debug(" Ignoring blank after RAW"); |
| } else { |
| this._display.fillRect(x, y, w, h, rQ, rQi); |
| rQi += this._FBU.bytes - 1; |
| } |
| } else if (this._FBU.subencoding & 0x01) { // Raw |
| this._display.blitImage(x, y, w, h, rQ, rQi); |
| rQi += this._FBU.bytes - 1; |
| } else { |
| if (this._FBU.subencoding & 0x02) { // Background |
| this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp); |
| rQi += this._fb_Bpp; |
| } |
| if (this._FBU.subencoding & 0x04) { // Foreground |
| this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); |
| rQi += this._fb_Bpp; |
| } |
| |
| this._display.startTile(x, y, w, h, this._FBU.background); |
| if (this._FBU.subencoding & 0x08) { // AnySubrects |
| subrects = rQ[rQi]; |
| rQi++; |
| |
| for (var s = 0; s < subrects; s++) { |
| var color; |
| if (this._FBU.subencoding & 0x10) { // SubrectsColoured |
| color = rQ.slice(rQi, rQi + this._fb_Bpp); |
| rQi += this._fb_Bpp; |
| } else { |
| color = this._FBU.foreground; |
| } |
| var xy = rQ[rQi]; |
| rQi++; |
| var sx = (xy >> 4); |
| var sy = (xy & 0x0f); |
| |
| var wh = rQ[rQi]; |
| rQi++; |
| var sw = (wh >> 4) + 1; |
| var sh = (wh & 0x0f) + 1; |
| |
| this._display.subTile(sx, sy, sw, sh, color); |
| } |
| } |
| this._display.finishTile(); |
| } |
| this._sock.set_rQi(rQi); |
| this._FBU.lastsubencoding = this._FBU.subencoding; |
| this._FBU.bytes = 0; |
| this._FBU.tiles--; |
| } |
| |
| if (this._FBU.tiles === 0) { |
| this._FBU.rects--; |
| } |
| |
| return true; |
| }, |
| |
| getTightCLength: function (arr) { |
| var header = 1, data = 0; |
| data += arr[0] & 0x7f; |
| if (arr[0] & 0x80) { |
| header++; |
| data += (arr[1] & 0x7f) << 7; |
| if (arr[1] & 0x80) { |
| header++; |
| data += arr[2] << 14; |
| } |
| } |
| return [header, data]; |
| }, |
| |
| display_tight: function (isTightPNG) { |
| if (this._fb_depth === 1) { |
| this._fail("Tight protocol handler only implements true color mode"); |
| } |
| |
| this._FBU.bytes = 1; // compression-control byte |
| if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } |
| |
| var checksum = function (data) { |
| var sum = 0; |
| for (var i = 0; i < data.length; i++) { |
| sum += data[i]; |
| if (sum > 65536) sum -= 65536; |
| } |
| return sum; |
| }; |
| |
| var resetStreams = 0; |
| var streamId = -1; |
| var decompress = function (data) { |
| for (var i = 0; i < 4; i++) { |
| if ((resetStreams >> i) & 1) { |
| this._FBU.zlibs[i].reset(); |
| Util.Info("Reset zlib stream " + i); |
| } |
| } |
| |
| var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); |
| if (uncompressed.status !== 0) { |
| Util.Error("Invalid data in zlib stream"); |
| } |
| |
| return uncompressed.data; |
| }.bind(this); |
| |
| var indexedToRGB = function (data, numColors, palette, width, height) { |
| // Convert indexed (palette based) image data to RGB |
| // TODO: reduce number of calculations inside loop |
| var dest = []; |
| var x, y, dp, sp; |
| if (numColors === 2) { |
| var w = Math.floor((width + 7) / 8); |
| var w1 = Math.floor(width / 8); |
| |
| for (y = 0; y < height; y++) { |
| var b; |
| for (x = 0; x < w1; x++) { |
| for (b = 7; b >= 0; b--) { |
| dp = (y * width + x * 8 + 7 - b) * 3; |
| sp = (data[y * w + x] >> b & 1) * 3; |
| dest[dp] = palette[sp]; |
| dest[dp + 1] = palette[sp + 1]; |
| dest[dp + 2] = palette[sp + 2]; |
| } |
| } |
| |
| for (b = 7; b >= 8 - width % 8; b--) { |
| dp = (y * width + x * 8 + 7 - b) * 3; |
| sp = (data[y * w + x] >> b & 1) * 3; |
| dest[dp] = palette[sp]; |
| dest[dp + 1] = palette[sp + 1]; |
| dest[dp + 2] = palette[sp + 2]; |
| } |
| } |
| } else { |
| for (y = 0; y < height; y++) { |
| for (x = 0; x < width; x++) { |
| dp = (y * width + x) * 3; |
| sp = data[y * width + x] * 3; |
| dest[dp] = palette[sp]; |
| dest[dp + 1] = palette[sp + 1]; |
| dest[dp + 2] = palette[sp + 2]; |
| } |
| } |
| } |
| |
| return dest; |
| }.bind(this); |
| |
| var rQ = this._sock.get_rQ(); |
| var rQi = this._sock.get_rQi(); |
| var cmode, clength, data; |
| |
| var handlePalette = function () { |
| var numColors = rQ[rQi + 2] + 1; |
| var paletteSize = numColors * this._fb_depth; |
| this._FBU.bytes += paletteSize; |
| if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } |
| |
| var bpp = (numColors <= 2) ? 1 : 8; |
| var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); |
| var raw = false; |
| if (rowSize * this._FBU.height < 12) { |
| raw = true; |
| clength = [0, rowSize * this._FBU.height]; |
| } else { |
| clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize, |
| 3 + paletteSize + 3)); |
| } |
| |
| this._FBU.bytes += clength[0] + clength[1]; |
| if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } |
| |
| // Shift ctl, filter id, num colors, palette entries, and clength off |
| this._sock.rQskipBytes(3); |
| var palette = this._sock.rQshiftBytes(paletteSize); |
| this._sock.rQskipBytes(clength[0]); |
| |
| if (raw) { |
| data = this._sock.rQshiftBytes(clength[1]); |
| } else { |
| data = decompress(this._sock.rQshiftBytes(clength[1])); |
| } |
| |
| // Convert indexed (palette based) image data to RGB |
| var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height); |
| |
| this._display.renderQ_push({ |
| 'type': 'blitRgb', |
| 'data': rgb, |
| 'x': this._FBU.x, |
| 'y': this._FBU.y, |
| 'width': this._FBU.width, |
| 'height': this._FBU.height |
| }); |
| |
| return true; |
| }.bind(this); |
| |
| var handleCopy = function () { |
| var raw = false; |
| var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; |
| if (uncompressedSize < 12) { |
| raw = true; |
| clength = [0, uncompressedSize]; |
| } else { |
| clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); |
| } |
| this._FBU.bytes = 1 + clength[0] + clength[1]; |
| if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } |
| |
| // Shift ctl, clength off |
| this._sock.rQshiftBytes(1 + clength[0]); |
| |
| if (raw) { |
| data = this._sock.rQshiftBytes(clength[1]); |
| } else { |
| data = decompress(this._sock.rQshiftBytes(clength[1])); |
| } |
| |
| this._display.renderQ_push({ |
| 'type': 'blitRgb', |
| 'data': data, |
| 'x': this._FBU.x, |
| 'y': this._FBU.y, |
| 'width': this._FBU.width, |
| 'height': this._FBU.height |
| }); |
| |
| return true; |
| }.bind(this); |
| |
| var ctl = this._sock.rQpeek8(); |
| |
| // Keep tight reset bits |
| resetStreams = ctl & 0xF; |
| |
| // Figure out filter |
| ctl = ctl >> 4; |
| streamId = ctl & 0x3; |
| |
| if (ctl === 0x08) cmode = "fill"; |
| else if (ctl === 0x09) cmode = "jpeg"; |
| else if (ctl === 0x0A) cmode = "png"; |
| else if (ctl & 0x04) cmode = "filter"; |
| else if (ctl < 0x04) cmode = "copy"; |
| else return this._fail("Illegal tight compression received, ctl: " + ctl); |
| |
| if (isTightPNG && (cmode === "filter" || cmode === "copy")) { |
| return this._fail("filter/copy received in tightPNG mode"); |
| } |
| |
| switch (cmode) { |
| // fill use fb_depth because TPIXELs drop the padding byte |
| case "fill": // TPIXEL |
| this._FBU.bytes += this._fb_depth; |
| break; |
| case "jpeg": // max clength |
| this._FBU.bytes += 3; |
| break; |
| case "png": // max clength |
| this._FBU.bytes += 3; |
| break; |
| case "filter": // filter id + num colors if palette |
| this._FBU.bytes += 2; |
| break; |
| case "copy": |
| break; |
| } |
| |
| if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } |
| |
| // Determine FBU.bytes |
| switch (cmode) { |
| case "fill": |
| this._sock.rQskip8(); // shift off ctl |
| var color = this._sock.rQshiftBytes(this._fb_depth); |
| this._display.renderQ_push({ |
| 'type': 'fill', |
| 'x': this._FBU.x, |
| 'y': this._FBU.y, |
| 'width': this._FBU.width, |
| 'height': this._FBU.height, |
| 'color': [color[2], color[1], color[0]] |
| }); |
| break; |
| case "png": |
| case "jpeg": |
| clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); |
| this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data |
| if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } |
| |
| // We have everything, render it |
| this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length |
| var img = new Image(); |
| img.src = "data: image/" + cmode + |
| RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1])); |
| this._display.renderQ_push({ |
| 'type': 'img', |
| 'img': img, |
| 'x': this._FBU.x, |
| 'y': this._FBU.y |
| }); |
| img = null; |
| break; |
| case "filter": |
| var filterId = rQ[rQi + 1]; |
| if (filterId === 1) { |
| if (!handlePalette()) { return false; } |
| } else { |
| // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter |
| // Filter 2, Gradient is valid but not use if jpeg is enabled |
| // TODO(directxman12): why aren't we just calling '_fail' here |
| throw new Error("Unsupported tight subencoding received, filter: " + filterId); |
| } |
| break; |
| case "copy": |
| if (!handleCopy()) { return false; } |
| break; |
| } |
| |
| |
| this._FBU.bytes = 0; |
| this._FBU.rects--; |
| |
| return true; |
| }, |
| |
| TIGHT: function () { return this._encHandlers.display_tight(false); }, |
| TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, |
| |
| last_rect: function () { |
| this._FBU.rects = 0; |
| return true; |
| }, |
| |
| DesktopSize: function () { |
| Util.Debug(">> set_desktopsize"); |
| this._fb_width = this._FBU.width; |
| this._fb_height = this._FBU.height; |
| this._onFBResize(this, this._fb_width, this._fb_height); |
| this._display.resize(this._fb_width, this._fb_height); |
| this._timing.fbu_rt_start = (new Date()).getTime(); |
| |
| this._FBU.bytes = 0; |
| this._FBU.rects--; |
| |
| Util.Debug("<< set_desktopsize"); |
| return true; |
| }, |
| |
| Cursor: function () { |
| Util.Debug(">> set_cursor"); |
| var x = this._FBU.x; // hotspot-x |
| var y = this._FBU.y; // hotspot-y |
| var w = this._FBU.width; |
| var h = this._FBU.height; |
| |
| var pixelslength = w * h * this._fb_Bpp; |
| var masklength = Math.floor((w + 7) / 8) * h; |
| |
| this._FBU.bytes = pixelslength + masklength; |
| if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } |
| |
| this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), |
| this._sock.rQshiftBytes(masklength), |
| x, y, w, h); |
| |
| this._FBU.bytes = 0; |
| this._FBU.rects--; |
| |
| Util.Debug("<< set_cursor"); |
| return true; |
| }, |
| |
| JPEG_quality_lo: function () { |
| Util.Error("Server sent jpeg_quality pseudo-encoding"); |
| }, |
| |
| compress_lo: function () { |
| Util.Error("Server sent compress level pseudo-encoding"); |
| } |
| }; |
| })(); |