UNPKG

peer-vnc

Version:
1,228 lines (1,030 loc) 89.4 kB
/* * 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 ], ['ExtendedDesktopSize', -308 ] ]; 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._destBuff = null; this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) 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 }; this._supportsSetDesktopSize = false; this._screen_id = 0; this._screen_flags = 0; // Mouse state this._mouse_buttonMask = 0; this._mouse_arr = []; this._viewportDragging = false; this._viewportDragPos = {}; this._viewportHasMoved = false; // 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'], // 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]; } // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception try { this._display = new Display({target: this._target}); } catch (exc) { Util.Error("Display exception: " + exc); throw exc; } 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); } this._sock.off('close'); }.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 { this._cleanupSocket('fatal'); throw new Error("WebSocket support is required to use noVNC"); } 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'); this._sock.off('error'); this._sock.off('message'); this._sock.off('open'); }, 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"); RFB.messages.keyEvent(this._sock, XK_Control_L, 1); RFB.messages.keyEvent(this._sock, XK_Alt_L, 1); RFB.messages.keyEvent(this._sock, XK_Delete, 1); RFB.messages.keyEvent(this._sock, XK_Delete, 0); RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); RFB.messages.keyEvent(this._sock, XK_Control_L, 0); this._sock.flush(); }, 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; } if (typeof down !== 'undefined') { Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); } else { Util.Info("Sending key code (down + up): " + code); RFB.messages.keyEvent(this._sock, code, 1); RFB.messages.keyEvent(this._sock, code, 0); } this._sock.flush(); }, clipboardPasteFrom: function (text) { if (this._rfb_state !== 'normal') { return; } RFB.messages.clientCutUTF8Text(this._sock, text); this._sock.flush(); }, // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message requestDesktopSize: function (width, height) { if (this._rfb_state !== "normal") { return; } if (this._supportsSetDesktopSize) { RFB.messages.setDesktopSize(this._sock, width, height, this._screen_id, this._screen_flags); this._sock.flush(); } }, // 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._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 inflator.Inflate(); } }, _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"); } }, _cleanupSocket: function (state) { 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(); if (state !== 'connect' && state !== 'loaded') { 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(); }, /* * 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}) { this._cleanupSocket(state); } 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; this._sock.off('close'); // make sure we don't get a double event } 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; } }, _handleKeyPress: function (keysym, down) { if (this._view_only) { return; } // View only, skip keyboard, events RFB.messages.keyEvent(this._sock, keysym, down); this._sock.flush(); }, _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 the viewport didn't actually move, then treat as a mouse click event // Send the button down event here, as the button up event is sent at the end of this function if (!this._viewportHasMoved && !this._view_only) { RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); } this._viewportHasMoved = false; } } if (this._view_only) { return; } // View only, skip mouse events if (this._rfb_state !== "normal") { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, _handleMouseMove: function (x, y) { if (this._viewportDragging) { var deltaX = this._viewportDragPos.x - x; var deltaY = this._viewportDragPos.y - y; // The goal is to trigger on a certain physical width, the // devicePixelRatio brings us a bit closer but is not optimal. var dragThreshold = 10 * (window.devicePixelRatio || 1); if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { this._viewportHasMoved = true; this._viewportDragPos = {'x': x, 'y': y}; this._display.viewportChangePos(deltaX, deltaY); } // Skip sending mouse events return; } if (this._view_only) { return; } // View only, skip mouse events if (this._rfb_state !== "normal") { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, // 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); return false; } if (this._sock.rQwait("auth challenge", 16)) { return false; } // TODO(directxman12): make genDES not require an Array var challenge = Array.prototype.slice.call(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(); this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); /* 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)); this._fb_name = ((this._sock.rQshiftStr(name_length)) || 'DeskTop').toString(); 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; } // we don't actually do anything with the capability information that TIGHT sends, // so we just skip the all of this. // TIGHT server message capabilities this._sock.rQskipBytes(16 * numServerMessages); // TIGHT client message capabilities this._sock.rQskipBytes(16 * numClientMessages); // TIGHT encoding capabilities this._sock.rQskipBytes(16 * numEncodings); } // 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._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, 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; } RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; this._sock.flush(); 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) { RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); this._sock.flush(); } 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(); var handler = this._encHandlers[this._FBU.encoding]; try { //ret = this._encHandlers[this._FBU.encoding](); ret = handler(); } catch (ex) { console.log("missed " + this._FBU.encoding + ": " + handler); 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'],