/* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin * Licensed under LGPL-3 (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, bitwise: false, plusplus: false */ /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ function RFB(defaults) { "use strict"; var that = {}, // Public API methods conf = {}, // Configuration attributes // Pre-declare private functions used before definitions (jslint) init_vars, updateState, fail, handle_message, init_msg, normal_msg, framebufferUpdate, print_stats, pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests, keyEvent, pointerEvent, clientCutText, getTightCLength, extract_data_uri, keyPress, mouseButton, mouseMove, checkEvents, // Overridable for testing // // Private RFB namespace variables // rfb_host = '', rfb_port = 5900, rfb_password = '', rfb_path = '', rfb_state = 'disconnected', rfb_version = 0, rfb_max_version= 3.8, rfb_auth_scheme= '', // In preference order 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 ] ], encHandlers = {}, encNames = {}, encStats = {}, // [rectCnt, rectCntTot] ws = null, // Websock object display = null, // Display object keyboard = null, // Keyboard input handler object mouse = null, // Mouse input handler object sendTimer = null, // Send Queue check timer connTimer = null, // connection timer disconnTimer = null, // disconnection timer msgTimer = null, // queued handle_message timer // Frame buffer update state 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, zlibs : [] // TIGHT zlib streams }, fb_Bpp = 4, fb_depth = 3, fb_width = 0, fb_height = 0, fb_name = "", last_req_time = 0, rre_chunk_sz = 100, 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 }, test_mode = false, def_con_timeout = Websock_native ? 2 : 5, /* Mouse state */ mouse_buttonMask = 0, mouse_arr = [], viewportDragging = false, viewportDragPos = {}; // Configuration attributes Util.conf_defaults(conf, that, defaults, [ ['target', 'wo', 'dom', null, 'VNC display rendering Canvas object'], ['focusContainer', 'wo', 'dom', document, 'DOM element that captures keyboard input'], ['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'], ['true_color', 'rw', 'bool', true, 'Request true color pixel data'], ['local_cursor', 'rw', 'bool', false, 'Request locally rendered cursor'], ['shared', 'rw', 'bool', true, 'Request shared mode'], ['view_only', 'rw', 'bool', false, 'Disable client mouse/keyboard'], ['connectTimeout', 'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'], ['disconnectTimeout', 'rw', 'int', 3, 'Time (s) to wait for disconnection'], // UltraVNC repeater ID to connect to ['repeaterID', 'rw', 'str', '', 'RepeaterID to connect to'], ['viewportDrag', 'rw', 'bool', false, 'Move the viewport on mouse drags'], ['check_rate', 'rw', 'int', 217, 'Timing (ms) of send/receive check'], ['fbu_req_rate', 'rw', 'int', 1413, 'Timing (ms) of frameBufferUpdate requests'], // Callback functions ['onUpdateState', 'rw', 'func', function() { }, 'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '], ['onPasswordRequired', 'rw', 'func', function() { }, 'onPasswordRequired(rfb): VNC password is required '], ['onClipboard', 'rw', 'func', function() { }, 'onClipboard(rfb, text): RFB clipboard contents received'], ['onBell', 'rw', 'func', function() { }, 'onBell(rfb): RFB Bell message received '], ['onFBUReceive', 'rw', 'func', function() { }, 'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '], ['onFBUComplete', 'rw', 'func', function() { }, 'onFBUComplete(rfb, fbu): RFB FBU received and processed '], // These callback names are deprecated ['updateState', 'rw', 'func', function() { }, 'obsolete, use onUpdateState'], ['clipboardReceive', 'rw', 'func', function() { }, 'obsolete, use onClipboard'] ]); // Override/add some specific configuration getters/setters that.set_local_cursor = function(cursor) { if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { conf.local_cursor = false; } else { if (display.get_cursor_uri()) { conf.local_cursor = true; } else { Util.Warn("Browser does not support local cursor"); } } }; // These are fake configuration getters that.get_display = function() { return display; }; that.get_keyboard = function() { return keyboard; }; that.get_mouse = function() { return mouse; }; // // Setup routines // // Create the public API interface and initialize values that stay // constant across connect/disconnect function constructor() { var i, rmode; Util.Debug(">> RFB.constructor"); // Create lookup tables based encoding number for (i=0; i < encodings.length; i+=1) { encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]]; encNames[encodings[i][1]] = encodings[i][0]; encStats[encodings[i][1]] = [0, 0]; } // Initialize display, mouse, keyboard, and websock try { display = new Display({'target': conf.target}); } catch (exc) { Util.Error("Display exception: " + exc); updateState('fatal', "No working Display"); } keyboard = new Keyboard({'target': conf.focusContainer, 'onKeyPress': keyPress}); mouse = new Mouse({'target': conf.target, 'onMouseButton': mouseButton, 'onMouseMove': mouseMove}); rmode = display.get_render_mode(); ws = new Websock(); ws.on('message', handle_message); ws.on('open', function() { if (rfb_state === "connect") { updateState('ProtocolVersion', "Starting VNC handshake"); } else { fail("Got unexpected WebSockets connection"); } }); ws.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 (rfb_state === 'disconnect') { updateState('disconnected', 'VNC disconnected' + msg); } else if (rfb_state === 'ProtocolVersion') { fail('Failed to connect to server' + msg); } else if (rfb_state in {'failed':1, 'disconnected':1}) { Util.Error("Received onclose while disconnected" + msg); } else { fail('Server disconnected' + msg); } }); ws.on('error', function(e) { Util.Warn("WebSocket on-error event"); //fail("WebSock reported an error"); }); init_vars(); /* Check web-socket-js if no builtin WebSocket support */ if (Websock_native) { Util.Info("Using native WebSockets"); 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)) { updateState('fatal', "WebSockets or Adobe Flash<\/a> is required"); } else if (document.location.href.substr(0, 7) === "file://") { updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); } else { updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); } } Util.Debug("<< RFB.constructor"); return that; // Return the public API interface } function connect() { Util.Debug(">> RFB.connect"); var uri; if (typeof UsingSocketIO !== "undefined") { uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path; } else { if (conf.encrypt) { uri = "wss://"; } else { uri = "ws://"; } uri += rfb_host + ":" + rfb_port + "/" + rfb_path; } Util.Info("connecting to " + uri); ws.open(uri); Util.Debug("<< RFB.connect"); } // Initialize variables that are reset before each connection init_vars = function() { var i; /* Reset state */ ws.init(); FBU.rects = 0; FBU.subrects = 0; // RRE and HEXTILE FBU.lines = 0; // RAW FBU.tiles = 0; // HEXTILE FBU.zlibs = []; // TIGHT zlib encoders mouse_buttonMask = 0; mouse_arr = []; // Clear the per connection encoding stats for (i=0; i < encodings.length; i+=1) { encStats[encodings[i][1]][0] = 0; } for (i=0; i < 4; i++) { //FBU.zlibs[i] = new InflateStream(); FBU.zlibs[i] = new TINF(); FBU.zlibs[i].init(); } }; // Print statistics print_stats = function() { var i, s; Util.Info("Encoding stats for this connection:"); for (i=0; i < encodings.length; i+=1) { s = encStats[encodings[i][1]]; if ((s[0] + s[1]) > 0) { Util.Info(" " + encodings[i][0] + ": " + s[0] + " rects"); } } Util.Info("Encoding stats since page load:"); for (i=0; i < encodings.length; i+=1) { s = encStats[encodings[i][1]]; if ((s[0] + s[1]) > 0) { Util.Info(" " + encodings[i][0] + ": " + s[1] + " rects"); } } }; // // Utility routines // /* * 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 func, cmsg, oldstate = rfb_state; if (state === oldstate) { /* Already here, ignore */ Util.Debug("Already in state '" + state + "', ignoring."); return; } /* * 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 (sendTimer) { clearInterval(sendTimer); sendTimer = null; } if (msgTimer) { clearInterval(msgTimer); msgTimer = null; } if (display && display.get_context()) { keyboard.ungrab(); mouse.ungrab(); display.defaultCursor(); if ((Util.get_logging() !== 'debug') || (state === 'loaded')) { // Show noVNC logo on load and when disconnected if // debug is off display.clear(); } } ws.close(); } if (oldstate === 'fatal') { Util.Error("Fatal error, cannot continue"); } if ((state === 'failed') || (state === 'fatal')) { func = Util.Error; } else { func = Util.Warn; } cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; func("New state '" + state + "', was '" + oldstate + "'." + cmsg); if ((oldstate === 'failed') && (state === 'disconnected')) { // Do disconnect action, but stay in failed state rfb_state = 'failed'; } else { rfb_state = state; } if (connTimer && (rfb_state !== 'connect')) { Util.Debug("Clearing connect timer"); clearInterval(connTimer); connTimer = null; } if (disconnTimer && (rfb_state !== 'disconnect')) { Util.Debug("Clearing disconnect timer"); clearInterval(disconnTimer); disconnTimer = null; } switch (state) { case 'normal': if ((oldstate === 'disconnected') || (oldstate === 'failed')) { Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); } break; case 'connect': connTimer = setTimeout(function () { fail("Connect timeout"); }, conf.connectTimeout * 1000); init_vars(); connect(); // WebSocket.onopen transitions to 'ProtocolVersion' break; case 'disconnect': if (! test_mode) { disconnTimer = setTimeout(function () { fail("Disconnect timeout"); }, conf.disconnectTimeout * 1000); } print_stats(); // WebSocket.onclose transitions to 'disconnected' break; case 'failed': if (oldstate === 'disconnected') { Util.Error("Invalid transition from 'disconnected' to 'failed'"); } if (oldstate === 'normal') { Util.Error("Error while connected."); } if (oldstate === 'init') { Util.Error("Error while initializing."); } // Make sure we transition to disconnected setTimeout(function() { updateState('disconnected'); }, 50); break; default: // No state change action to take } if ((oldstate === 'failed') && (state === 'disconnected')) { // Leave the failed message conf.updateState(that, state, oldstate); // Obsolete conf.onUpdateState(that, state, oldstate); } else { conf.updateState(that, state, oldstate, statusMsg); // Obsolete conf.onUpdateState(that, state, oldstate, statusMsg); } }; fail = function(msg) { updateState('failed', msg); return false; }; handle_message = function() { //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen()); //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); if (ws.rQlen() === 0) { Util.Warn("handle_message called on empty receive queue"); return; } switch (rfb_state) { case 'disconnected': case 'failed': Util.Error("Got data while disconnected"); break; case 'normal': if (normal_msg() && ws.rQlen() > 0) { // true means we can continue processing // Give other events a chance to run if (msgTimer === null) { Util.Debug("More data to process, creating timer"); msgTimer = setTimeout(function () { msgTimer = null; handle_message(); }, 10); } else { Util.Debug("More data to process, existing timer"); } } break; default: init_msg(); break; } }; function genDES(password, challenge) { var i, passwd = []; for (i=0; i < password.length; i += 1) { passwd.push(password.charCodeAt(i)); } return (new DES(passwd)).encrypt(challenge); } function flushClient() { if (mouse_arr.length > 0) { //send(mouse_arr.concat(fbUpdateRequests())); ws.send(mouse_arr); setTimeout(function() { ws.send(fbUpdateRequests()); }, 50); mouse_arr = []; return true; } else { return false; } } // overridable for testing checkEvents = function() { var now; if (rfb_state === 'normal' && !viewportDragging) { if (! flushClient()) { now = new Date().getTime(); if (now > last_req_time + conf.fbu_req_rate) { last_req_time = now; ws.send(fbUpdateRequests()); } } } setTimeout(checkEvents, conf.check_rate); }; keyPress = function(keysym, down) { var arr; if (conf.view_only) { return; } // View only, skip keyboard events arr = keyEvent(keysym, down); arr = arr.concat(fbUpdateRequests()); ws.send(arr); }; mouseButton = function(x, y, down, bmask) { if (down) { mouse_buttonMask |= bmask; } else { mouse_buttonMask ^= bmask; } if (conf.viewportDrag) { if (down && !viewportDragging) { viewportDragging = true; viewportDragPos = {'x': x, 'y': y}; // Skip sending mouse events return; } else { viewportDragging = false; ws.send(fbUpdateRequests()); // Force immediate redraw } } if (conf.view_only) { return; } // View only, skip mouse events mouse_arr = mouse_arr.concat( pointerEvent(display.absX(x), display.absY(y)) ); flushClient(); }; mouseMove = function(x, y) { //Util.Debug('>> mouseMove ' + x + "," + y); var deltaX, deltaY; if (viewportDragging) { //deltaX = x - viewportDragPos.x; // drag viewport deltaX = viewportDragPos.x - x; // drag frame buffer //deltaY = y - viewportDragPos.y; // drag viewport deltaY = viewportDragPos.y - y; // drag frame buffer viewportDragPos = {'x': x, 'y': y}; display.viewportChange(deltaX, deltaY); // Skip sending mouse events return; } if (conf.view_only) { return; } // View only, skip mouse events mouse_arr = mouse_arr.concat( pointerEvent(display.absX(x), display.absY(y)) ); }; // // Server message handlers // // RFB/VNC initialisation message handler init_msg = function() { //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); var strlen, reason, length, sversion, cversion, repeaterID, i, types, num_types, challenge, response, bpp, depth, big_endian, red_max, green_max, blue_max, red_shift, green_shift, blue_shift, true_color, name_length, is_repeater; //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0)); switch (rfb_state) { case 'ProtocolVersion' : if (ws.rQlen() < 12) { return fail("Incomplete protocol version"); } sversion = ws.rQshiftStr(12).substr(4,7); Util.Info("Server ProtocolVersion: " + sversion); is_repeater = 0; switch (sversion) { case "000.000": is_repeater = 1; break; // UltraVNC repeater case "003.003": rfb_version = 3.3; break; case "003.006": rfb_version = 3.3; break; // UltraVNC case "003.889": rfb_version = 3.3; break; // Apple Remote Desktop case "003.007": rfb_version = 3.7; break; case "003.008": rfb_version = 3.8; break; case "004.000": rfb_version = 3.8; break; // Intel AMT KVM case "004.001": rfb_version = 3.8; break; // RealVNC 4.6 default: return fail("Invalid server version " + sversion); } if (is_repeater) { repeaterID = conf.repeaterID; while (repeaterID.length < 250) { repeaterID += "\0"; } ws.send_string(repeaterID); break; } if (rfb_version > rfb_max_version) { rfb_version = rfb_max_version; } if (! test_mode) { sendTimer = setInterval(function() { // Send updates either at a rate of one update // every 50ms, or whatever slower rate the network // can handle. ws.flush(); }, 50); } cversion = "00" + parseInt(rfb_version,10) + ".00" + ((rfb_version * 10) % 10); ws.send_string("RFB " + cversion + "\n"); updateState('Security', "Sent ProtocolVersion: " + cversion); break; case 'Security' : if (rfb_version >= 3.7) { // Server sends supported list, client decides num_types = ws.rQshift8(); if (ws.rQwait("security type", num_types, 1)) { return false; } if (num_types === 0) { strlen = ws.rQshift32(); reason = ws.rQshiftStr(strlen); return fail("Security failure: " + reason); } rfb_auth_scheme = 0; types = ws.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); for (i=0; i < types.length; i+=1) { if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) { rfb_auth_scheme = types[i]; } } if (rfb_auth_scheme === 0) { return fail("Unsupported security types: " + types); } ws.send([rfb_auth_scheme]); } else { // Server decides if (ws.rQwait("security scheme", 4)) { return false; } rfb_auth_scheme = ws.rQshift32(); } updateState('Authentication', "Authenticating using scheme: " + rfb_auth_scheme); init_msg(); // Recursive fallthrough (workaround JSLint complaint) break; // Triggered by fallthough, not by server message case 'Authentication' : //Util.Debug("Security auth scheme: " + rfb_auth_scheme); switch (rfb_auth_scheme) { case 0: // connection failed if (ws.rQwait("auth reason", 4)) { return false; } strlen = ws.rQshift32(); reason = ws.rQshiftStr(strlen); return fail("Auth failure: " + reason); case 1: // no authentication if (rfb_version >= 3.8) { updateState('SecurityResult'); return; } // Fall through to ClientInitialisation break; case 2: // VNC authentication if (rfb_password.length === 0) { // Notify via both callbacks since it is kind of // a RFB state change and a UI interface issue. updateState('password', "Password Required"); conf.onPasswordRequired(that); return; } if (ws.rQwait("auth challenge", 16)) { return false; } challenge = ws.rQshiftBytes(16); //Util.Debug("Password: " + rfb_password); //Util.Debug("Challenge: " + challenge + // " (" + challenge.length + ")"); response = genDES(rfb_password, challenge); //Util.Debug("Response: " + response + // " (" + response.length + ")"); //Util.Debug("Sending DES encrypted auth response"); ws.send(response); updateState('SecurityResult'); return; default: fail("Unsupported auth scheme: " + rfb_auth_scheme); return; } updateState('ClientInitialisation', "No auth required"); init_msg(); // Recursive fallthrough (workaround JSLint complaint) break; case 'SecurityResult' : if (ws.rQwait("VNC auth response ", 4)) { return false; } switch (ws.rQshift32()) { case 0: // OK // Fall through to ClientInitialisation break; case 1: // failed if (rfb_version >= 3.8) { length = ws.rQshift32(); if (ws.rQwait("SecurityResult reason", length, 8)) { return false; } reason = ws.rQshiftStr(length); fail(reason); } else { fail("Authentication failed"); } return; case 2: // too-many return fail("Too many auth attempts"); } updateState('ClientInitialisation', "Authentication OK"); init_msg(); // Recursive fallthrough (workaround JSLint complaint) break; // Triggered by fallthough, not by server message case 'ClientInitialisation' : ws.send([conf.shared ? 1 : 0]); // ClientInitialisation updateState('ServerInitialisation', "Authentication OK"); break; case 'ServerInitialisation' : if (ws.rQwait("server initialization", 24)) { return false; } /* Screen size */ fb_width = ws.rQshift16(); fb_height = ws.rQshift16(); /* PIXEL_FORMAT */ bpp = ws.rQshift8(); depth = ws.rQshift8(); big_endian = ws.rQshift8(); true_color = ws.rQshift8(); red_max = ws.rQshift16(); green_max = ws.rQshift16(); blue_max = ws.rQshift16(); red_shift = ws.rQshift8(); green_shift = ws.rQshift8(); blue_shift = ws.rQshift8(); ws.rQshiftStr(3); // padding Util.Info("Screen: " + fb_width + "x" + 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"); } /* Connection name/title */ name_length = ws.rQshift32(); fb_name = ws.rQshiftStr(name_length); if (conf.true_color && fb_name === "Intel(r) AMT KVM") { Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color"); conf.true_color = false; } display.set_true_color(conf.true_color); display.resize(fb_width, fb_height); keyboard.grab(); mouse.grab(); if (conf.true_color) { fb_Bpp = 4; fb_depth = 3; } else { fb_Bpp = 1; fb_depth = 1; } response = pixelFormat(); response = response.concat(clientEncodings()); response = response.concat(fbUpdateRequests()); timing.fbu_rt_start = (new Date()).getTime(); timing.pixels = 0; ws.send(response); /* Start pushing/polling */ setTimeout(checkEvents, conf.check_rate); if (conf.encrypt) { updateState('normal', "Connected (encrypted) to: " + fb_name); } else { updateState('normal', "Connected (unencrypted) to: " + fb_name); } break; } //Util.Debug("<< init_msg"); }; /* Normal RFB/VNC server message handler */ normal_msg = function() { //Util.Debug(">> normal_msg"); var ret = true, msg_type, length, text, c, first_colour, num_colours, red, green, blue; if (FBU.rects > 0) { msg_type = 0; } else { msg_type = ws.rQshift8(); } switch (msg_type) { case 0: // FramebufferUpdate ret = framebufferUpdate(); // false means need more data break; case 1: // SetColourMapEntries Util.Debug("SetColourMapEntries"); ws.rQshift8(); // Padding first_colour = ws.rQshift16(); // First colour num_colours = ws.rQshift16(); if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; } for (c=0; c < num_colours; c+=1) { red = ws.rQshift16(); //Util.Debug("red before: " + red); red = parseInt(red / 256, 10); //Util.Debug("red after: " + red); green = parseInt(ws.rQshift16() / 256, 10); blue = parseInt(ws.rQshift16() / 256, 10); display.set_colourMap([blue, green, red], first_colour + c); } Util.Debug("colourMap: " + display.get_colourMap()); Util.Info("Registered " + num_colours + " colourMap entries"); //Util.Debug("colourMap: " + display.get_colourMap()); break; case 2: // Bell Util.Debug("Bell"); conf.onBell(that); break; case 3: // ServerCutText Util.Debug("ServerCutText"); if (ws.rQwait("ServerCutText header", 7, 1)) { return false; } ws.rQshiftBytes(3); // Padding length = ws.rQshift32(); if (ws.rQwait("ServerCutText", length, 8)) { return false; } text = ws.rQshiftStr(length); conf.clipboardReceive(that, text); // Obsolete conf.onClipboard(that, text); break; default: fail("Disconnected: illegal server message type " + msg_type); Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); break; } //Util.Debug("<< normal_msg"); return ret; }; framebufferUpdate = function() { var now, hdr, fbu_rt_diff, ret = true; if (FBU.rects === 0) { //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20)); if (ws.rQwait("FBU header", 3)) { ws.rQunshift8(0); // FBU msg_type return false; } ws.rQshift8(); // padding FBU.rects = ws.rQshift16(); //Util.Debug("FramebufferUpdate, rects:" + FBU.rects); FBU.bytes = 0; timing.cur_fbu = 0; if (timing.fbu_rt_start > 0) { now = (new Date()).getTime(); Util.Info("First FBU latency: " + (now - timing.fbu_rt_start)); } } while (FBU.rects > 0) { if (rfb_state !== "normal") { return false; } if (ws.rQwait("FBU", FBU.bytes)) { return false; } if (FBU.bytes === 0) { if (ws.rQwait("rect header", 12)) { return false; } /* New FramebufferUpdate */ hdr = ws.rQshiftBytes(12); FBU.x = (hdr[0] << 8) + hdr[1]; FBU.y = (hdr[2] << 8) + hdr[3]; FBU.width = (hdr[4] << 8) + hdr[5]; FBU.height = (hdr[6] << 8) + hdr[7]; FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + (hdr[10] << 8) + hdr[11], 10); conf.onFBUReceive(that, {'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height, 'encoding': FBU.encoding, 'encodingName': encNames[FBU.encoding]}); if (encNames[FBU.encoding]) { // Debug: /* var msg = "FramebufferUpdate rects:" + FBU.rects; msg += " x: " + FBU.x + " y: " + FBU.y; msg += " width: " + FBU.width + " height: " + FBU.height; msg += " encoding:" + FBU.encoding; msg += "(" + encNames[FBU.encoding] + ")"; msg += ", ws.rQlen(): " + ws.rQlen(); Util.Debug(msg); */ } else { fail("Disconnected: unsupported encoding " + FBU.encoding); return false; } } timing.last_fbu = (new Date()).getTime(); ret = encHandlers[FBU.encoding](); now = (new Date()).getTime(); timing.cur_fbu += (now - timing.last_fbu); if (ret) { encStats[FBU.encoding][0] += 1; encStats[FBU.encoding][1] += 1; timing.pixels += FBU.width * FBU.height; } if (timing.pixels >= (fb_width * fb_height)) { if (((FBU.width === fb_width) && (FBU.height === fb_height)) || (timing.fbu_rt_start > 0)) { timing.full_fbu_total += timing.cur_fbu; timing.full_fbu_cnt += 1; Util.Info("Timing of full FBU, cur: " + timing.cur_fbu + ", total: " + timing.full_fbu_total + ", cnt: " + timing.full_fbu_cnt + ", avg: " + (timing.full_fbu_total / timing.full_fbu_cnt)); } if (timing.fbu_rt_start > 0) { fbu_rt_diff = now - timing.fbu_rt_start; timing.fbu_rt_total += fbu_rt_diff; timing.fbu_rt_cnt += 1; Util.Info("full FBU round-trip, cur: " + fbu_rt_diff + ", total: " + timing.fbu_rt_total + ", cnt: " + timing.fbu_rt_cnt + ", avg: " + (timing.fbu_rt_total / timing.fbu_rt_cnt)); timing.fbu_rt_start = 0; } } if (! ret) { return ret; // false ret means need more data } } conf.onFBUComplete(that, {'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height, 'encoding': FBU.encoding, 'encodingName': encNames[FBU.encoding]}); return true; // We finished this FBU }; // // FramebufferUpdate encodings // encHandlers.RAW = function display_raw() { //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)"); var cur_y, cur_height; if (FBU.lines === 0) { FBU.lines = FBU.height; } FBU.bytes = FBU.width * fb_Bpp; // At least a line if (ws.rQwait("RAW", FBU.bytes)) { return false; } cur_y = FBU.y + (FBU.height - FBU.lines); cur_height = Math.min(FBU.lines, Math.floor(ws.rQlen()/(FBU.width * fb_Bpp))); display.blitImage(FBU.x, cur_y, FBU.width, cur_height, ws.get_rQ(), ws.get_rQi()); ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp); FBU.lines -= cur_height; if (FBU.lines > 0) { FBU.bytes = FBU.width * fb_Bpp; // At least another line } else { FBU.rects -= 1; FBU.bytes = 0; } //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)"); return true; }; encHandlers.COPYRECT = function display_copy_rect() { //Util.Debug(">> display_copy_rect"); var old_x, old_y; if (ws.rQwait("COPYRECT", 4)) { return false; } display.renderQ_push({ 'type': 'copy', 'old_x': ws.rQshift16(), 'old_y': ws.rQshift16(), 'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height}); FBU.rects -= 1; FBU.bytes = 0; return true; }; encHandlers.RRE = function display_rre() { //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)"); var color, x, y, width, height, chunk; if (FBU.subrects === 0) { if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; } FBU.subrects = ws.rQshift32(); color = ws.rQshiftBytes(fb_Bpp); // Background display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); } while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) { color = ws.rQshiftBytes(fb_Bpp); x = ws.rQshift16(); y = ws.rQshift16(); width = ws.rQshift16(); height = ws.rQshift16(); display.fillRect(FBU.x + x, FBU.y + y, width, height, color); FBU.subrects -= 1; } //Util.Debug(" display_rre: rects: " + FBU.rects + // ", FBU.subrects: " + FBU.subrects); if (FBU.subrects > 0) { chunk = Math.min(rre_chunk_sz, FBU.subrects); FBU.bytes = (fb_Bpp + 8) * chunk; } else { FBU.rects -= 1; FBU.bytes = 0; } //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes); return true; }; encHandlers.HEXTILE = function display_hextile() { //Util.Debug(">> display_hextile"); var subencoding, subrects, color, cur_tile, tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh, rQ = ws.get_rQ(), rQi = ws.get_rQi(); if (FBU.tiles === 0) { FBU.tiles_x = Math.ceil(FBU.width/16); FBU.tiles_y = Math.ceil(FBU.height/16); FBU.total_tiles = FBU.tiles_x * FBU.tiles_y; FBU.tiles = FBU.total_tiles; } /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */ while (FBU.tiles > 0) { FBU.bytes = 1; if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; } subencoding = rQ[rQi]; // Peek if (subencoding > 30) { // Raw fail("Disconnected: illegal hextile subencoding " + subencoding); //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); return false; } subrects = 0; cur_tile = FBU.total_tiles - FBU.tiles; tile_x = cur_tile % FBU.tiles_x; tile_y = Math.floor(cur_tile / FBU.tiles_x); x = FBU.x + tile_x * 16; y = FBU.y + tile_y * 16; w = Math.min(16, (FBU.x + FBU.width) - x); h = Math.min(16, (FBU.y + FBU.height) - y); /* Figure out how much we are expecting */ if (subencoding & 0x01) { // Raw //Util.Debug(" Raw subencoding"); FBU.bytes += w * h * fb_Bpp; } else { if (subencoding & 0x02) { // Background FBU.bytes += fb_Bpp; } if (subencoding & 0x04) { // Foreground FBU.bytes += fb_Bpp; } if (subencoding & 0x08) { // AnySubrects FBU.bytes += 1; // Since we aren't shifting it off if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; } subrects = rQ[rQi + FBU.bytes-1]; // Peek if (subencoding & 0x10) { // SubrectsColoured FBU.bytes += subrects * (fb_Bpp + 2); } else { FBU.bytes += subrects * 2; } } } /* Util.Debug(" tile:" + cur_tile + "/" + (FBU.total_tiles - 1) + " (" + tile_x + "," + tile_y + ")" + " [" + x + "," + y + "]@" + w + "x" + h + ", subenc:" + subencoding + "(last: " + FBU.lastsubencoding + "), subrects:" + subrects + ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes + " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) + " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10)); */ if (ws.rQwait("hextile", FBU.bytes)) { return false; } /* We know the encoding and have a whole tile */ FBU.subencoding = rQ[rQi]; rQi += 1; if (FBU.subencoding === 0) { if (FBU.lastsubencoding & 0x01) { /* Weird: ignore blanks after RAW */ Util.Debug(" Ignoring blank after RAW"); } else { display.fillRect(x, y, w, h, FBU.background); } } else if (FBU.subencoding & 0x01) { // Raw display.blitImage(x, y, w, h, rQ, rQi); rQi += FBU.bytes - 1; } else { if (FBU.subencoding & 0x02) { // Background FBU.background = rQ.slice(rQi, rQi + fb_Bpp); rQi += fb_Bpp; } if (FBU.subencoding & 0x04) { // Foreground FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp); rQi += fb_Bpp; } display.startTile(x, y, w, h, FBU.background); if (FBU.subencoding & 0x08) { // AnySubrects subrects = rQ[rQi]; rQi += 1; for (s = 0; s < subrects; s += 1) { if (FBU.subencoding & 0x10) { // SubrectsColoured color = rQ.slice(rQi, rQi + fb_Bpp); rQi += fb_Bpp; } else { color = FBU.foreground; } xy = rQ[rQi]; rQi += 1; sx = (xy >> 4); sy = (xy & 0x0f); wh = rQ[rQi]; rQi += 1; sw = (wh >> 4) + 1; sh = (wh & 0x0f) + 1; display.subTile(sx, sy, sw, sh, color); } } display.finishTile(); } ws.set_rQi(rQi); FBU.lastsubencoding = FBU.subencoding; FBU.bytes = 0; FBU.tiles -= 1; } if (FBU.tiles === 0) { FBU.rects -= 1; } //Util.Debug("<< display_hextile"); return true; }; // Get 'compact length' header and data size getTightCLength = function (arr) { var header = 1, data = 0; data += arr[0] & 0x7f; if (arr[0] & 0x80) { header += 1; data += (arr[1] & 0x7f) << 7; if (arr[1] & 0x80) { header += 1; data += arr[2] << 14; } } return [header, data]; }; function display_tight(isTightPNG) { //Util.Debug(">> display_tight"); if (fb_depth === 1) { fail("Tight protocol handler only implements true color mode"); } var ctl, cmode, clength, color, img, data; var filterId = -1, resetStreams = 0, streamId = -1; var rQ = ws.get_rQ(), rQi = ws.get_rQi(); FBU.bytes = 1; // compression-control byte if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; } var checksum = function(data) { var sum=0, i; for (i=0; i 65536) sum -= 65536; } return sum; } var decompress = function(data) { for (var i=0; i<4; i++) { if ((resetStreams >> i) & 1) { FBU.zlibs[i].reset(); Util.Info("Reset zlib stream " + i); } } var uncompressed = FBU.zlibs[streamId].uncompress(data, 0); if (uncompressed.status !== 0) { Util.Error("Invalid data in zlib stream"); } //Util.Warn("Decompressed " + data.length + " to " + // uncompressed.data.length + " checksums " + // checksum(data) + ":" + checksum(uncompressed.data)); return uncompressed.data; } var handlePalette = function() { var numColors = rQ[rQi + 2] + 1; var paletteSize = numColors * fb_depth; FBU.bytes += paletteSize; if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; } var bpp = (numColors <= 2) ? 1 : 8; var rowSize = Math.floor((FBU.width * bpp + 7) / 8); var raw = false; if (rowSize * FBU.height < 12) { raw = true; clength = [0, rowSize * FBU.height]; } else { clength = getTightCLength(ws.rQslice(3 + paletteSize, 3 + paletteSize + 3)); } FBU.bytes += clength[0] + clength[1]; if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } // Shift ctl, filter id, num colors, palette entries, and clength off ws.rQshiftBytes(3); var palette = ws.rQshiftBytes(paletteSize); ws.rQshiftBytes(clength[0]); if (raw) { data = ws.rQshiftBytes(clength[1]); } else { data = decompress(ws.rQshiftBytes(clength[1])); } // Convert indexed (palette based) image data to RGB // TODO: reduce number of calculations inside loop var dest = []; var x, y, b, w, w1, dp, sp; if (numColors === 2) { w = Math.floor((FBU.width + 7) / 8); w1 = Math.floor(FBU.width / 8); for (y = 0; y < FBU.height; y++) { for (x = 0; x < w1; x++) { for (b = 7; b >= 0; b--) { dp = (y*FBU.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 - FBU.width % 8; b--) { dp = (y*FBU.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 < FBU.height; y++) { for (x = 0; x < FBU.width; x++) { dp = (y*FBU.width + x) * 3; sp = data[y*FBU.width + x] * 3; dest[dp ] = palette[sp ]; dest[dp+1] = palette[sp+1]; dest[dp+2] = palette[sp+2]; } } } display.renderQ_push({ 'type': 'blitRgb', 'data': dest, 'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height}); return true; } var handleCopy = function() { var raw = false; var uncompressedSize = FBU.width * FBU.height * fb_depth; if (uncompressedSize < 12) { raw = true; clength = [0, uncompressedSize]; } else { clength = getTightCLength(ws.rQslice(1, 4)); } FBU.bytes = 1 + clength[0] + clength[1]; if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } // Shift ctl, clength off ws.rQshiftBytes(1 + clength[0]); if (raw) { data = ws.rQshiftBytes(clength[1]); } else { data = decompress(ws.rQshiftBytes(clength[1])); } display.renderQ_push({ 'type': 'blitRgb', 'data': data, 'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height}); return true; } ctl = ws.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 fail("Illegal tight compression received, ctl: " + ctl); if (isTightPNG && (cmode === "filter" || cmode === "copy")) { return fail("filter/copy received in tightPNG mode"); } switch (cmode) { // fill uses fb_depth because TPIXELs drop the padding byte case "fill": FBU.bytes += fb_depth; break; // TPIXEL case "jpeg": FBU.bytes += 3; break; // max clength case "png": FBU.bytes += 3; break; // max clength case "filter": FBU.bytes += 2; break; // filter id + num colors if palette case "copy": break; } if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } //Util.Debug(" ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); //Util.Debug(" cmode: " + cmode); // Determine FBU.bytes switch (cmode) { case "fill": ws.rQshift8(); // shift off ctl color = ws.rQshiftBytes(fb_depth); display.renderQ_push({ 'type': 'fill', 'x': FBU.x, 'y': FBU.y, 'width': FBU.width, 'height': FBU.height, 'color': [color[2], color[1], color[0]] }); break; case "png": case "jpeg": clength = getTightCLength(ws.rQslice(1, 4)); FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } // We have everything, render it //Util.Debug(" jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " + // clength[0] + ", clength[1]: " + clength[1]); ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length img = new Image(); img.src = "data:image/" + cmode + extract_data_uri(ws.rQshiftBytes(clength[1])); display.renderQ_push({ 'type': 'img', 'img': img, 'x': FBU.x, 'y': FBU.y}); img = null; break; case "filter": 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 used if jpeg is enabled throw("Unsupported tight subencoding received, filter: " + filterId); } break; case "copy": if (!handleCopy()) { return false; } break; } FBU.bytes = 0; FBU.rects -= 1; //Util.Debug(" ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); //Util.Debug("<< display_tight_png"); return true; } extract_data_uri = function(arr) { //var i, stra = []; //for (i=0; i< arr.length; i += 1) { // stra.push(String.fromCharCode(arr[i])); //} //return "," + escape(stra.join('')); return ";base64," + Base64.encode(arr); }; encHandlers.TIGHT = function () { return display_tight(false); }; encHandlers.TIGHT_PNG = function () { return display_tight(true); }; encHandlers.last_rect = function last_rect() { //Util.Debug(">> last_rect"); FBU.rects = 0; //Util.Debug("<< last_rect"); return true; }; encHandlers.DesktopSize = function set_desktopsize() { Util.Debug(">> set_desktopsize"); fb_width = FBU.width; fb_height = FBU.height; display.resize(fb_width, fb_height); timing.fbu_rt_start = (new Date()).getTime(); // Send a new non-incremental request ws.send(fbUpdateRequests()); FBU.bytes = 0; FBU.rects -= 1; Util.Debug("<< set_desktopsize"); return true; }; encHandlers.Cursor = function set_cursor() { var x, y, w, h, pixelslength, masklength; Util.Debug(">> set_cursor"); x = FBU.x; // hotspot-x y = FBU.y; // hotspot-y w = FBU.width; h = FBU.height; pixelslength = w * h * fb_Bpp; masklength = Math.floor((w + 7) / 8) * h; FBU.bytes = pixelslength + masklength; if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; } //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); display.changeCursor(ws.rQshiftBytes(pixelslength), ws.rQshiftBytes(masklength), x, y, w, h); FBU.bytes = 0; FBU.rects -= 1; Util.Debug("<< set_cursor"); return true; }; encHandlers.JPEG_quality_lo = function set_jpeg_quality() { Util.Error("Server sent jpeg_quality pseudo-encoding"); }; encHandlers.compress_lo = function set_compress_level() { Util.Error("Server sent compress level pseudo-encoding"); }; /* * Client message routines */ pixelFormat = function() { //Util.Debug(">> pixelFormat"); var arr; arr = [0]; // msg-type arr.push8(0); // padding arr.push8(0); // padding arr.push8(0); // padding arr.push8(fb_Bpp * 8); // bits-per-pixel arr.push8(fb_depth * 8); // depth arr.push8(0); // little-endian arr.push8(conf.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 //Util.Debug("<< pixelFormat"); return arr; }; clientEncodings = function() { //Util.Debug(">> clientEncodings"); var arr, i, encList = []; for (i=0; i> fbUpdateRequest"); if (typeof(x) === "undefined") { x = 0; } if (typeof(y) === "undefined") { y = 0; } if (typeof(xw) === "undefined") { xw = fb_width; } if (typeof(yw) === "undefined") { yw = fb_height; } var arr; arr = [3]; // msg-type arr.push8(incremental); arr.push16(x); arr.push16(y); arr.push16(xw); arr.push16(yw); //Util.Debug("<< fbUpdateRequest"); return arr; }; // Based on clean/dirty areas, generate requests to send fbUpdateRequests = function() { var cleanDirty = display.getCleanDirtyReset(), arr = [], i, cb, db; cb = cleanDirty.cleanBox; if (cb.w > 0 && cb.h > 0) { // Request incremental for clean box arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h)); } for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) { db = cleanDirty.dirtyBoxes[i]; // Force all (non-incremental for dirty box arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h)); } return arr; }; keyEvent = function(keysym, down) { //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down); var arr; arr = [4]; // msg-type arr.push8(down); arr.push16(0); arr.push32(keysym); //Util.Debug("<< keyEvent"); return arr; }; pointerEvent = function(x, y) { //Util.Debug(">> pointerEvent, x,y: " + x + "," + y + // " , mask: " + mouse_buttonMask); var arr; arr = [5]; // msg-type arr.push8(mouse_buttonMask); arr.push16(x); arr.push16(y); //Util.Debug("<< pointerEvent"); return arr; }; clientCutText = function(text) { //Util.Debug(">> clientCutText"); var arr, i, n; arr = [6]; // msg-type arr.push8(0); // padding arr.push8(0); // padding arr.push8(0); // padding arr.push32(text.length); n = text.length; for (i=0; i < n; i+=1) { arr.push(text.charCodeAt(i)); } //Util.Debug("<< clientCutText:" + arr); return arr; }; // // Public API interface functions // that.connect = function(host, port, password, path) { //Util.Debug(">> connect"); rfb_host = host; rfb_port = port; rfb_password = (password !== undefined) ? password : ""; rfb_path = (path !== undefined) ? path : ""; if ((!rfb_host) || (!rfb_port)) { return fail("Must set host and port"); } updateState('connect'); //Util.Debug("<< connect"); }; that.disconnect = function() { //Util.Debug(">> disconnect"); updateState('disconnect', 'Disconnecting'); //Util.Debug("<< disconnect"); }; that.sendPassword = function(passwd) { rfb_password = passwd; rfb_state = "Authentication"; setTimeout(init_msg, 1); }; that.sendCtrlAltDel = function() { if (rfb_state !== "normal" || conf.view_only) { return false; } Util.Info("Sending Ctrl-Alt-Del"); var arr = []; arr = arr.concat(keyEvent(0xFFE3, 1)); // Control arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt arr = arr.concat(keyEvent(0xFFE3, 0)); // Control arr = arr.concat(fbUpdateRequests()); ws.send(arr); }; // Send a key press. If 'down' is not specified then send a down key // followed by an up key. that.sendKey = function(code, down) { if (rfb_state !== "normal" || conf.view_only) { return false; } var arr = []; if (typeof down !== 'undefined') { Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); arr = arr.concat(keyEvent(code, down ? 1 : 0)); } else { Util.Info("Sending key code (down + up): " + code); arr = arr.concat(keyEvent(code, 1)); arr = arr.concat(keyEvent(code, 0)); } arr = arr.concat(fbUpdateRequests()); ws.send(arr); }; that.clipboardPasteFrom = function(text) { if (rfb_state !== "normal") { return; } //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); ws.send(clientCutText(text)); //Util.Debug("<< clipboardPasteFrom"); }; // Override internal functions for testing that.testMode = function(override_send) { test_mode = true; that.recv_message = ws.testMode(override_send); checkEvents = function () { /* Stub Out */ }; that.connect = function(host, port, password) { rfb_host = host; rfb_port = port; rfb_password = password; updateState('ProtocolVersion', "Starting VNC handshake"); }; }; return constructor(); // Return the public API interface } // End of RFB()