text: /*
   * noVNC: HTML5 VNC client
   * Copyright (C) 2011 Joel Martin
   * Licensed under LGPL-3 (see LICENSE.txt)
   *
   * See README.md for usage and integration instructions.
   */
  
  /*jslint white: false, browser: true, bitwise: false, plusplus: false */
  /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, noVNC_logo */
  
  
  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,
      keyEvent, pointerEvent, clientCutText,
  
      extract_data_uri, scan_tight_imgQ,
      keyPress, mouseButton, mouseMove,
  
      checkEvents,  // Overridable for testing
  
  
      //
      // Private RFB namespace variables
      //
      rfb_host       = '',
      rfb_port       = 5900,
      rfb_password   = '',
      rfb_uri        = '',
  
      rfb_state      = 'disconnected',
      rfb_version    = 0,
      rfb_max_version= 3.8,
      rfb_auth_scheme= '',
  
  
      // In preference order
      encodings      = [
          ['COPYRECT',         0x01 ],
          ['TIGHT_PNG',        -260 ],
          ['HEXTILE',          0x05 ],
          ['RRE',              0x02 ],
          ['RAW',              0x00 ],
          ['DesktopSize',      -223 ],
          ['Cursor',           -239 ],
  
          // Psuedo-encoding settings
          ['JPEG_quality_lo',   -32 ],
          //['JPEG_quality_hi',   -23 ],
          ['compress_lo',      -255 ]
          //['compress_hi',      -247 ]
          ],
  
      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,
          imgQ           : []   // TIGHT_PNG image queue
      },
  
      fb_Bpp         = 4,
      fb_depth       = 3,
      fb_width       = 0,
      fb_height      = 0,
      fb_name        = "",
  
      scan_imgQ_rate = 40, // 25 times per second or so
      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
      },
  
      test_mode        = false,
  
      //def_con_timeout  = Websock_native ? 2 : 5,
  	def_con_timeout = 10,
  
      /* Mouse state */
      mouse_buttonMask = 0,
      mouse_arr        = [];
  
  // 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'],
  
      ['connectTimeout',     'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'],
      ['disconnectTimeout',  'rw', 'int', 3,    'Time (s) to wait for disconnection'],
  
      ['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");
      }
  	
  	console.log("conf.focusContainer: " + conf.focusContainer);
  	
      keyboard = new Keyboard({'target': conf.focusContainer,
                                  'onKeyPress': keyPress});
  
  /*								
      mouse = new Mouse({'target': conf.target,
                              'onMouseButton': mouseButton,
                              'onMouseMove': mouseMove}); */
  	
  	mouse = new Mouse({'target': conf.focusContainer,
                              'onMouseButton': mouseButton,
                              'onMouseMove': mouseMove});
  	
      rmode = display.get_render_mode();
  
      if (typeof noVNC_logo !== 'undefined') {
          display.set_logo(noVNC_logo);
      }
  
      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() {
          if (rfb_state === 'disconnect') {
              updateState('disconnected', 'VNC disconnected');
          } else if (rfb_state === 'ProtocolVersion') {
              fail('Failed to connect to server');
          } else if (rfb_state in {'failed':1, 'disconnected':1}) {
              Util.Error("Received onclose while disconnected");
          } else  {
              fail('Server disconnected');
          }
      });
      ws.on('error', function(e) {
          fail("WebSock error: " + e);
      });
  
  
      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 <a href='http://get.adobe.com/flashplayer'>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 (conf.encrypt) {
          uri = "wss://";
      } else {
          uri = "ws://";
      }
      uri += rfb_host + ":" + rfb_port + "/" + rfb_uri;
      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.imgQ         = []; // TIGHT_PNG image queue
      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;
      }
  };
  
  // 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;
  
  	Util.Debug("UpdateState function, RFB, state: " + state + " statusMSG: " + statusMsg);
  
      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}) {
  		Util.Debug("UpdateState function, RFB: State is disconnected");
          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();
              }
          }
  
  		Util.Debug("UpdateState function, RFB: Close");
          ws.close();
      }
  
      if (oldstate === 'fatal') {
          Util.Error("Fatal error, cannot continue");
      }
  
      if ((state === 'failed') || (state === 'fatal')) {
          func = Util.Error;
      } else {
          func = Util.Warn;
      }
  
      if ((oldstate === 'failed') && (state === 'disconnected')) {
          // Do disconnect action, but stay in failed state.
          rfb_state = 'failed';
      } else {
          rfb_state = state;
      }
  
      cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
      func("New state '" + rfb_state + "', was '" + oldstate + "'." + cmsg);
  
      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 () {
  				Util.Debug("SetTimeout function: Connect timeout, value:" + conf.connectTimeout * 1000);
                  fail("Connect timeout");
              }, 10 * 1000); // Was 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(fbUpdateRequest(1)));
          ws.send(mouse_arr);
          setTimeout(function() {
                  ws.send(fbUpdateRequest(1));
              }, 50);
  
          mouse_arr = [];
          return true;
      } else {
          return false;
      }
  }
  
  // overridable for testing
  checkEvents = function() {
      var now;
      if (rfb_state === 'normal') {
          if (! flushClient()) {
              now = new Date().getTime();
              if (now > last_req_time + conf.fbu_req_rate) {
                  last_req_time = now;
                  ws.send(fbUpdateRequest(1));
              }
          }
      }
      setTimeout(checkEvents, conf.check_rate);
  };
  
  keyPress = function(keysym, down) {
  	console.log(">>keypress");
      var arr;
      arr = keyEvent(keysym, down);
      arr = arr.concat(fbUpdateRequest(1));
      ws.send(arr);
  };
  
  mouseButton = function(x, y, down, bmask) {
  	console.log(">> mousebutton !!!");
      if (down) {
          mouse_buttonMask |= bmask;
      } else {
          mouse_buttonMask ^= bmask;
      }
      mouse_arr = mouse_arr.concat( pointerEvent(x, y) );
      flushClient();
  };
  
  mouseMove = function(x, y) {
      //Util.Debug('>> mouseMove ' + x + "," + y);
      mouse_arr = mouse_arr.concat( pointerEvent(x, 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,
          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;
  
      //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);
          switch (sversion) {
              case "003.003": rfb_version = 3.3; break;
              case "003.006": rfb_version = 3.3; break;  // UltraVNC
              case "003.007": rfb_version = 3.7; break;
              case "003.008": rfb_version = 3.8; break;
              default:
                  return fail("Invalid server version " + sversion);
          }
          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);
  
          /* Connection name/title */
          name_length   = ws.rQshift32();
          fb_name = ws.rQshiftStr(name_length);
  
          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(fbUpdateRequest(0));
          timing.fbu_rt_start = (new Date()).getTime();
          ws.send(response);
          
          /* Start pushing/polling */
          setTimeout(checkEvents, conf.check_rate);
          setTimeout(scan_tight_imgQ, scan_imgQ_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();
          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);
              Util.Debug("*** colourMap: " + display.get_colourMap());
              display.set_colourMap([red, green, blue], first_colour + c);
          }
          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;
          }
  
          if (FBU.rects === 0) {
              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; }
      old_x = ws.rQshift16();
      old_y = ws.rQshift16();
      display.copyImage(old_x, old_y, FBU.x, FBU.y, FBU.width, 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, tile, 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;
              }
  
              tile = display.getTile(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.setSubTile(tile, sx, sy, sw, sh, color);
                  }
              }
              display.putTile(tile);
          }
          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;
  };
  
  
  encHandlers.TIGHT_PNG = function display_tight_png() {
      //Util.Debug(">> display_tight_png");
      var ctl, cmode, clength, getCLength, color, img;
      //Util.Debug("   FBU.rects: " + FBU.rects);
      //Util.Debug("   starting ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
  
      FBU.bytes = 1; // compression-control byte
      if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; }
  
      // Get 'compact length' header and data size
      getCLength = 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];
      };
  
      ctl = ws.rQpeek8();
      switch (ctl >> 4) {
          case 0x08: cmode = "fill"; break;
          case 0x09: cmode = "jpeg"; break;
          case 0x0A: cmode = "png";  break;
          default:   throw("Illegal basic compression received, ctl: " + ctl);
      }
      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
      }
  
      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);
          FBU.imgQ.push({
                  'type': 'fill',
                  'img': {'complete': true},
                  'x': FBU.x,
                  'y': FBU.y,
                  'width': FBU.width,
                  'height': FBU.height,
                  'color': color});
          break;
      case "jpeg":
      case "png":
          clength = getCLength(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("   png, 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.onload = scan_tight_imgQ;
          FBU.imgQ.push({
                  'type': 'img',
                  'img': img,
                  'x': FBU.x,
                  'y': FBU.y});
          img.src = "data:image/" + cmode +
              extract_data_uri(ws.rQshiftBytes(clength[1]));
          img = null;
          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);
  };
  
  scan_tight_imgQ = function() {
      var data, imgQ, ctx;
      ctx = display.get_context();
      if (rfb_state === 'normal') {
          imgQ = FBU.imgQ;
          while ((imgQ.length > 0) && (imgQ[0].img.complete)) {
              data = imgQ.shift();
              if (data['type'] === 'fill') {
                  display.fillRect(data.x, data.y, data.width, data.height, data.color);
              } else {
                  ctx.drawImage(data.img, data.x, data.y);
              }
          }
          setTimeout(scan_tight_imgQ, scan_imgQ_rate);
      }
  };
  
  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(fbUpdateRequest(0));
  
      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(0);     // red-shift
      arr.push8(8);     // green-shift
      arr.push8(16);    // 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<encodings.length; i += 1) {
          if ((encodings[i][0] === "Cursor") &&
              (! conf.local_cursor)) {
              Util.Debug("Skipping Cursor pseudo-encoding");
          } else {
              //Util.Debug("Adding encoding: " + encodings[i][0]);
              encList.push(encodings[i][1]);
          }
      }
  
      arr = [2];     // msg-type
      arr.push8(0);  // padding
  
      arr.push16(encList.length); // encoding count
      for (i=0; i < encList.length; i += 1) {
          arr.push32(encList[i]);
      }
      //Util.Debug("<< clientEncodings: " + arr);
      return arr;
  };
  
  fbUpdateRequest = function(incremental, x, y, xw, yw) {
      //Util.Debug(">> fbUpdateRequest");
      if (!x) { x = 0; }
      if (!y) { y = 0; }
      if (!xw) { xw = fb_width; }
      if (!yw) { 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;
  };
  
  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, uri) {
      //Util.Debug(">> connect");
  
      rfb_host       = host;
      rfb_port       = port;
      rfb_password   = (password !== undefined)   ? password : "";
      rfb_uri        = (uri !== undefined) ? uri : "";
  
      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") { 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(fbUpdateRequest(1));
      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") { 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(fbUpdateRequest(1));
      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()