/* * 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 Adobe Flash 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(XK_Control_L, 1)); arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1)); arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1)); arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0)); arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0)); arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0)); 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, this._FBU.background); } } 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"); } }; })();