]> git.proxmox.com Git - mirror_novnc.git/blobdiff - include/rfb.js
New API. Refactor Canvas and RFB objects.
[mirror_novnc.git] / include / rfb.js
index ab0aacf5c2b1fd1b2d703c2806045b777bf5640c..766f6eb9d5aeee5bac69705b2a27b28178490aaa 100644 (file)
  */
 
 "use strict";
-/*jslint white: false, nomen: false, browser: true, bitwise: false */
+/*jslint white: false, browser: true, bitwise: false */
 /*global window, WebSocket, Util, Canvas, VNC_native_ws, Base64, DES */
 
-// Globals defined here
-var RFB;
 
-/*
- * RFB namespace
- */
-
-RFB = {
-
-/* 
- * External interface variables and methods
- */
-host           : '',
-port           : 5900,
-password       : '',
-
-encrypt        : true,
-true_color     : false,
-b64encode      : true,  // false means UTF-8 on the wire
-local_cursor   : true,
-connectTimeout : 2000,  // time to wait for connection
-
-
-// In preference order
-encodings      : [
-    ['COPYRECT',         0x01, 'display_copy_rect'],
-    ['TIGHT_PNG',        -260, 'display_tight_png'],
-    ['HEXTILE',          0x05, 'display_hextile'],
-    ['RRE',              0x02, 'display_rre'],
-    ['RAW',              0x00, 'display_raw'],
-    ['DesktopSize',      -223, 'set_desktopsize'],
-    ['Cursor',           -239, 'set_cursor'],
-
-    // Psuedo-encoding settings
-    ['JPEG_quality_lo',   -32, 'set_jpeg_quality'],
-//    ['JPEG_quality_hi',   -23, 'set_jpeg_quality'],
-    ['compress_lo',      -255, 'set_compress_level']
-//    ['compress_hi',      -247, 'set_compress_level']
-    ],
-
-
-setUpdateState: function(externalUpdateState) {
-    RFB.externalUpdateState = externalUpdateState;
-},
-
-setClipboardReceive: function(clipReceive) {
-    RFB.clipboardCopyTo = clipReceive;
-},
-
-setCanvasID: function(canvasID) {
-    RFB.canvasID = canvasID;
-},
-
-setEncrypt: function(encrypt) {
-    if ((!encrypt) || (encrypt in {'0':1, 'no':1, 'false':1})) {
-        RFB.encrypt = false;
-    } else {
-        RFB.encrypt = true;
-    }
-},
-
-setBase64: function(b64) {
-    if ((!b64) || (b64 in {'0':1, 'no':1, 'false':1})) {
-        RFB.b64encode = false;
-    } else {
-        RFB.b64encode = true;
-    }
-    Util.Debug("Set b64encode to: " + RFB.b64encode);
-},
-
-setTrueColor: function(trueColor) {
-    if ((!trueColor) || (trueColor in {'0':1, 'no':1, 'false':1})) {
-        RFB.true_color = false;
-    } else {
-        RFB.true_color = true;
-    }
-},
-
-setCursor: function(cursor) {
+function RFB(conf) {
+
+conf               = conf || {}; // Configuration
+var that           = {},         // Public API interface
+
+    // Pre-declare private functions used before definitions (jslint)
+    init_vars, updateState, init_msg, normal_msg, recv_message,
+    framebufferUpdate, show_timings,
+
+    pixelFormat, clientEncodings, fbUpdateRequest,
+    keyEvent, pointerEvent, clientCutText,
+
+    extract_data_uri, scan_tight_imgs,
+
+    send_array, checkEvents,  // Overridable for testing
+
+
+    //
+    // Private RFB namespace variables
+    //
+    rfb_host       = '',
+    rfb_port       = 5900,
+    rfb_password   = '',
+
+    rfb_state      = 'disconnected',
+    rfb_version    = 0,
+    rfb_max_version= 3.8,
+    rfb_auth_scheme= '',
+    rfb_shared     = 1,
+
+
+    // 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       = {}, 
+
+    ws             = null,  // Web Socket object
+    canvas         = null,  // Canvas object
+    sendID         = null,  // Send Queue check timer
+
+    // Receive and send queues
+    RQ             = [],  // Receive Queue
+    SQ             = "",  // Send Queue
+
+    // Frame buffer update state
+    FBU            = {
+        rects          : 0,
+        subrects       : 0,  // RRE and HEXTILE
+        lines          : 0,  // RAW
+        tiles          : 0,  // HEXTILE
+        bytes          : 0,
+        x              : 0,
+        y              : 0,
+        width          : 0, 
+        height         : 0,
+        encoding       : 0,
+        subencoding    : -1,
+        background     : null,
+        imgs           : []  // TIGHT_PNG image queue
+    },
+
+    fb_Bpp         = 4,
+    fb_depth       = 3,
+    fb_width       = 0,
+    fb_height      = 0,
+    fb_name        = "",
+
+    cuttext        = 'none', // ServerCutText wait state
+    cuttext_length = 0,
+
+    scan_imgs_rate = 100,
+    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,
+
+        history        : [],
+        history_start  : 0,
+        h_time         : 0,
+        h_rects        : 0,
+        h_fbus         : 0,
+        h_bytes        : 0,
+        h_pixels       : 0
+    },
+
+    test_mode        = false,
+
+    /* Mouse state */
+    mouse_buttonMask = 0,
+    mouse_arr        = [];
+
+
+//
+// Configuration settings
+//
+
+// VNC viewport rendering Canvas
+Util.conf_default(conf, that, 'target', 'VNC_canvas');
+
+Util.conf_default(conf, that, 'encrypt',        false, true);
+Util.conf_default(conf, that, 'true_color',     true, true);
+// false means UTF-8 on the wire
+Util.conf_default(conf, that, 'b64encode',      true, true);
+Util.conf_default(conf, that, 'local_cursor',   true, true);
+
+// time to wait for connection
+Util.conf_default(conf, that, 'connectTimeout', 2000);
+// frequency to check for send/receive
+Util.conf_default(conf, that, 'check_rate',     217);
+// frequency to send frameBufferUpdate requests
+Util.conf_default(conf, that, 'fbu_req_rate',   1413);
+
+// state update callback
+Util.conf_default(conf, that, 'updateState', function () {
+        Util.Debug(">> externalUpdateState stub"); });
+// clipboard contents received callback
+Util.conf_default(conf, that, 'clipboardReceive', function () {
+    Util.Debug(">> clipboardReceive stub"); });
+
+
+// Override/add some specific getters/setters
+that.set_local_cursor = function(cursor) {
     if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) {
-        RFB.local_cursor = false;
+        conf.local_cursor = false;
     } else {
-        if (Canvas.isCursor()) {
-            RFB.local_cursor = true;
+        if (canvas.get_cursor_uri()) {
+            conf.local_cursor = true;
         } else {
             Util.Warn("Browser does not support local cursor");
         }
     }
-},
+};
 
-sendPassword: function(passwd) {
-    RFB.password = passwd;
-    RFB.state = "Authentication";
-    setTimeout(RFB.init_msg, 1);
-},
+that.get_canvas = function() {
+    return canvas;
+};
 
-sendCtrlAltDel: function() {
-    if (RFB.state !== "normal") { return false; }
-    Util.Info("Sending Ctrl-Alt-Del");
-    var arr = [];
-    arr = arr.concat(RFB.keyEvent(0xFFE3, 1)); // Control
-    arr = arr.concat(RFB.keyEvent(0xFFE9, 1)); // Alt
-    arr = arr.concat(RFB.keyEvent(0xFFFF, 1)); // Delete
-    arr = arr.concat(RFB.keyEvent(0xFFFF, 0)); // Delete
-    arr = arr.concat(RFB.keyEvent(0xFFE9, 0)); // Alt
-    arr = arr.concat(RFB.keyEvent(0xFFE3, 0)); // Control
-    arr = arr.concat(RFB.fbUpdateRequest(1));
-    RFB.send_array(arr);
-},
-
-load: function () {
+
+
+
+//
+// Private functions
+//
+
+//
+// Setup routines
+//
+
+// Create the public API interface
+function constructor() {
     var i;
-    //Util.Debug(">> load");
+    //Util.Debug(">> init");
 
-    /* Load web-socket-js if no builtin WebSocket support */
-    if (VNC_native_ws) {
-        Util.Info("Using native WebSockets");
-        RFB.updateState('loaded', 'noVNC ready (using native WebSockets)');
-    } else {
-        Util.Warn("Using web-socket-js flash bridge");
-        if ((! Util.Flash) ||
-            (Util.Flash.version < 9)) {
-            RFB.updateState('fatal', "WebSockets or Adobe Flash is required");
-        } else if (document.location.href.substr(0, 7) === "file://") {
-            RFB.updateState('fatal',
-                    "'file://' URL is incompatible with Adobe Flash");
-        } else {
-            RFB.updateState('loaded', 'noVNC ready (using Flash WebSockets emulation)');
-        }
+    // 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];
     }
-
-    // Initialize canvas/fxcanvas
+    // Initialize canvas
     try {
-        Canvas.init(RFB.canvasID);
+        canvas = new Canvas({'target': conf.target});
     } catch (exc) {
-        RFB.updateState('fatal', "No working Canvas");
+        Util.Error("Canvas exception: " + exc);
+        updateState('fatal', "No working Canvas");
     }
 
-    // Populate encoding lookup tables
-    RFB.encHandlers = {};
-    RFB.encNames = {};
-    for (i=0; i < RFB.encodings.length; i+=1) {
-        RFB.encHandlers[RFB.encodings[i][1]] = RFB[RFB.encodings[i][2]];
-        RFB.encNames[RFB.encodings[i][1]] = RFB.encodings[i][0];
-    }
-    //Util.Debug("<< load");
-},
-
-connect: function (host, port, password) {
-    //Util.Debug(">> connect");
+    //Util.Debug("<< init");
+    return that;  // Return the public API interface
+}
 
-    RFB.host       = host;
-    RFB.port       = port;
-    RFB.password   = (password !== undefined)   ? password : "";
+function init_ws() {
+    //Util.Debug(">> init_ws");
 
-    if ((!RFB.host) || (!RFB.port)) {
-        RFB.updateState('failed', "Must set host and port");
-        return;
+    var uri = "", vars = [];
+    if (conf.encrypt) {
+        uri = "wss://";
+    } else {
+        uri = "ws://";
+    }
+    uri += rfb_host + ":" + rfb_port + "/";
+    if (conf.b64encode) {
+        vars.push("b64encode");
     }
+    if (vars.length > 0) {
+        uri += "?" + vars.join("&");
+    }
+    Util.Info("connecting to " + uri);
+    ws = new WebSocket(uri);
 
-    RFB.updateState('connect');
-    //Util.Debug("<< connect");
+    ws.onmessage = recv_message;
+    ws.onopen = function(e) {
+        Util.Debug(">> WebSocket.onopen");
+        if (rfb_state === "connect") {
+            updateState('ProtocolVersion', "Starting VNC handshake");
+        } else {
+            updateState('failed', "Got unexpected WebSockets connection");
+        }
+        Util.Debug("<< WebSocket.onopen");
+    };
+    ws.onclose = function(e) {
+        Util.Debug(">> WebSocket.onclose");
+        if (rfb_state === 'normal') {
+            updateState('failed', 'Server disconnected');
+        } else if (rfb_state === 'ProtocolVersion') {
+            updateState('failed', 'Failed to connect to server');
+        } else  {
+            updateState('disconnected', 'VNC disconnected');
+        }
+        Util.Debug("<< WebSocket.onclose");
+    };
+    ws.onerror = function(e) {
+        Util.Debug(">> WebSocket.onerror");
+        updateState('failed', "WebSocket error");
+        Util.Debug("<< WebSocket.onerror");
+    };
 
-},
+    setTimeout(function () {
+            if (ws.readyState === WebSocket.CONNECTING) {
+                updateState('failed', "Connect timeout");
+            }
+        }, conf.connectTimeout);
 
-disconnect: function () {
-    //Util.Debug(">> disconnect");
-    RFB.updateState('disconnected', 'Disconnected');
-    //Util.Debug("<< disconnect");
-},
+    //Util.Debug("<< init_ws");
+}
 
-clipboardPasteFrom: function (text) {
-    if (RFB.state !== "normal") { return; }
-    //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "...");
-    RFB.send_array(RFB.clientCutText(text));
-    //Util.Debug("<< clipboardPasteFrom");
-},
+init_vars = function() {
+    /* Reset state */
+    cuttext          = 'none';
+    cuttext_length   = 0;
+    RQ               = [];
+    SQ               = "";
+    FBU.rects        = 0;
+    FBU.subrects     = 0;  // RRE and HEXTILE
+    FBU.lines        = 0;  // RAW
+    FBU.tiles        = 0;  // HEXTILE
+    FBU.imgs         = []; // TIGHT_PNG image queue
+    mouse_buttonMask = 0;
+    mouse_arr        = [];
+
+    timing.history_start = 0;
+    timing.history       = [];
+    timing.h_fbus        = 0;
+    timing.h_rects       = 0;
+    timing.h_bytes       = 0;
+    timing.h_pixels      = 0;
+};
+
+//
+// Utility routines
+//
 
 
 /*
- * Private variables and methods
+ * Running states:
+ *   disconnected - idle state
+ *   normal       - connected
+ *
+ * Page states:
+ *   loaded       - page load, equivalent to disconnected
+ *   connect      - starting initialization
+ *   password     - waiting for password
+ *   failed       - abnormal transition to disconnected
+ *   fatal        - failed to load page, or fatal error
+ *
+ * VNC initialization states:
+ *   ProtocolVersion
+ *   Security
+ *   Authentication
+ *   SecurityResult
+ *   ServerInitialization
  */
+updateState = function(state, statusMsg) {
+    var func, cmsg, oldstate = rfb_state;
+    if (state === oldstate) {
+        /* Already here, ignore */
+        Util.Debug("Already in state '" + state + "', ignoring.");
+        return;
+    }
 
-ws             : null,  // Web Socket object
-sendID         : null,
-scanID         : null,  // TIGHT_PNG render image scanner
-
-// Receive and send queues
-RQ             : [],  // Receive Queue
-SQ             : "",  // Send Queue
-
-encHandlers    : {},
-encNames       : {},
-
-// Frame buffer update state
-FBU            : {
-    rects          : 0,
-    subrects       : 0,  // RRE and HEXTILE
-    lines          : 0,  // RAW
-    tiles          : 0,  // HEXTILE
-    bytes          : 0,
-    x              : 0,
-    y              : 0,
-    width          : 0, 
-    height         : 0,
-    encoding       : 0,
-    subencoding    : -1,
-    background     : null,
-    imgs           : []  // TIGHT_PNG image queue
-},
-
-fb_Bpp         : 4,
-fb_depth       : 3,
-
-max_version    : 3.8,
-version        : 0,
-auth_scheme    : '',
-state          : 'disconnected',
-cuttext        : 'none', // ServerCutText wait state
-ct_length      : 0,
-
-shared         : 1,
-check_rate     : 217,
-scan_imgs_rate : 100,
-req_rate       : 1413,
-last_req       : 0,
-
-canvasID       : 'VNC_canvas',
-fb_width       : 0,
-fb_height      : 0,
-fb_name        : "",
-rre_chunk      : 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,
-
-    history        : [],
-    history_start  : 0,
-    h_time         : 0,
-    h_rects        : 0,
-    h_fbus         : 0,
-    h_bytes        : 0,
-    h_pixels       : 0
-},
-
-/* Mouse state */
-mouse_buttonmask : 0,
-mouse_arr        : [],
+    if (oldstate === 'fatal') {
+        Util.Error("Fatal error, cannot continue");
+    }
 
-/*
- * Server message handlers
- */
+    if ((state === 'failed') || (state === 'fatal')) {
+        func = Util.Error;
+    } else {
+        func = Util.Warn;
+    }
 
-/* RFB/VNC initialisation */
-init_msg: function () {
-    //Util.Debug(">> init_msg [RFB.state '" + RFB.state + "']");
+    cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
+    func("New state '" + state + "', was '" + oldstate + "'." + cmsg);
 
-    var RQ = RFB.RQ, strlen, reason, reason_len, sversion, cversion,
-        i, types, num_types, challenge, response, bpp, depth,
-        big_endian, true_color, name_length;
+    if ((oldstate === 'failed') && (state === 'disconnected')) {
+        // Do disconnect action, but stay in failed state.
+        rfb_state = 'failed';
+    } else {
+        rfb_state = state;
+    }
 
-    //Util.Debug("RQ (" + RQ.length + ") " + RQ);
-    switch (RFB.state) {
+    switch (state) {
+    case 'loaded':
+    case 'disconnected':
 
-    case 'ProtocolVersion' :
-        if (RQ.length < 12) {
-            RFB.updateState('failed',
-                    "Disconnected: incomplete protocol version");
-            return;
+        if (sendID) {
+            clearInterval(sendID);
+            sendID = null;
         }
-        sversion = RQ.shiftStr(12).substr(4,7);
-        Util.Info("Server ProtocolVersion: " + sversion);
-        switch (sversion) {
-            case "003.003": RFB.version = 3.3; break;
-            case "003.007": RFB.version = 3.7; break;
-            case "003.008": RFB.version = 3.8; break;
-            default:
-                RFB.updateState('failed',
-                        "Invalid server version " + sversion);
-                return;
+
+        if (ws) {
+            if (ws.readyState === WebSocket.OPEN) {
+                ws.close();
+            }
+            ws.onmessage = function (e) { return; };
         }
-        if (RFB.version > RFB.max_version) { 
-            RFB.version = RFB.max_version;
+
+        if (canvas && canvas.getContext()) {
+            canvas.stop();
+            if (! /__debug__$/i.test(document.location.href)) {
+                canvas.clear();
+            }
         }
 
-        RFB.sendID = setInterval(function() {
-                /*
-                 * Send updates either at a rate of one update every 50ms,
-                 * or whatever slower rate the network can handle
-                 */
-                if (RFB.ws.bufferedAmount === 0) {
-                    if (RFB.SQ) {
-                        RFB.ws.send(RFB.SQ);
-                        RFB.SQ = "";
-                    }
-                } else {
-                    Util.Debug("Delaying send");
-                }
-            }, 50);
+        show_timings();
 
-        cversion = "00" + parseInt(RFB.version,10) +
-                   ".00" + ((RFB.version * 10) % 10);
-        RFB.send_string("RFB " + cversion + "\n");
-        RFB.updateState('Security', "Sent ProtocolVersion: " + sversion);
         break;
 
-    case 'Security' :
-        if (RFB.version >= 3.7) {
-            num_types = RQ.shift8();
-            if (num_types === 0) {
-                strlen = RQ.shift32();
-                reason = RQ.shiftStr(strlen);
-                RFB.updateState('failed',
-                        "Disconnected: security failure: " + reason);
-                return;
-            }
-            RFB.auth_scheme = 0;
-            types = RQ.shiftBytes(num_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) {
-                RFB.updateState('failed',
-                        "Disconnected: unsupported security types: " + types);
-                return;
-            }
-            
-            RFB.send_array([RFB.auth_scheme]);
-        } else {
-            if (RQ.length < 4) {
-                RFB.updateState('failed', "Invalid security frame");
-                return;
-            }
-            RFB.auth_scheme = RQ.shift32();
-        }
-        RFB.updateState('Authentication',
-                "Authenticating using scheme: " + RFB.auth_scheme);
-        // Fall through
 
-    case 'Authentication' :
-        //Util.Debug("Security auth scheme: " + RFB.auth_scheme);
-        switch (RFB.auth_scheme) {
-            case 0:  // connection failed
-                if (RQ.length < 4) {
-                    //Util.Debug("   waiting for auth reason bytes");
-                    return;
-                }
-                strlen = RQ.shift32();
-                reason = RQ.shiftStr(strlen);
-                RFB.updateState('failed',
-                        "Disconnected: auth failure: " + reason);
-                return;
-            case 1:  // no authentication
-                // RFB.send_array([RFB.shared]); // ClientInitialisation
-                RFB.updateState('SecurityResult');
-                break;
-            case 2:  // VNC authentication
-                if (RFB.password.length === 0) {
-                    RFB.updateState('password', "Password Required");
-                    return;
-                }
-                if (RQ.length < 16) {
-                    //Util.Debug("   waiting for auth challenge bytes");
-                    return;
-                }
-                challenge = RQ.shiftBytes(16);
-                //Util.Debug("Password: " + RFB.password);
-                //Util.Debug("Challenge: " + challenge +
-                //           " (" + challenge.length + ")");
-                response = RFB.DES(RFB.password, challenge);
-                //Util.Debug("Response: " + response +
-                //           " (" + response.length + ")");
-                
-                //Util.Debug("Sending DES encrypted auth response");
-                RFB.send_array(response);
-                RFB.updateState('SecurityResult');
-                break;
-            default:
-                RFB.updateState('failed',
-                        "Disconnected: unsupported auth scheme: " +
-                        RFB.auth_scheme);
-                return;
-        }
-        break;
+    case 'connect':
+        init_vars();
 
-    case 'SecurityResult' :
-        if (RQ.length < 4) {
-            RFB.updateState('failed', "Invalid VNC auth response");
-            return;
+        if ((ws) && (ws.readyState === WebSocket.OPEN)) {
+            ws.close();
         }
-        switch (RQ.shift32()) {
-            case 0:  // OK
-                RFB.updateState('ServerInitialisation', "Authentication OK");
-                break;
-            case 1:  // failed
-                if (RFB.version >= 3.8) {
-                    reason_len = RQ.shift32();
-                    reason = RQ.shiftStr(reason_len);
-                    RFB.updateState('failed', reason);
-                } else {
-                    RFB.updateState('failed', "Authentication failed");
-                }
-                return;
-            case 2:  // too-many
-                RFB.updateState('failed',
-                        "Disconnected: too many auth attempts");
-                return;
-        }
-        RFB.send_array([RFB.shared]); // ClientInitialisation
+        init_ws(); // onopen transitions to 'ProtocolVersion'
+
         break;
 
-    case 'ServerInitialisation' :
-        if (RQ.length < 24) {
-            RFB.updateState('failed', "Invalid server initialisation");
-            return;
-        }
 
-        /* Screen size */
-        RFB.fb_width  = RQ.shift16();
-        RFB.fb_height = RQ.shift16();
+    case 'password':
+        // Ignore password state by default
+        break;
 
-        /* PIXEL_FORMAT */
-        bpp            = RQ.shift8();
-        depth          = RQ.shift8();
-        big_endian     = RQ.shift8();
-        true_color     = RQ.shift8();
 
-        Util.Info("Screen: " + RFB.fb_width + "x" + RFB.fb_height + 
-                  ", bpp: " + bpp + ", depth: " + depth +
-                  ", big_endian: " + big_endian +
-                  ", true_color: " + true_color);
+    case 'normal':
+        if ((oldstate === 'disconnected') || (oldstate === 'failed')) {
+            Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
+        }
 
-        /* Connection name/title */
-        RQ.shiftStr(12);
-        name_length   = RQ.shift32();
-        RFB.fb_name = RQ.shiftStr(name_length);
+        break;
 
-        Canvas.resize(RFB.fb_width, RFB.fb_height, RFB.true_color);
-        Canvas.start(RFB.keyPress, RFB.mouseButton, RFB.mouseMove);
 
-        if (RFB.true_color) {
-            RFB.fb_Bpp           = 4;
-            RFB.fb_depth         = 3;
-        } else {
-            RFB.fb_Bpp           = 1;
-            RFB.fb_depth         = 1;
+    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.");
         }
 
-        response = RFB.pixelFormat();
-        response = response.concat(RFB.clientEncodings());
-        response = response.concat(RFB.fbUpdateRequest(0));
-        RFB.timing.fbu_rt_start = (new Date()).getTime();
-        RFB.send_array(response);
-        
-        /* Start pushing/polling */
-        setTimeout(RFB.checkEvents, RFB.check_rate);
-        setTimeout(RFB.scan_tight_imgs, RFB.scan_imgs_rate);
-        RFB.timing.history_start = (new Date()).getTime();
-        setTimeout(RFB.update_timings, 1000);
-
-        if (RFB.encrypt) {
-            RFB.updateState('normal', "Connected (encrypted) to: " + RFB.fb_name);
-        } else {
-            RFB.updateState('normal', "Connected (unencrypted) to: " + RFB.fb_name);
+        if ((ws) && (ws.readyState === WebSocket.OPEN)) {
+            ws.close();
         }
+        // Make sure we transition to disconnected
+        setTimeout(function() { updateState('disconnected'); }, 50);
+
         break;
-    }
-    //Util.Debug("<< init_msg");
-},
 
 
-/* Normal RFB/VNC server messages */
-normal_msg: function () {
-    //Util.Debug(">> normal_msg");
+    default:
+        // Invalid state transition
 
-    var RQ = RFB.RQ, ret = true, msg_type,
-        c, first_colour, num_colours, red, green, blue;
+    }
 
-    //Util.Debug(">> msg RQ.slice(0,10): " + RQ.slice(0,20));
-    //Util.Debug(">> msg RQ.slice(-10,-1): " + RQ.slice(RQ.length-10,RQ.length));
-    if (RFB.FBU.rects > 0) {
-        msg_type = 0;
-    } else if (RFB.cuttext !== 'none') {
+    if ((oldstate === 'failed') && (state === 'disconnected')) {
+        // Leave the failed message
+        conf.updateState(that, state, oldstate);
+    } else {
+        conf.updateState(that, state, oldstate, statusMsg);
+    }
+};
+
+function encode_message(arr) {
+    if (conf.b64encode) {
+        /* base64 encode */
+        SQ = SQ + Base64.encode(arr);
+    } else {
+        /* UTF-8 encode. 0 -> 256 to avoid WebSockets framing */
+        SQ = SQ + arr.map(function (num) {
+                if (num === 0) {
+                    return String.fromCharCode(256);
+                } else {
+                    return String.fromCharCode(num);
+                }
+            } ).join('');
+    }
+}
+
+function decode_message(data) {
+    var i, length;
+    //Util.Debug(">> decode_message: " + data);
+    if (conf.b64encode) {
+        /* base64 decode */
+        RQ = RQ.concat(Base64.decode(data, 0));
+    } else {
+        /* UTF-8 decode. 256 -> 0 to WebSockets framing */
+        length = data.length;
+        for (i=0; i < length; i += 1) {
+            RQ.push(data.charCodeAt(i) % 256);
+        }
+    }
+    //Util.Debug(">> decode_message, RQ: " + RQ);
+}
+
+function handle_message() {
+    //Util.Debug("RQ.slice(0,20): " + RQ.slice(0,20) + " (" + RQ.length + ")");
+    if (RQ.length == 0) {
+        Util.Warn("handle_message called on empty receive queue");
+        return;
+    }
+    switch (rfb_state) {
+    case 'disconnected':
+        Util.Error("Got data while disconnected");
+        break;
+    case 'failed':
+        Util.Warn("Giving up!");
+        that.disconnect();
+        break;
+    case 'normal':
+        if (normal_msg() && RQ.length > 0) {
+            // true means we can continue processing
+            Util.Debug("More data to process");
+            // Give other events a chance to run
+            setTimeout(handle_message, 10);
+        }
+        break;
+    default:
+        init_msg();
+        break;
+    }
+}
+
+recv_message = function(e) {
+    //Util.Debug(">> recv_message");
+
+    try {
+        decode_message(e.data);
+        if (RQ.length > 0) {
+            handle_message();
+        } else {
+            Util.Debug("Ignoring empty message");
+        }
+    } catch (exc) {
+        if (typeof exc.stack !== 'undefined') {
+            Util.Warn("recv_message, caught exception: " + exc.stack);
+        } else if (typeof exc.description !== 'undefined') {
+            Util.Warn("recv_message, caught exception: " + exc.description);
+        } else {
+            Util.Warn("recv_message, caught exception:" + exc);
+        }
+        if (typeof exc.name !== 'undefined') {
+            updateState('failed', exc.name + ": " + exc.message);
+        } else {
+            updateState('failed', exc);
+        }
+    }
+    //Util.Debug("<< recv_message");
+};
+
+// overridable for testing
+send_array = function(arr) {
+    //Util.Debug(">> send_array: " + arr);
+    encode_message(arr);
+    if (ws.bufferedAmount === 0) {
+        //Util.Debug("arr: " + arr);
+        //Util.Debug("SQ: " + SQ);
+        ws.send(SQ);
+        SQ = "";
+    } else {
+        Util.Debug("Delaying send");
+    }
+};
+
+function send_string(str) {
+    //Util.Debug(">> send_string: " + str);
+    send_array(str.split('').map(
+        function (chr) { return chr.charCodeAt(0); } ) );
+}
+
+function genDES(password, challenge) {
+    var i, passwd, response;
+    passwd = [];
+    response = challenge.slice();
+    for (i=0; i < password.length; i += 1) {
+        passwd.push(password.charCodeAt(i));
+    }
+
+    DES.setKeys(passwd);
+    DES.encrypt(response, 0, response, 0);
+    DES.encrypt(response, 8, response, 8);
+    return response;
+}
+
+function flushClient() {
+    if (mouse_arr.length > 0) {
+        //send_array(mouse_arr.concat(fbUpdateRequest(1)));
+        send_array(mouse_arr);
+        setTimeout(function() {
+                send_array(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;
+                send_array(fbUpdateRequest(1));
+            }
+        }
+    }
+    setTimeout(checkEvents, conf.check_rate);
+};
+
+function keyPress(keysym, down) {
+    var arr;
+    arr = keyEvent(keysym, down);
+    arr = arr.concat(fbUpdateRequest(1));
+    send_array(arr);
+}
+
+function mouseButton(x, y, down, bmask) {
+    if (down) {
+        mouse_buttonMask |= bmask;
+    } else {
+        mouse_buttonMask ^= bmask;
+    }
+    mouse_arr = mouse_arr.concat( pointerEvent(x, y) );
+    flushClient();
+}
+
+function mouseMove(x, y) {
+    //Util.Debug('>> mouseMove ' + x + "," + y);
+    mouse_arr = mouse_arr.concat( pointerEvent(x, y) );
+}
+
+
+function update_timings() {
+    var now, offset;
+    now = (new Date()).getTime();
+    timing.history.push([now,
+            timing.h_fbus,
+            timing.h_rects,
+            timing.h_bytes,
+            timing.h_pixels]);
+    timing.h_fbus = 0;
+    timing.h_rects = 0;
+    timing.h_bytes = 0;
+    timing.h_pixels = 0;
+    if ((rfb_state !== 'disconnected') && (rfb_state !== 'failed')) {
+        // Try for every second
+        offset = (now - timing.history_start) % 1000;
+        if (offset < 500) {
+            setTimeout(update_timings, 1000 - offset);
+        } else {
+            setTimeout(update_timings, 2000 - offset);
+        }
+    }
+}
+
+show_timings = function() {
+    var i, history, msg,
+        delta, tot_time = 0, tot_fbus = 0, tot_rects = 0,
+        tot_bytes = 0, tot_pixels = 0;
+    if (timing.history_start === 0) { return; }
+    //Util.Debug(">> show_timings");
+    update_timings();  // Final accumulate
+    msg = "\nTimings\n";
+    msg += "  time: fbus,rects,bytes,pixels\n";
+    for (i=0; i < timing.history.length; i += 1) {
+        history = timing.history[i];
+        delta = ((history[0]-timing.history_start)/1000);
+        tot_time = delta;
+        tot_fbus += history[1];
+        tot_rects += history[2];
+        tot_bytes += history[3];
+        tot_pixels += history[4];
+
+        msg += "  " + delta.toFixed(3);
+        msg += ": " + history.slice(1) + "\n";
+    }
+    msg += "\nTotals:\n";
+    msg += "  time: fbus,rects,bytes,pixels\n";
+    msg += "  " + tot_time.toFixed(3);
+    msg += ": " + tot_fbus + "," + tot_rects;
+    msg += "," + tot_bytes + "," + tot_pixels;
+    Util.Info(msg);
+    //Util.Debug("<< show_timings");
+};
+
+//
+// Server message handlers
+//
+
+// RFB/VNC initialisation message handler
+init_msg = function() {
+    //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']");
+
+    var strlen, reason, reason_len, sversion, cversion,
+        i, types, num_types, challenge, response, bpp, depth,
+        big_endian, true_color, name_length;
+
+    //Util.Debug("RQ (" + RQ.length + ") " + RQ);
+    switch (rfb_state) {
+
+    case 'ProtocolVersion' :
+        if (RQ.length < 12) {
+            updateState('failed',
+                    "Disconnected: incomplete protocol version");
+            return;
+        }
+        sversion = RQ.shiftStr(12).substr(4,7);
+        Util.Info("Server ProtocolVersion: " + sversion);
+        switch (sversion) {
+            case "003.003": rfb_version = 3.3; break;
+            case "003.007": rfb_version = 3.7; break;
+            case "003.008": rfb_version = 3.8; break;
+            default:
+                updateState('failed',
+                        "Invalid server version " + sversion);
+                return;
+        }
+        if (rfb_version > rfb_max_version) { 
+            rfb_version = rfb_max_version;
+        }
+
+        if (! test_mode) {
+            sendID = setInterval(function() {
+                    // Send updates either at a rate of one update
+                    // every 50ms, or whatever slower rate the network
+                    // can handle.
+                    if (ws.bufferedAmount === 0) {
+                        if (SQ) {
+                            ws.send(SQ);
+                            SQ = "";
+                        }
+                    } else {
+                        Util.Debug("Delaying send");
+                    }
+                }, 50);
+        }
+
+        cversion = "00" + parseInt(rfb_version,10) +
+                   ".00" + ((rfb_version * 10) % 10);
+        send_string("RFB " + cversion + "\n");
+        updateState('Security', "Sent ProtocolVersion: " + sversion);
+        break;
+
+    case 'Security' :
+        if (rfb_version >= 3.7) {
+            num_types = RQ.shift8();
+            if (num_types === 0) {
+                strlen = RQ.shift32();
+                reason = RQ.shiftStr(strlen);
+                updateState('failed',
+                        "Disconnected: security failure: " + reason);
+                return;
+            }
+            rfb_auth_scheme = 0;
+            types = RQ.shiftBytes(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) {
+                updateState('failed',
+                        "Disconnected: unsupported security types: " + types);
+                return;
+            }
+            
+            send_array([rfb_auth_scheme]);
+        } else {
+            if (RQ.length < 4) {
+                updateState('failed', "Invalid security frame");
+                return;
+            }
+            rfb_auth_scheme = RQ.shift32();
+        }
+        updateState('Authentication',
+                "Authenticating using scheme: " + rfb_auth_scheme);
+        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
+        break;
+
+    case 'Authentication' :
+        //Util.Debug("Security auth scheme: " + rfb_auth_scheme);
+        switch (rfb_auth_scheme) {
+            case 0:  // connection failed
+                if (RQ.length < 4) {
+                    //Util.Debug("   waiting for auth reason bytes");
+                    return;
+                }
+                strlen = RQ.shift32();
+                reason = RQ.shiftStr(strlen);
+                updateState('failed',
+                        "Disconnected: auth failure: " + reason);
+                return;
+            case 1:  // no authentication
+                updateState('SecurityResult');
+                break;
+            case 2:  // VNC authentication
+                if (rfb_password.length === 0) {
+                    updateState('password', "Password Required");
+                    return;
+                }
+                if (RQ.length < 16) {
+                    //Util.Debug("   waiting for auth challenge bytes");
+                    return;
+                }
+                challenge = RQ.shiftBytes(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");
+                send_array(response);
+                updateState('SecurityResult');
+                break;
+            default:
+                updateState('failed',
+                        "Disconnected: unsupported auth scheme: " +
+                        rfb_auth_scheme);
+                return;
+        }
+        break;
+
+    case 'SecurityResult' :
+        if (RQ.length < 4) {
+            updateState('failed', "Invalid VNC auth response");
+            return;
+        }
+        switch (RQ.shift32()) {
+            case 0:  // OK
+                updateState('ServerInitialisation', "Authentication OK");
+                break;
+            case 1:  // failed
+                if (rfb_version >= 3.8) {
+                    reason_len = RQ.shift32();
+                    reason = RQ.shiftStr(reason_len);
+                    updateState('failed', reason);
+                } else {
+                    updateState('failed', "Authentication failed");
+                }
+                return;
+            case 2:  // too-many
+                updateState('failed',
+                        "Disconnected: too many auth attempts");
+                return;
+        }
+        send_array([rfb_shared]); // ClientInitialisation
+        break;
+
+    case 'ServerInitialisation' :
+        if (RQ.length < 24) {
+            updateState('failed', "Invalid server initialisation");
+            return;
+        }
+
+        /* Screen size */
+        fb_width  = RQ.shift16();
+        fb_height = RQ.shift16();
+
+        /* PIXEL_FORMAT */
+        bpp            = RQ.shift8();
+        depth          = RQ.shift8();
+        big_endian     = RQ.shift8();
+        true_color     = RQ.shift8();
+
+        Util.Info("Screen: " + fb_width + "x" + fb_height + 
+                  ", bpp: " + bpp + ", depth: " + depth +
+                  ", big_endian: " + big_endian +
+                  ", true_color: " + true_color);
+
+        /* Connection name/title */
+        RQ.shiftStr(12);
+        name_length   = RQ.shift32();
+        fb_name = RQ.shiftStr(name_length);
+
+        canvas.resize(fb_width, fb_height, conf.true_color);
+        canvas.start(keyPress, mouseButton, mouseMove);
+
+        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();
+        send_array(response);
+        
+        /* Start pushing/polling */
+        setTimeout(checkEvents, conf.check_rate);
+        setTimeout(scan_tight_imgs, scan_imgs_rate);
+        timing.history_start = (new Date()).getTime();
+        setTimeout(update_timings, 1000);
+
+        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,
+        c, first_colour, num_colours, red, green, blue;
+
+    //Util.Debug(">> msg RQ.slice(0,10): " + RQ.slice(0,20));
+    //Util.Debug(">> msg RQ.slice(-10,-1): " + RQ.slice(RQ.length-10,RQ.length));
+    if (FBU.rects > 0) {
+        msg_type = 0;
+    } else if (cuttext !== 'none') {
         msg_type = 3;
     } else {
         msg_type = RQ.shift8();
     }
     switch (msg_type) {
     case 0:  // FramebufferUpdate
-        ret = RFB.framebufferUpdate();
+        ret = framebufferUpdate(); // false means need more data
         break;
     case 1:  // SetColourMapEntries
         Util.Debug("SetColourMapEntries");
@@ -523,10 +904,10 @@ normal_msg: function () {
             //Util.Debug("red after: " + red);
             green = parseInt(RQ.shift16() / 256, 10);
             blue = parseInt(RQ.shift16() / 256, 10);
-            Canvas.colourMap[first_colour + c] = [red, green, blue];
+            canvas.set_colourMap([red, green, blue], first_colour + c);
         }
         Util.Info("Registered " + num_colours + " colourMap entries");
-        //Util.Debug("colourMap: " + Canvas.colourMap);
+        //Util.Debug("colourMap: " + canvas.get_colourMap());
         break;
     case 2:  // Bell
         Util.Warn("Bell (unsupported)");
@@ -534,39 +915,37 @@ normal_msg: function () {
     case 3:  // ServerCutText
         Util.Debug("ServerCutText");
         Util.Debug("RQ:" + RQ.slice(0,20));
-        if (RFB.cuttext === 'none') {
-            RFB.cuttext = 'header';
+        if (cuttext === 'none') {
+            cuttext = 'header';
         }
-        if (RFB.cuttext === 'header') {
+        if (cuttext === 'header') {
             if (RQ.length < 7) {
                 //Util.Debug("waiting for ServerCutText header");
                 return false;
             }
             RQ.shiftBytes(3);  // Padding
-            RFB.ct_length = RQ.shift32();
+            cuttext_length = RQ.shift32();
         }
-        RFB.cuttext = 'bytes';
-        if (RQ.length < RFB.ct_length) {
+        cuttext = 'bytes';
+        if (RQ.length < cuttext_length) {
             //Util.Debug("waiting for ServerCutText bytes");
             return false;
         }
-        RFB.clipboardCopyTo(RQ.shiftStr(RFB.ct_length));
-        RFB.cuttext = 'none';
+        conf.clipboardReceive(that, RQ.shiftStr(cuttext_length));
+        cuttext = 'none';
         break;
     default:
-        RFB.updateState('failed',
+        updateState('failed',
                 "Disconnected: illegal server message type " + msg_type);
         Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30));
         break;
     }
     //Util.Debug("<< normal_msg");
     return ret;
-},
+};
 
-framebufferUpdate: function() {
-    var RQ = RFB.RQ, FBU = RFB.FBU, timing = RFB.timing,
-        now, fbu_rt_diff, last_bytes, last_rects,
-        ret = true;
+framebufferUpdate = function() {
+    var now, fbu_rt_diff, last_bytes, last_rects, ret = true;
 
     if (FBU.rects === 0) {
         //Util.Debug("New FBU: RQ.slice(0,20): " + RQ.slice(0,20));
@@ -587,7 +966,13 @@ framebufferUpdate: function() {
         }
     }
 
-    while ((FBU.rects > 0) && (RQ.length >= FBU.bytes)) {
+    while (FBU.rects > 0) {
+        if (rfb_state !== "normal") {
+            return false;
+        }
+        if (RQ.length < FBU.bytes) {
+            return false;
+        }
         if (FBU.bytes === 0) {
             if (RQ.length < 12) {
                 //Util.Debug("   waiting for rect header bytes");
@@ -601,19 +986,19 @@ framebufferUpdate: function() {
             FBU.encoding = parseInt(RQ.shift32(), 10);
             timing.h_bytes += 12;
 
-            if (RFB.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 += "(" + RFB.encNames[FBU.encoding] + ")";
+                msg += "(" + encNames[FBU.encoding] + ")";
                 msg += ", RQ.length: " + RQ.length;
                 Util.Debug(msg);
                 */
             } else {
-                RFB.updateState('failed',
+                updateState('failed',
                         "Disconnected: unsupported encoding " +
                         FBU.encoding);
                 return false;
@@ -624,7 +1009,8 @@ framebufferUpdate: function() {
         last_bytes = RQ.length;
         last_rects = FBU.rects;
 
-        ret = RFB.encHandlers[FBU.encoding]();
+        // false ret means need more data
+        ret = encHandlers[FBU.encoding]();
 
         now = (new Date()).getTime();
         timing.cur_fbu += (now - timing.last_fbu);
@@ -637,8 +1023,8 @@ framebufferUpdate: function() {
         }
 
         if (FBU.rects === 0) {
-            if (((FBU.width === RFB.fb_width) &&
-                        (FBU.height === RFB.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;
@@ -662,101 +1048,102 @@ framebufferUpdate: function() {
                 timing.fbu_rt_start = 0;
             }
         }
-
-        if (RFB.state !== "normal") { return true; }
     }
     return ret;
-},
+};
 
-/*
- * FramebufferUpdate encodings
- */
+//
+// FramebufferUpdate encodings
+//
 
-display_raw: function () {
+encHandlers.RAW = function display_raw() {
     //Util.Debug(">> display_raw");
 
-    var RQ = RFB.RQ, FBU = RFB.FBU, cur_y, cur_height; 
+    var cur_y, cur_height; 
 
     if (FBU.lines === 0) {
         FBU.lines = FBU.height;
     }
-    FBU.bytes = FBU.width * RFB.fb_Bpp; // At least a line
+    FBU.bytes = FBU.width * fb_Bpp; // At least a line
     if (RQ.length < FBU.bytes) {
         //Util.Debug("   waiting for " +
         //           (FBU.bytes - RQ.length) + " RAW bytes");
-        return;
+        return false;
     }
     cur_y = FBU.y + (FBU.height - FBU.lines);
     cur_height = Math.min(FBU.lines,
-                          Math.floor(RQ.length/(FBU.width * RFB.fb_Bpp)));
-    Canvas.blitImage(FBU.x, cur_y, FBU.width, cur_height, RQ, 0);
-    RQ.shiftBytes(FBU.width * cur_height * RFB.fb_Bpp);
+                          Math.floor(RQ.length/(FBU.width * fb_Bpp)));
+    canvas.blitImage(FBU.x, cur_y, FBU.width, cur_height, RQ, 0);
+    RQ.shiftBytes(FBU.width * cur_height * fb_Bpp);
     FBU.lines -= cur_height;
 
     if (FBU.lines > 0) {
-        FBU.bytes = FBU.width * RFB.fb_Bpp; // At least another line
+        FBU.bytes = FBU.width * fb_Bpp; // At least another line
     } else {
         FBU.rects -= 1;
         FBU.bytes = 0;
     }
-},
+    return true;
+};
 
-display_copy_rect: function () {
+encHandlers.COPYRECT = function display_copy_rect() {
     //Util.Debug(">> display_copy_rect");
 
-    var RQ = RFB.RQ, FBU = RFB.FBU, old_x, old_y;
+    var old_x, old_y;
 
     if (RQ.length < 4) {
         //Util.Debug("   waiting for " +
         //           (FBU.bytes - RQ.length) + " COPYRECT bytes");
-        return;
+        return false;
     }
     old_x = RQ.shift16();
     old_y = RQ.shift16();
-    Canvas.copyImage(old_x, old_y, FBU.x, FBU.y, FBU.width, FBU.height);
+    canvas.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 (" + RQ.length + " bytes)");
+    var color, x, y, width, height, chunk;
 
-display_rre: function () {
-    //Util.Debug(">> display_rre (" + RFB.RQ.length + " bytes)");
-    var RQ = RFB.RQ, FBU = RFB.FBU, color, x, y, width, height, chunk;
     if (FBU.subrects === 0) {
-        if (RQ.length < 4 + RFB.fb_Bpp) {
+        if (RQ.length < 4 + fb_Bpp) {
             //Util.Debug("   waiting for " +
-            //           (4 + RFB.fb_Bpp - RQ.length) + " RRE bytes");
-            return;
+            //           (4 + fb_Bpp - RQ.length) + " RRE bytes");
+            return false;
         }
         FBU.subrects = RQ.shift32();
-        color = RQ.shiftBytes(RFB.fb_Bpp); // Background
-        Canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
+        color = RQ.shiftBytes(fb_Bpp); // Background
+        canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
     }
-    while ((FBU.subrects > 0) && (RQ.length >= (RFB.fb_Bpp + 8))) {
-        color = RQ.shiftBytes(RFB.fb_Bpp);
+    while ((FBU.subrects > 0) && (RQ.length >= (fb_Bpp + 8))) {
+        color = RQ.shiftBytes(fb_Bpp);
         x = RQ.shift16();
         y = RQ.shift16();
         width = RQ.shift16();
         height = RQ.shift16();
-        Canvas.fillRect(FBU.x + x, FBU.y + y, width, height, color);
+        canvas.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(RFB.rre_chunk, FBU.subrects);
-        FBU.bytes = (RFB.fb_Bpp + 8) * chunk;
+        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;
+};
 
-display_hextile: function() {
+encHandlers.HEXTILE = function display_hextile() {
     //Util.Debug(">> display_hextile");
-    var RQ = RFB.RQ, FBU = RFB.FBU,
-        subencoding, subrects, idx, tile, color, cur_tile,
+    var subencoding, subrects, idx, tile, color, cur_tile,
         tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh;
 
     if (FBU.tiles === 0) {
@@ -771,14 +1158,14 @@ display_hextile: function() {
         FBU.bytes = 1;
         if (RQ.length < FBU.bytes) {
             //Util.Debug("   waiting for HEXTILE subencoding byte");
-            return;
+            return false;
         }
         subencoding = RQ[0];  // Peek
         if (subencoding > 30) { // Raw
-            RFB.updateState('failed',
+            updateState('failed',
                     "Disconnected: illegal hextile subencoding " + subencoding);
             //Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30));
-            return;
+            return false;
         }
         subrects = 0;
         cur_tile = FBU.total_tiles - FBU.tiles;
@@ -792,24 +1179,24 @@ display_hextile: function() {
         /* Figure out how much we are expecting */
         if (subencoding & 0x01) { // Raw
             //Util.Debug("   Raw subencoding");
-            FBU.bytes += w * h * RFB.fb_Bpp;
+            FBU.bytes += w * h * fb_Bpp;
         } else {
             if (subencoding & 0x02) { // Background
-                FBU.bytes += RFB.fb_Bpp;
+                FBU.bytes += fb_Bpp;
             }
             if (subencoding & 0x04) { // Foreground
-                FBU.bytes += RFB.fb_Bpp;
+                FBU.bytes += fb_Bpp;
             }
             if (subencoding & 0x08) { // AnySubrects
                 FBU.bytes += 1;   // Since we aren't shifting it off
                 if (RQ.length < FBU.bytes) {
                     /* Wait for subrects byte */
                     //Util.Debug("   waiting for hextile subrects header byte");
-                    return;
+                    return false;
                 }
                 subrects = RQ[FBU.bytes-1]; // Peek
                 if (subencoding & 0x10) { // SubrectsColoured
-                    FBU.bytes += subrects * (RFB.fb_Bpp + 2);
+                    FBU.bytes += subrects * (fb_Bpp + 2);
                 } else {
                     FBU.bytes += subrects * 2;
                 }
@@ -827,7 +1214,7 @@ display_hextile: function() {
         if (RQ.length < FBU.bytes) {
             //Util.Debug("   waiting for " +
             //           (FBU.bytes - RQ.length) + " hextile bytes");
-            return;
+            return false;
         }
 
         /* We know the encoding and have a whole tile */
@@ -838,28 +1225,28 @@ display_hextile: function() {
                 /* Weird: ignore blanks after RAW */
                 Util.Debug("     Ignoring blank after RAW");
             } else {
-                Canvas.fillRect(x, y, w, h, FBU.background);
+                canvas.fillRect(x, y, w, h, FBU.background);
             }
         } else if (FBU.subencoding & 0x01) { // Raw
-            Canvas.blitImage(x, y, w, h, RQ, idx);
+            canvas.blitImage(x, y, w, h, RQ, idx);
         } else {
             if (FBU.subencoding & 0x02) { // Background
-                FBU.background = RQ.slice(idx, idx + RFB.fb_Bpp);
-                idx += RFB.fb_Bpp;
+                FBU.background = RQ.slice(idx, idx + fb_Bpp);
+                idx += fb_Bpp;
             }
             if (FBU.subencoding & 0x04) { // Foreground
-                FBU.foreground = RQ.slice(idx, idx + RFB.fb_Bpp);
-                idx += RFB.fb_Bpp;
+                FBU.foreground = RQ.slice(idx, idx + fb_Bpp);
+                idx += fb_Bpp;
             }
 
-            tile = Canvas.getTile(x, y, w, h, FBU.background);
+            tile = canvas.getTile(x, y, w, h, FBU.background);
             if (FBU.subencoding & 0x08) { // AnySubrects
                 subrects = RQ[idx];
                 idx += 1;
                 for (s = 0; s < subrects; s += 1) {
                     if (FBU.subencoding & 0x10) { // SubrectsColoured
-                        color = RQ.slice(idx, idx + RFB.fb_Bpp);
-                        idx += RFB.fb_Bpp;
+                        color = RQ.slice(idx, idx + fb_Bpp);
+                        idx += fb_Bpp;
                     } else {
                         color = FBU.foreground;
                     }
@@ -873,10 +1260,10 @@ display_hextile: function() {
                     sw = (wh >> 4)   + 1;
                     sh = (wh & 0x0f) + 1;
 
-                    Canvas.setSubTile(tile, sx, sy, sw, sh, color);
+                    canvas.setSubTile(tile, sx, sy, sw, sh, color);
                 }
             }
-            Canvas.putTile(tile);
+            canvas.putTile(tile);
         }
         RQ.shiftBytes(FBU.bytes);
         FBU.lastsubencoding = FBU.subencoding;
@@ -889,13 +1276,13 @@ display_hextile: function() {
     }
 
     //Util.Debug("<< display_hextile");
-},
+    return true;
+};
 
 
-display_tight_png: function() {
+encHandlers.TIGHT_PNG = function display_tight_png() {
     //Util.Debug(">> display_tight_png");
-    var RQ = RFB.RQ, FBU = RFB.FBU, 
-        ctl, cmode, clength, getCLength, color, img;
+    var ctl, cmode, clength, getCLength, color, img;
     //Util.Debug("   FBU.rects: " + FBU.rects);
     //Util.Debug("   RQ.length: " + RQ.length);
     //Util.Debug("   RQ.slice(0,20): " + RQ.slice(0,20));
@@ -904,7 +1291,7 @@ display_tight_png: function() {
     FBU.bytes = 1; // compression-control byte
     if (RQ.length < FBU.bytes) {
         Util.Debug("   waiting for TIGHT compression-control byte");
-        return;
+        return false;
     }
 
     // Get 'compact length' header and data size
@@ -931,25 +1318,25 @@ display_tight_png: function() {
     }
     switch (cmode) {
         // fill uses fb_depth because TPIXELs drop the padding byte
-        case "fill": FBU.bytes += RFB.fb_depth; break; // TPIXEL
+        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 (RQ.length < FBU.bytes) {
         Util.Debug("   waiting for TIGHT " + cmode + " bytes");
-        return;
+        return false;
     }
 
-    //Util.Debug("   RQ.slice(0,20): " + RFB.RQ.slice(0,20) + " (" + RFB.RQ.length + ")");
+    //Util.Debug("   RQ.slice(0,20): " + RQ.slice(0,20) + " (" + RQ.length + ")");
     //Util.Debug("   cmode: " + cmode);
 
     // Determine FBU.bytes
     switch (cmode) {
     case "fill":
         RQ.shift8(); // shift off ctl
-        color = RQ.shiftBytes(RFB.fb_depth);
-        Canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
+        color = RQ.shiftBytes(fb_depth);
+        canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
         break;
     case "jpeg":
     case "png":
@@ -957,17 +1344,17 @@ display_tight_png: function() {
         FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
         if (RQ.length < FBU.bytes) {
             Util.Debug("   waiting for TIGHT " + cmode + " bytes");
-            return;
+            return false;
         }
 
         // We have everything, render it
         //Util.Debug("   png, RQ.length: " + RQ.length + ", clength[0]: " + clength[0] + ", clength[1]: " + clength[1]);
         RQ.shiftBytes(1 + clength[0]); // shift off ctl + compact length
         img = new Image();
-        img.onload = RFB.scan_tight_imgs;
+        img.onload = scan_tight_imgs;
         FBU.imgs.push([img, FBU.x, FBU.y]);
         img.src = "data:image/" + cmode +
-            RFB.extract_data_uri(RQ.shiftBytes(clength[1]));
+            extract_data_uri(RQ.shiftBytes(clength[1]));
         img = null;
         break;
     }
@@ -976,643 +1363,294 @@ display_tight_png: function() {
     //Util.Debug("   ending RQ.length: " + RQ.length);
     //Util.Debug("   ending RQ.slice(0,20): " + RQ.slice(0,20));
     //Util.Debug("<< display_tight_png");
-},
+    return true;
+};
 
-extract_data_uri : function (arr) {
+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_imgs : function () {
-    var img, imgs;
-    if (RFB.state === 'normal') {
-        imgs = RFB.FBU.imgs;
+scan_tight_imgs = function() {
+    var img, imgs, ctx;
+    ctx = canvas.getContext();
+    if (rfb_state === 'normal') {
+        imgs = FBU.imgs;
         while ((imgs.length > 0) && (imgs[0][0].complete)) {
-            img = imgs.shift();
-            Canvas.ctx.drawImage(img[0], img[1], img[2]);
-        }
-        setTimeout(RFB.scan_tight_imgs, RFB.scan_imgs_rate);
-    }
-},
-
-set_desktopsize : function () {
-    Util.Debug(">> set_desktopsize");
-    RFB.fb_width = RFB.FBU.width;
-    RFB.fb_height = RFB.FBU.height;
-    Canvas.clear();
-    Canvas.resize(RFB.fb_width, RFB.fb_height);
-    RFB.timing.fbu_rt_start = (new Date()).getTime();
-    // Send a new non-incremental request
-    RFB.send_array(RFB.fbUpdateRequest(0));
-
-    RFB.FBU.bytes = 0;
-    RFB.FBU.rects -= 1;
-
-    Util.Debug("<< set_desktopsize");
-},
-
-set_cursor: function () {
-    var x, y, w, h, pixelslength, masklength;
-    //Util.Debug(">> set_cursor");
-    x = RFB.FBU.x;  // hotspot-x
-    y = RFB.FBU.y;  // hotspot-y
-    w = RFB.FBU.width;
-    h = RFB.FBU.height;
-
-    pixelslength = w * h * RFB.fb_Bpp;
-    masklength = Math.floor((w + 7) / 8) * h;
-
-    if (RFB.RQ.length < (pixelslength + masklength)) {
-        //Util.Debug("waiting for cursor encoding bytes");
-        RFB.FBU.bytes = pixelslength + masklength;
-        return false;
-    }
-
-    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);
-
-    Canvas.changeCursor(RFB.RQ.shiftBytes(pixelslength),
-                        RFB.RQ.shiftBytes(masklength),
-                        x, y, w, h);
-
-    RFB.FBU.bytes = 0;
-    RFB.FBU.rects -= 1;
-
-    //Util.Debug("<< set_cursor");
-},
-
-set_jpeg_quality : function () {
-    Util.Debug(">> set_jpeg_quality");
-},
-set_compress_level: function () {
-    Util.Debug(">> set_compress_level");
-},
-
-/*
- * 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(RFB.fb_Bpp * 8); // bits-per-pixel
-    arr.push8(RFB.fb_depth * 8); // depth
-    arr.push8(0);  // little-endian
-    arr.push8(RFB.true_color);  // 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;
-},
-
-fixColourMapEntries: function () {
-},
-
-clientEncodings: function () {
-    //Util.Debug(">> clientEncodings");
-    var arr, i, encList = [];
-
-    for (i=0; i<RFB.encodings.length; i += 1) {
-        if ((RFB.encodings[i][0] === "Cursor") &&
-            (! RFB.local_cursor)) {
-            Util.Debug("Skipping Cursor pseudo-encoding");
-        } else {
-            //Util.Debug("Adding encoding: " + RFB.encodings[i][0]);
-            encList.push(RFB.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 = RFB.fb_width; }
-    if (!yw) { yw = RFB.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: " + RFB.mouse_buttonMask);
-    var arr;
-    arr = [5];  // msg-type
-    arr.push8(RFB.mouse_buttonMask);
-    arr.push16(x);
-    arr.push16(y);
-    //Util.Debug("<< pointerEvent");
-    return arr;
-},
-
-clientCutText: function (text) {
-    //Util.Debug(">> clientCutText");
-    var arr;
-    arr = [6];     // msg-type
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-    arr.push8(0);  // padding
-    arr.push32(text.length);
-    arr.pushStr(text);
-    //Util.Debug("<< clientCutText:" + arr);
-    return arr;
-},
-
-
-/*
- * Utility routines
- */
-
-encode_message: function(arr) {
-    if (RFB.b64encode) {
-        /* base64 encode */
-        RFB.SQ = RFB.SQ + Base64.encode(arr);
-    } else {
-        /* UTF-8 encode. 0 -> 256 to avoid WebSockets framing */
-        RFB.SQ = RFB.SQ + arr.map(function (num) {
-                if (num === 0) {
-                    return String.fromCharCode(256);
-                } else {
-                    return String.fromCharCode(num);
-                }
-            } ).join('');
-    }
-},
-
-decode_message: function(data) {
-    var i, length, RQ = RFB.RQ;
-    //Util.Debug(">> decode_message: " + data);
-    if (RFB.b64encode) {
-        /* base64 decode */
-        RFB.RQ = RFB.RQ.concat(Base64.decode(data, 0));
-    } else {
-        /* UTF-8 decode. 256 -> 0 to WebSockets framing */
-        length = data.length;
-        for (i=0; i < length; i += 1) {
-            RQ.push(data.charCodeAt(i) % 256);
-        }
-    }
-    //Util.Debug(">> decode_message, RQ: " + RFB.RQ);
-},
-
-recv_message: function(e) {
-    //Util.Debug(">> recv_message");
-
-    try {
-        RFB.decode_message(e.data);
-        if (RFB.RQ.length > 0) {
-            RFB.handle_message();
-        } else {
-            Util.Debug("Ignoring empty message");
-        }
-    } catch (exc) {
-        if (typeof exc.stack !== 'undefined') {
-            Util.Warn("recv_message, caught exception: " + exc.stack);
-        } else if (typeof exc.description !== 'undefined') {
-            Util.Warn("recv_message, caught exception: " + exc.description);
-        } else {
-            Util.Warn("recv_message, caught exception:" + exc);
-        }
-        if (typeof exc.name !== 'undefined') {
-            RFB.updateState('failed', exc.name + ": " + exc.message);
-        } else {
-            RFB.updateState('failed', exc);
-        }
-    }
-    //Util.Debug("<< recv_message");
-},
-
-handle_message: function () {
-    //Util.Debug("RQ.slice(0,20): " + RFB.RQ.slice(0,20) + " (" + RFB.RQ.length + ")");
-    switch (RFB.state) {
-    case 'disconnected':
-        Util.Error("Got data while disconnected");
-        break;
-    case 'failed':
-        Util.Warn("Giving up!");
-        RFB.disconnect();
-        break;
-    case 'normal':
-        RFB.normal_msg();
-        /*
-        while (RFB.RQ.length > 0) {
-            if (RFB.normal_msg() && RFB.state === 'normal') {
-                Util.Debug("More to process");
-            } else {
-                break;
-            }
-        }
-        */
-        break;
-    default:
-        RFB.init_msg();
-        break;
-    }
-},
-
-send_string: function (str) {
-    //Util.Debug(">> send_string: " + str);
-    RFB.send_array(str.split('').map(
-        function (chr) { return chr.charCodeAt(0); } ) );
-},
-
-send_array: function (arr) {
-    //Util.Debug(">> send_array: " + arr);
-    RFB.encode_message(arr);
-    if (RFB.ws.bufferedAmount === 0) {
-        //Util.Debug("arr: " + arr);
-        //Util.Debug("RFB.SQ: " + RFB.SQ);
-        RFB.ws.send(RFB.SQ);
-        RFB.SQ = "";
-    } else {
-        Util.Debug("Delaying send");
-    }
-},
-
-DES: function (password, challenge) {
-    var i, passwd, response;
-    passwd = [];
-    response = challenge.slice();
-    for (i=0; i < password.length; i += 1) {
-        passwd.push(password.charCodeAt(i));
-    }
-
-    DES.setKeys(passwd);
-    DES.encrypt(response, 0, response, 0);
-    DES.encrypt(response, 8, response, 8);
-    return response;
-},
-
-flushClient: function () {
-    if (RFB.mouse_arr.length > 0) {
-        //RFB.send_array(RFB.mouse_arr.concat(RFB.fbUpdateRequest(1)));
-        RFB.send_array(RFB.mouse_arr);
-        setTimeout(function() {
-                RFB.send_array(RFB.fbUpdateRequest(1));
-            }, 50);
-
-        RFB.mouse_arr = [];
-        return true;
-    } else {
-        return false;
-    }
-},
-
-checkEvents: function () {
-    var now;
-    if (RFB.state === 'normal') {
-        if (! RFB.flushClient()) {
-            now = new Date().getTime();
-            if (now > RFB.last_req + RFB.req_rate) {
-                RFB.last_req = now;
-                RFB.send_array(RFB.fbUpdateRequest(1));
-            }
-        }
-    }
-    setTimeout(RFB.checkEvents, RFB.check_rate);
-},
-
-keyPress: function (keysym, down) {
-    var arr;
-    arr = RFB.keyEvent(keysym, down);
-    arr = arr.concat(RFB.fbUpdateRequest(1));
-    RFB.send_array(arr);
-},
-
-mouseButton: function(x, y, down, bmask) {
-    if (down) {
-        RFB.mouse_buttonMask |= bmask;
-    } else {
-        RFB.mouse_buttonMask ^= bmask;
+            img = imgs.shift();
+            ctx.drawImage(img[0], img[1], img[2]);
+        }
+        setTimeout(scan_tight_imgs, scan_imgs_rate);
     }
-    RFB.mouse_arr = RFB.mouse_arr.concat( RFB.pointerEvent(x, y) );
-    RFB.flushClient();
-},
+};
 
-mouseMove: function(x, y) {
-    //Util.Debug('>> mouseMove ' + x + "," + y);
-    RFB.mouse_arr = RFB.mouse_arr.concat( RFB.pointerEvent(x, y) );
-},
+encHandlers.DesktopSize = function set_desktopsize() {
+    Util.Debug(">> set_desktopsize");
+    fb_width = FBU.width;
+    fb_height = FBU.height;
+    canvas.clear();
+    canvas.resize(fb_width, fb_height);
+    timing.fbu_rt_start = (new Date()).getTime();
+    // Send a new non-incremental request
+    send_array(fbUpdateRequest(0));
 
-clipboardCopyTo: function (text) {
-    Util.Debug(">> clipboardCopyTo stub");
-    // Stub
-},
+    FBU.bytes = 0;
+    FBU.rects -= 1;
 
-externalUpdateState: function(state, msg) {
-    Util.Debug(">> externalUpdateState stub");
-    // Stub
-},
+    Util.Debug("<< set_desktopsize");
+    return true;
+};
 
-/*
- * Running states:
- *   disconnected - idle state
- *   normal       - connected
- *
- * Page states:
- *   loaded       - page load, equivalent to disconnected
- *   connect      - starting initialization
- *   password     - waiting for password
- *   failed       - abnormal transition to disconnected
- *   fatal        - failed to load page, or fatal error
- *
- * VNC initialization states:
- *   ProtocolVersion
- *   Security
- *   Authentication
- *   SecurityResult
- *   ServerInitialization
- */
-updateState: function(state, statusMsg) {
-    var func, cmsg, oldstate = RFB.state;
-    if (state === oldstate) {
-        /* Already here, ignore */
-        Util.Debug("Already in state '" + state + "', ignoring.");
-        return;
-    }
+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;
 
-    if (oldstate === 'fatal') {
-        Util.Error("Fatal error, cannot continue");
-    }
+    pixelslength = w * h * fb_Bpp;
+    masklength = Math.floor((w + 7) / 8) * h;
 
-    if ((state === 'failed') || (state === 'fatal')) {
-        func = Util.Error;
-    } else {
-        func = Util.Warn;
+    if (RQ.length < (pixelslength + masklength)) {
+        //Util.Debug("waiting for cursor encoding bytes");
+        FBU.bytes = pixelslength + masklength;
+        return false;
     }
 
-    cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
-    func("New state '" + state + "', was '" + oldstate + "'." + cmsg);
+    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);
 
-    if ((oldstate === 'failed') && (state === 'disconnected')) {
-        // Do disconnect action, but stay in failed state.
-        RFB.state = 'failed';
-    } else {
-        RFB.state = state;
-    }
+    canvas.changeCursor(RQ.shiftBytes(pixelslength),
+                            RQ.shiftBytes(masklength),
+                            x, y, w, h);
 
-    switch (state) {
-    case 'loaded':
-    case 'disconnected':
+    FBU.bytes = 0;
+    FBU.rects -= 1;
 
-        if (RFB.sendID) {
-            clearInterval(RFB.sendID);
-            RFB.sendID = null;
-        }
+    //Util.Debug("<< set_cursor");
+    return true;
+};
 
-        if (RFB.ws) {
-            if (RFB.ws.readyState === WebSocket.OPEN) {
-                RFB.ws.close();
-            }
-            RFB.ws.onmessage = function (e) { return; };
-        }
+encHandlers.JPEG_quality_lo = function set_jpeg_quality() {
+    Util.Error("Server sent jpeg_quality pseudo-encoding");
+};
 
-        if (Canvas.ctx) {
-            Canvas.stop();
-            if (! /__debug__$/i.test(document.location.href)) {
-                Canvas.clear();
-            }
-        }
+encHandlers.compress_lo = function set_compress_level() {
+    Util.Error("Server sent compress level pseudo-encoding");
+};
 
-        RFB.show_timings();
+/*
+ * Client message routines
+ */
 
-        break;
+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
 
-    case 'connect':
-        RFB.init_vars();
+    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
 
-        if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) {
-            RFB.ws.close();
-        }
-        RFB.init_ws(); // onopen transitions to 'ProtocolVersion'
+    arr.push8(0);     // padding
+    arr.push8(0);     // padding
+    arr.push8(0);     // padding
+    //Util.Debug("<< pixelFormat");
+    return arr;
+};
 
-        break;
+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]);
+        }
+    }
 
-    case 'password':
-        // Ignore password state by default
-        break;
+    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;
+};
 
-    case 'normal':
-        if ((oldstate === 'disconnected') || (oldstate === 'failed')) {
-            Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
-        }
+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;
+};
 
-        break;
+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;
+};
 
-    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.");
-        }
+clientCutText = function(text) {
+    //Util.Debug(">> clientCutText");
+    var arr;
+    arr = [6];     // msg-type
+    arr.push8(0);  // padding
+    arr.push8(0);  // padding
+    arr.push8(0);  // padding
+    arr.push32(text.length);
+    arr.pushStr(text);
+    //Util.Debug("<< clientCutText:" + arr);
+    return arr;
+};
 
-        if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) {
-            RFB.ws.close();
-        }
-        // Make sure we transition to disconnected
-        setTimeout(function() { RFB.updateState('disconnected'); }, 50);
 
-        break;
 
+//
+// Public API interface functions
+//
 
-    default:
-        // Invalid state transition
+that.init = function () {
 
-    }
+    init_vars();
 
-    if ((oldstate === 'failed') && (state === 'disconnected')) {
-        // Leave the failed message
-        RFB.externalUpdateState(state);
+    /* Check web-socket-js if no builtin WebSocket support */
+    if (VNC_native_ws) {
+        Util.Info("Using native WebSockets");
+        updateState('loaded', 'noVNC ready (using native WebSockets)');
     } else {
-        RFB.externalUpdateState(state, statusMsg);
-    }
-},
-
-update_timings: function() {
-    var now, timing = RFB.timing, offset;
-    now = (new Date()).getTime();
-    timing.history.push([now,
-            timing.h_fbus,
-            timing.h_rects,
-            timing.h_bytes,
-            timing.h_pixels]);
-    timing.h_fbus = 0;
-    timing.h_rects = 0;
-    timing.h_bytes = 0;
-    timing.h_pixels = 0;
-    if ((RFB.state !== 'disconnected') && (RFB.state !== 'failed')) {
-        // Try for every second
-        offset = (now - timing.history_start) % 1000;
-        if (offset < 500) {
-            setTimeout(RFB.update_timings, 1000 - offset);
+        Util.Warn("Using web-socket-js flash bridge");
+        if ((! Util.Flash) ||
+            (Util.Flash.version < 9)) {
+            updateState('fatal', "WebSockets or Adobe Flash is required");
+        } else if (document.location.href.substr(0, 7) === "file://") {
+            updateState('fatal',
+                    "'file://' URL is incompatible with Adobe Flash");
         } else {
-            setTimeout(RFB.update_timings, 2000 - offset);
+            updateState('loaded', 'noVNC ready (using Flash WebSockets emulation)');
         }
     }
-},
+};
 
-show_timings: function() {
-    var i, timing = RFB.timing, history, msg,
-        delta, tot_time = 0, tot_fbus = 0, tot_rects = 0,
-        tot_bytes = 0, tot_pixels = 0;
-    if (timing.history_start === 0) { return; }
-    //Util.Debug(">> show_timings");
-    RFB.update_timings();  // Final accumulate
-    msg = "\nTimings\n";
-    msg += "  time: fbus,rects,bytes,pixels\n";
-    for (i=0; i < timing.history.length; i += 1) {
-        history = timing.history[i];
-        delta = ((history[0]-timing.history_start)/1000);
-        tot_time = delta;
-        tot_fbus += history[1];
-        tot_rects += history[2];
-        tot_bytes += history[3];
-        tot_pixels += history[4];
+that.connect = function(host, port, password) {
+    //Util.Debug(">> connect");
 
-        msg += "  " + delta.toFixed(3);
-        msg += ": " + history.slice(1) + "\n";
+    // Make sure we have done init checks
+    if ((rfb_state !== 'loaded') && (rfb_state !== 'fatal')) {
+        that.init();
     }
-    msg += "\nTotals:\n";
-    msg += "  time: fbus,rects,bytes,pixels\n";
-    msg += "  " + tot_time.toFixed(3);
-    msg += ": " + tot_fbus + "," + tot_rects;
-    msg += "," + tot_bytes + "," + tot_pixels;
-    Util.Info(msg);
-    //Util.Debug("<< show_timings");
-},
-
-/*
- * Setup routines
- */
 
-init_ws: function () {
-    //Util.Debug(">> init_ws");
+    rfb_host       = host;
+    rfb_port       = port;
+    rfb_password   = (password !== undefined)   ? password : "";
 
-    var uri = "", vars = [];
-    if (RFB.encrypt) {
-        uri = "wss://";
-    } else {
-        uri = "ws://";
-    }
-    uri += RFB.host + ":" + RFB.port + "/";
-    if (RFB.b64encode) {
-        vars.push("b64encode");
-    }
-    if (vars.length > 0) {
-        uri += "?" + vars.join("&");
+    if ((!rfb_host) || (!rfb_port)) {
+        updateState('failed', "Must set host and port");
+        return;
     }
-    Util.Info("connecting to " + uri);
-    RFB.ws = new WebSocket(uri);
 
-    RFB.ws.onmessage = RFB.recv_message;
-    RFB.ws.onopen = function(e) {
-        Util.Debug(">> WebSocket.onopen");
-        if (RFB.state === "connect") {
-            RFB.updateState('ProtocolVersion', "Starting VNC handshake");
-        } else {
-            RFB.updateState('failed', "Got unexpected WebSockets connection");
-        }
-        Util.Debug("<< WebSocket.onopen");
-    };
-    RFB.ws.onclose = function(e) {
-        Util.Debug(">> WebSocket.onclose");
-        if (RFB.state === 'normal') {
-            RFB.updateState('failed', 'Server disconnected');
-        } else if (RFB.state === 'ProtocolVersion') {
-            RFB.updateState('failed', 'Failed to connect to server');
-        } else  {
-            RFB.updateState('disconnected', 'VNC disconnected');
-        }
-        Util.Debug("<< WebSocket.onclose");
-    };
-    RFB.ws.onerror = function(e) {
-        Util.Debug(">> WebSocket.onerror");
-        RFB.updateState('failed', "WebSocket error");
-        Util.Debug("<< WebSocket.onerror");
-    };
+    updateState('connect');
+    //Util.Debug("<< connect");
 
-    setTimeout(function () {
-            if (RFB.ws.readyState === WebSocket.CONNECTING) {
-                RFB.updateState('failed', "Connect timeout");
-            }
-        }, RFB.connectTimeout);
+};
 
-    //Util.Debug("<< init_ws");
-},
+that.disconnect = function() {
+    //Util.Debug(">> disconnect");
+    updateState('disconnected', 'Disconnected');
+    //Util.Debug("<< disconnect");
+};
 
-init_vars: function () {
-    /* Reset state */
-    RFB.cuttext          = 'none';
-    RFB.ct_length        = 0;
-    RFB.RQ               = [];
-    RFB.SQ               = "";
-    RFB.FBU.rects        = 0;
-    RFB.FBU.subrects     = 0;  // RRE and HEXTILE
-    RFB.FBU.lines        = 0;  // RAW
-    RFB.FBU.tiles        = 0;  // HEXTILE
-    RFB.FBU.imgs         = []; // TIGHT_PNG image queue
-    RFB.mouse_buttonmask = 0;
-    RFB.mouse_arr        = [];
-
-    RFB.timing.history_start = 0;
-    RFB.timing.history = [];
-    RFB.timing.h_fbus = 0;
-    RFB.timing.h_rects = 0;
-    RFB.timing.h_bytes = 0;
-    RFB.timing.h_pixels = 0;
-}
+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));
+    send_array(arr);
+};
+
+that.clipboardPasteFrom = function(text) {
+    if (rfb_state !== "normal") { return; }
+    //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "...");
+    send_array(clientCutText(text));
+    //Util.Debug("<< clipboardPasteFrom");
+};
+
+that.testMode = function(override_send_array) {
+    // Overridable internal functions for testing
+    test_mode = true;
+    send_array = override_send_array;
+    that.recv_message = recv_message;  // Expose it
+
+    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 */
+}  // End of RFB()