• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2011 Joel Martin
4 * Licensed under LGPL-3 (see LICENSE.txt)
5 *
6 * See README.md for usage and integration instructions.
7 *
8 * TIGHT decoder portion:
9 * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
10 */
11
12/*jslint white: false, browser: true, bitwise: false, plusplus: false */
13/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */
14
15
16function RFB(defaults) {
17"use strict";
18
19var that           = {},  // Public API methods
20    conf           = {},  // Configuration attributes
21
22    // Pre-declare private functions used before definitions (jslint)
23    init_vars, updateState, fail, handle_message,
24    init_msg, normal_msg, framebufferUpdate, print_stats,
25
26    pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests,
27    keyEvent, pointerEvent, clientCutText,
28
29    getTightCLength, extract_data_uri, scan_tight_imgQ,
30    keyPress, mouseButton, mouseMove,
31
32    checkEvents,  // Overridable for testing
33
34
35    //
36    // Private RFB namespace variables
37    //
38    rfb_host       = '',
39    rfb_port       = 5900,
40    rfb_password   = '',
41    rfb_path       = '',
42
43    rfb_state      = 'disconnected',
44    rfb_version    = 0,
45    rfb_max_version= 3.8,
46    rfb_auth_scheme= '',
47
48
49    // In preference order
50    encodings      = [
51        ['COPYRECT',         0x01 ],
52        ['TIGHT',            0x07 ],
53        ['TIGHT_PNG',        -260 ],
54        ['HEXTILE',          0x05 ],
55        ['RRE',              0x02 ],
56        ['RAW',              0x00 ],
57        ['DesktopSize',      -223 ],
58        ['Cursor',           -239 ],
59
60        // Psuedo-encoding settings
61        //['JPEG_quality_lo',   -32 ],
62        ['JPEG_quality_med',    -26 ],
63        //['JPEG_quality_hi',   -23 ],
64        //['compress_lo',      -255 ],
65        ['compress_hi',        -247 ],
66        ['last_rect',          -224 ]
67        ],
68
69    encHandlers    = {},
70    encNames       = {},
71    encStats       = {},     // [rectCnt, rectCntTot]
72
73    ws             = null,   // Websock object
74    display        = null,   // Display object
75    keyboard       = null,   // Keyboard input handler object
76    mouse          = null,   // Mouse input handler object
77    sendTimer      = null,   // Send Queue check timer
78    connTimer      = null,   // connection timer
79    disconnTimer   = null,   // disconnection timer
80    msgTimer       = null,   // queued handle_message timer
81
82    // Frame buffer update state
83    FBU            = {
84        rects          : 0,
85        subrects       : 0,  // RRE
86        lines          : 0,  // RAW
87        tiles          : 0,  // HEXTILE
88        bytes          : 0,
89        x              : 0,
90        y              : 0,
91        width          : 0,
92        height         : 0,
93        encoding       : 0,
94        subencoding    : -1,
95        background     : null,
96        imgQ           : [],  // TIGHT_PNG image queue
97        zlibs          : []   // TIGHT zlib streams
98    },
99
100    fb_Bpp         = 4,
101    fb_depth       = 3,
102    fb_width       = 0,
103    fb_height      = 0,
104    fb_name        = "",
105
106    scan_imgQ_rate = 40, // 25 times per second or so
107    last_req_time  = 0,
108    rre_chunk_sz   = 100,
109
110    timing         = {
111        last_fbu       : 0,
112        fbu_total      : 0,
113        fbu_total_cnt  : 0,
114        full_fbu_total : 0,
115        full_fbu_cnt   : 0,
116
117        fbu_rt_start   : 0,
118        fbu_rt_total   : 0,
119        fbu_rt_cnt     : 0,
120        pixels         : 0
121    },
122
123    test_mode        = false,
124
125    def_con_timeout  = Websock_native ? 2 : 5,
126
127    /* Mouse state */
128    mouse_buttonMask = 0,
129    mouse_arr        = [],
130    viewportDragging = false,
131    viewportDragPos  = {};
132
133// Configuration attributes
134Util.conf_defaults(conf, that, defaults, [
135    ['target',             'wo', 'dom', null, 'VNC display rendering Canvas object'],
136    ['focusContainer',     'wo', 'dom', document, 'DOM element that captures keyboard input'],
137
138    ['encrypt',            'rw', 'bool', false, 'Use TLS/SSL/wss encryption'],
139    ['true_color',         'rw', 'bool', true,  'Request true color pixel data'],
140    ['local_cursor',       'rw', 'bool', false, 'Request locally rendered cursor'],
141    ['shared',             'rw', 'bool', true,  'Request shared mode'],
142    ['view_only',          'rw', 'bool', false, 'Disable client mouse/keyboard'],
143
144    ['connectTimeout',     'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'],
145    ['disconnectTimeout',  'rw', 'int', 3,    'Time (s) to wait for disconnection'],
146
147    ['viewportDrag',       'rw', 'bool', false, 'Move the viewport on mouse drags'],
148
149    ['check_rate',         'rw', 'int', 217,  'Timing (ms) of send/receive check'],
150    ['fbu_req_rate',       'rw', 'int', 1413, 'Timing (ms) of frameBufferUpdate requests'],
151
152    // Callback functions
153    ['onUpdateState',      'rw', 'func', function() { },
154        'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '],
155    ['onPasswordRequired', 'rw', 'func', function() { },
156        'onPasswordRequired(rfb): VNC password is required '],
157    ['onClipboard',        'rw', 'func', function() { },
158        'onClipboard(rfb, text): RFB clipboard contents received'],
159    ['onBell',             'rw', 'func', function() { },
160        'onBell(rfb): RFB Bell message received '],
161    ['onFBUReceive',       'rw', 'func', function() { },
162        'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '],
163    ['onFBUComplete',      'rw', 'func', function() { },
164        'onFBUComplete(rfb, fbu): RFB FBU received and processed '],
165
166    // These callback names are deprecated
167    ['updateState',        'rw', 'func', function() { },
168        'obsolete, use onUpdateState'],
169    ['clipboardReceive',   'rw', 'func', function() { },
170        'obsolete, use onClipboard']
171    ]);
172
173
174// Override/add some specific configuration getters/setters
175that.set_local_cursor = function(cursor) {
176    if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) {
177        conf.local_cursor = false;
178    } else {
179        if (display.get_cursor_uri()) {
180            conf.local_cursor = true;
181        } else {
182            Util.Warn("Browser does not support local cursor");
183        }
184    }
185};
186
187// These are fake configuration getters
188that.get_display = function() { return display; };
189
190that.get_keyboard = function() { return keyboard; };
191
192that.get_mouse = function() { return mouse; };
193
194
195
196//
197// Setup routines
198//
199
200// Create the public API interface and initialize values that stay
201// constant across connect/disconnect
202function constructor() {
203    var i, rmode;
204    Util.Debug(">> RFB.constructor");
205
206    // Create lookup tables based encoding number
207    for (i=0; i < encodings.length; i+=1) {
208        encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]];
209        encNames[encodings[i][1]] = encodings[i][0];
210        encStats[encodings[i][1]] = [0, 0];
211    }
212    // Initialize display, mouse, keyboard, and websock
213    try {
214        display   = new Display({'target': conf.target});
215    } catch (exc) {
216        Util.Error("Display exception: " + exc);
217        updateState('fatal', "No working Display");
218    }
219    keyboard = new Keyboard({'target': conf.focusContainer,
220                                'onKeyPress': keyPress});
221    mouse    = new Mouse({'target': conf.target,
222                            'onMouseButton': mouseButton,
223                            'onMouseMove': mouseMove});
224
225    rmode = display.get_render_mode();
226
227    ws = new Websock();
228    ws.on('message', handle_message);
229    ws.on('open', function() {
230        if (rfb_state === "connect") {
231            updateState('ProtocolVersion', "Starting VNC handshake");
232        } else {
233            fail("Got unexpected WebSockets connection");
234        }
235    });
236    ws.on('close', function(e) {
237        if (e.code) {
238            Util.Info("Close code: " + e.code + ", reason: " + e.reason + ", wasClean: " + e.wasClean);
239        }
240        if (rfb_state === 'disconnect') {
241            updateState('disconnected', 'VNC disconnected');
242        } else if (rfb_state === 'ProtocolVersion') {
243            fail('Failed to connect to server');
244        } else if (rfb_state in {'failed':1, 'disconnected':1}) {
245            Util.Error("Received onclose while disconnected");
246        } else  {
247            fail('Server disconnected');
248        }
249    });
250    ws.on('error', function(e) {
251        fail("WebSock error: " + e);
252    });
253
254
255    init_vars();
256
257    /* Check web-socket-js if no builtin WebSocket support */
258    if (Websock_native) {
259        Util.Info("Using native WebSockets");
260        updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
261    } else {
262        Util.Warn("Using web-socket-js bridge. Flash version: " +
263                  Util.Flash.version);
264        if ((! Util.Flash) ||
265            (Util.Flash.version < 9)) {
266            updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash<\/a> is required");
267        } else if (document.location.href.substr(0, 7) === "file://") {
268            updateState('fatal',
269                    "'file://' URL is incompatible with Adobe Flash");
270        } else {
271            updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
272        }
273    }
274
275    Util.Debug("<< RFB.constructor");
276    return that;  // Return the public API interface
277}
278
279function connect() {
280    Util.Debug(">> RFB.connect");
281    var uri;
282
283    if (typeof UsingSocketIO !== "undefined") {
284        uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path;
285    } else {
286        if (conf.encrypt) {
287            uri = "wss://";
288        } else {
289            uri = "ws://";
290        }
291        uri += rfb_host + ":" + rfb_port + "/" + rfb_path;
292    }
293    Util.Info("connecting to " + uri);
294    ws.open(uri);
295
296    Util.Debug("<< RFB.connect");
297}
298
299// Initialize variables that are reset before each connection
300init_vars = function() {
301    var i;
302
303    /* Reset state */
304    ws.init();
305
306    FBU.rects        = 0;
307    FBU.subrects     = 0;  // RRE and HEXTILE
308    FBU.lines        = 0;  // RAW
309    FBU.tiles        = 0;  // HEXTILE
310    FBU.imgQ         = []; // TIGHT_PNG image queue
311    FBU.zlibs        = []; // TIGHT zlib encoders
312    mouse_buttonMask = 0;
313    mouse_arr        = [];
314
315    // Clear the per connection encoding stats
316    for (i=0; i < encodings.length; i+=1) {
317        encStats[encodings[i][1]][0] = 0;
318    }
319
320    for (i=0; i < 4; i++) {
321        //FBU.zlibs[i] = new InflateStream();
322        FBU.zlibs[i] = new TINF();
323        FBU.zlibs[i].init();
324    }
325};
326
327// Print statistics
328print_stats = function() {
329    var i, s;
330    Util.Info("Encoding stats for this connection:");
331    for (i=0; i < encodings.length; i+=1) {
332        s = encStats[encodings[i][1]];
333        if ((s[0] + s[1]) > 0) {
334            Util.Info("    " + encodings[i][0] + ": " +
335                      s[0] + " rects");
336        }
337    }
338    Util.Info("Encoding stats since page load:");
339    for (i=0; i < encodings.length; i+=1) {
340        s = encStats[encodings[i][1]];
341        if ((s[0] + s[1]) > 0) {
342            Util.Info("    " + encodings[i][0] + ": " +
343                      s[1] + " rects");
344        }
345    }
346};
347
348//
349// Utility routines
350//
351
352
353/*
354 * Page states:
355 *   loaded       - page load, equivalent to disconnected
356 *   disconnected - idle state
357 *   connect      - starting to connect (to ProtocolVersion)
358 *   normal       - connected
359 *   disconnect   - starting to disconnect
360 *   failed       - abnormal disconnect
361 *   fatal        - failed to load page, or fatal error
362 *
363 * RFB protocol initialization states:
364 *   ProtocolVersion
365 *   Security
366 *   Authentication
367 *   password     - waiting for password, not part of RFB
368 *   SecurityResult
369 *   ClientInitialization - not triggered by server message
370 *   ServerInitialization (to normal)
371 */
372updateState = function(state, statusMsg) {
373    var func, cmsg, oldstate = rfb_state;
374
375    if (state === oldstate) {
376        /* Already here, ignore */
377        Util.Debug("Already in state '" + state + "', ignoring.");
378        return;
379    }
380
381    /*
382     * These are disconnected states. A previous connect may
383     * asynchronously cause a connection so make sure we are closed.
384     */
385    if (state in {'disconnected':1, 'loaded':1, 'connect':1,
386                  'disconnect':1, 'failed':1, 'fatal':1}) {
387        if (sendTimer) {
388            clearInterval(sendTimer);
389            sendTimer = null;
390        }
391
392        if (msgTimer) {
393            clearInterval(msgTimer);
394            msgTimer = null;
395        }
396
397        if (display && display.get_context()) {
398            keyboard.ungrab();
399            mouse.ungrab();
400            display.defaultCursor();
401            if ((Util.get_logging() !== 'debug') ||
402                (state === 'loaded')) {
403                // Show noVNC logo on load and when disconnected if
404                // debug is off
405                display.clear();
406            }
407        }
408
409        ws.close();
410    }
411
412    if (oldstate === 'fatal') {
413        Util.Error("Fatal error, cannot continue");
414    }
415
416    if ((state === 'failed') || (state === 'fatal')) {
417        func = Util.Error;
418    } else {
419        func = Util.Warn;
420    }
421
422    if ((oldstate === 'failed') && (state === 'disconnected')) {
423        // Do disconnect action, but stay in failed state.
424        rfb_state = 'failed';
425    } else {
426        rfb_state = state;
427    }
428
429    cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
430    func("New state '" + rfb_state + "', was '" + oldstate + "'." + cmsg);
431
432    if (connTimer && (rfb_state !== 'connect')) {
433        Util.Debug("Clearing connect timer");
434        clearInterval(connTimer);
435        connTimer = null;
436    }
437
438    if (disconnTimer && (rfb_state !== 'disconnect')) {
439        Util.Debug("Clearing disconnect timer");
440        clearInterval(disconnTimer);
441        disconnTimer = null;
442    }
443
444    switch (state) {
445    case 'normal':
446        if ((oldstate === 'disconnected') || (oldstate === 'failed')) {
447            Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
448        }
449
450        break;
451
452
453    case 'connect':
454
455        connTimer = setTimeout(function () {
456                fail("Connect timeout");
457            }, conf.connectTimeout * 1000);
458
459        init_vars();
460        connect();
461
462        // WebSocket.onopen transitions to 'ProtocolVersion'
463        break;
464
465
466    case 'disconnect':
467
468        if (! test_mode) {
469            disconnTimer = setTimeout(function () {
470                    fail("Disconnect timeout");
471                }, conf.disconnectTimeout * 1000);
472        }
473
474        print_stats();
475
476        // WebSocket.onclose transitions to 'disconnected'
477        break;
478
479
480    case 'failed':
481        if (oldstate === 'disconnected') {
482            Util.Error("Invalid transition from 'disconnected' to 'failed'");
483        }
484        if (oldstate === 'normal') {
485            Util.Error("Error while connected.");
486        }
487        if (oldstate === 'init') {
488            Util.Error("Error while initializing.");
489        }
490
491        // Make sure we transition to disconnected
492        setTimeout(function() { updateState('disconnected'); }, 50);
493
494        break;
495
496
497    default:
498        // No state change action to take
499
500    }
501
502    if ((oldstate === 'failed') && (state === 'disconnected')) {
503        // Leave the failed message
504        conf.updateState(that, state, oldstate); // Obsolete
505        conf.onUpdateState(that, state, oldstate);
506    } else {
507        conf.updateState(that, state, oldstate, statusMsg); // Obsolete
508        conf.onUpdateState(that, state, oldstate, statusMsg);
509    }
510};
511
512fail = function(msg) {
513    updateState('failed', msg);
514    return false;
515};
516
517handle_message = function() {
518    //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen());
519    //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
520    if (ws.rQlen() === 0) {
521        Util.Warn("handle_message called on empty receive queue");
522        return;
523    }
524    switch (rfb_state) {
525    case 'disconnected':
526    case 'failed':
527        Util.Error("Got data while disconnected");
528        break;
529    case 'normal':
530        if (normal_msg() && ws.rQlen() > 0) {
531            // true means we can continue processing
532            // Give other events a chance to run
533            if (msgTimer === null) {
534                Util.Debug("More data to process, creating timer");
535                msgTimer = setTimeout(function () {
536                            msgTimer = null;
537                            handle_message();
538                        }, 10);
539            } else {
540                Util.Debug("More data to process, existing timer");
541            }
542        }
543        break;
544    default:
545        init_msg();
546        break;
547    }
548};
549
550
551function genDES(password, challenge) {
552    var i, passwd = [];
553    for (i=0; i < password.length; i += 1) {
554        passwd.push(password.charCodeAt(i));
555    }
556    return (new DES(passwd)).encrypt(challenge);
557}
558
559function flushClient() {
560    if (mouse_arr.length > 0) {
561        //send(mouse_arr.concat(fbUpdateRequests()));
562        ws.send(mouse_arr);
563        setTimeout(function() {
564                ws.send(fbUpdateRequests());
565            }, 50);
566
567        mouse_arr = [];
568        return true;
569    } else {
570        return false;
571    }
572}
573
574// overridable for testing
575checkEvents = function() {
576    var now;
577    if (rfb_state === 'normal' && !viewportDragging) {
578        if (! flushClient()) {
579            now = new Date().getTime();
580            if (now > last_req_time + conf.fbu_req_rate) {
581                last_req_time = now;
582                ws.send(fbUpdateRequests());
583            }
584        }
585    }
586    setTimeout(checkEvents, conf.check_rate);
587};
588
589keyPress = function(keysym, down) {
590    var arr;
591
592    if (conf.view_only) { return; } // View only, skip keyboard events
593
594    arr = keyEvent(keysym, down);
595    arr = arr.concat(fbUpdateRequests());
596    ws.send(arr);
597};
598
599mouseButton = function(x, y, down, bmask) {
600    if (down) {
601        mouse_buttonMask |= bmask;
602    } else {
603        mouse_buttonMask ^= bmask;
604    }
605
606    if (conf.viewportDrag) {
607        if (down && !viewportDragging) {
608            viewportDragging = true;
609            viewportDragPos = {'x': x, 'y': y};
610
611            // Skip sending mouse events
612            return;
613        } else {
614            viewportDragging = false;
615            ws.send(fbUpdateRequests()); // Force immediate redraw
616        }
617    }
618
619    if (conf.view_only) { return; } // View only, skip mouse events
620
621    mouse_arr = mouse_arr.concat(
622            pointerEvent(display.absX(x), display.absY(y)) );
623    flushClient();
624};
625
626mouseMove = function(x, y) {
627    //Util.Debug('>> mouseMove ' + x + "," + y);
628    var deltaX, deltaY;
629
630    if (viewportDragging) {
631        //deltaX = x - viewportDragPos.x; // drag viewport
632        deltaX = viewportDragPos.x - x; // drag frame buffer
633        //deltaY = y - viewportDragPos.y; // drag viewport
634        deltaY = viewportDragPos.y - y; // drag frame buffer
635        viewportDragPos = {'x': x, 'y': y};
636
637        display.viewportChange(deltaX, deltaY);
638
639        // Skip sending mouse events
640        return;
641    }
642
643    if (conf.view_only) { return; } // View only, skip mouse events
644
645    mouse_arr = mouse_arr.concat(
646            pointerEvent(display.absX(x), display.absY(y)) );
647};
648
649
650//
651// Server message handlers
652//
653
654// RFB/VNC initialisation message handler
655init_msg = function() {
656    //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']");
657
658    var strlen, reason, length, sversion, cversion,
659        i, types, num_types, challenge, response, bpp, depth,
660        big_endian, red_max, green_max, blue_max, red_shift,
661        green_shift, blue_shift, true_color, name_length;
662
663    //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0));
664    switch (rfb_state) {
665
666    case 'ProtocolVersion' :
667        if (ws.rQlen() < 12) {
668            return fail("Incomplete protocol version");
669        }
670        sversion = ws.rQshiftStr(12).substr(4,7);
671        Util.Info("Server ProtocolVersion: " + sversion);
672        switch (sversion) {
673            case "003.003": rfb_version = 3.3; break;
674            case "003.006": rfb_version = 3.3; break;  // UltraVNC
675            case "003.889": rfb_version = 3.3; break;  // Apple Remote Desktop
676            case "003.007": rfb_version = 3.7; break;
677            case "003.008": rfb_version = 3.8; break;
678            case "004.000": rfb_version = 3.8; break;  // Intel AMT KVM
679            default:
680                return fail("Invalid server version " + sversion);
681        }
682        if (rfb_version > rfb_max_version) {
683            rfb_version = rfb_max_version;
684        }
685
686        if (! test_mode) {
687            sendTimer = setInterval(function() {
688                    // Send updates either at a rate of one update
689                    // every 50ms, or whatever slower rate the network
690                    // can handle.
691                    ws.flush();
692                }, 50);
693        }
694
695        cversion = "00" + parseInt(rfb_version,10) +
696                   ".00" + ((rfb_version * 10) % 10);
697        ws.send_string("RFB " + cversion + "\n");
698        updateState('Security', "Sent ProtocolVersion: " + cversion);
699        break;
700
701    case 'Security' :
702        if (rfb_version >= 3.7) {
703            // Server sends supported list, client decides
704            num_types = ws.rQshift8();
705            if (ws.rQwait("security type", num_types, 1)) { return false; }
706            if (num_types === 0) {
707                strlen = ws.rQshift32();
708                reason = ws.rQshiftStr(strlen);
709                return fail("Security failure: " + reason);
710            }
711            rfb_auth_scheme = 0;
712            types = ws.rQshiftBytes(num_types);
713            Util.Debug("Server security types: " + types);
714            for (i=0; i < types.length; i+=1) {
715                if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) {
716                    rfb_auth_scheme = types[i];
717                }
718            }
719            if (rfb_auth_scheme === 0) {
720                return fail("Unsupported security types: " + types);
721            }
722
723            ws.send([rfb_auth_scheme]);
724        } else {
725            // Server decides
726            if (ws.rQwait("security scheme", 4)) { return false; }
727            rfb_auth_scheme = ws.rQshift32();
728        }
729        updateState('Authentication',
730                "Authenticating using scheme: " + rfb_auth_scheme);
731        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
732        break;
733
734    // Triggered by fallthough, not by server message
735    case 'Authentication' :
736        //Util.Debug("Security auth scheme: " + rfb_auth_scheme);
737        switch (rfb_auth_scheme) {
738            case 0:  // connection failed
739                if (ws.rQwait("auth reason", 4)) { return false; }
740                strlen = ws.rQshift32();
741                reason = ws.rQshiftStr(strlen);
742                return fail("Auth failure: " + reason);
743            case 1:  // no authentication
744                if (rfb_version >= 3.8) {
745                    updateState('SecurityResult');
746                    return;
747                }
748                // Fall through to ClientInitialisation
749                break;
750            case 2:  // VNC authentication
751                if (rfb_password.length === 0) {
752                    // Notify via both callbacks since it is kind of
753                    // a RFB state change and a UI interface issue.
754                    updateState('password', "Password Required");
755                    conf.onPasswordRequired(that);
756                    return;
757                }
758                if (ws.rQwait("auth challenge", 16)) { return false; }
759                challenge = ws.rQshiftBytes(16);
760                //Util.Debug("Password: " + rfb_password);
761                //Util.Debug("Challenge: " + challenge +
762                //           " (" + challenge.length + ")");
763                response = genDES(rfb_password, challenge);
764                //Util.Debug("Response: " + response +
765                //           " (" + response.length + ")");
766
767                //Util.Debug("Sending DES encrypted auth response");
768                ws.send(response);
769                updateState('SecurityResult');
770                return;
771            default:
772                fail("Unsupported auth scheme: " + rfb_auth_scheme);
773                return;
774        }
775        updateState('ClientInitialisation', "No auth required");
776        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
777        break;
778
779    case 'SecurityResult' :
780        if (ws.rQwait("VNC auth response ", 4)) { return false; }
781        switch (ws.rQshift32()) {
782            case 0:  // OK
783                // Fall through to ClientInitialisation
784                break;
785            case 1:  // failed
786                if (rfb_version >= 3.8) {
787                    length = ws.rQshift32();
788                    if (ws.rQwait("SecurityResult reason", length, 8)) {
789                        return false;
790                    }
791                    reason = ws.rQshiftStr(length);
792                    fail(reason);
793                } else {
794                    fail("Authentication failed");
795                }
796                return;
797            case 2:  // too-many
798                return fail("Too many auth attempts");
799        }
800        updateState('ClientInitialisation', "Authentication OK");
801        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
802        break;
803
804    // Triggered by fallthough, not by server message
805    case 'ClientInitialisation' :
806        ws.send([conf.shared ? 1 : 0]); // ClientInitialisation
807        updateState('ServerInitialisation', "Authentication OK");
808        break;
809
810    case 'ServerInitialisation' :
811        if (ws.rQwait("server initialization", 24)) { return false; }
812
813        /* Screen size */
814        fb_width  = ws.rQshift16();
815        fb_height = ws.rQshift16();
816
817        /* PIXEL_FORMAT */
818        bpp            = ws.rQshift8();
819        depth          = ws.rQshift8();
820        big_endian     = ws.rQshift8();
821        true_color     = ws.rQshift8();
822
823        red_max        = ws.rQshift16();
824        green_max      = ws.rQshift16();
825        blue_max       = ws.rQshift16();
826        red_shift      = ws.rQshift8();
827        green_shift    = ws.rQshift8();
828        blue_shift     = ws.rQshift8();
829        ws.rQshiftStr(3); // padding
830
831        Util.Info("Screen: " + fb_width + "x" + fb_height +
832                  ", bpp: " + bpp + ", depth: " + depth +
833                  ", big_endian: " + big_endian +
834                  ", true_color: " + true_color +
835                  ", red_max: " + red_max +
836                  ", green_max: " + green_max +
837                  ", blue_max: " + blue_max +
838                  ", red_shift: " + red_shift +
839                  ", green_shift: " + green_shift +
840                  ", blue_shift: " + blue_shift);
841
842        if (big_endian !== 0) {
843            Util.Warn("Server native endian is not little endian");
844        }
845        if (red_shift !== 16) {
846            Util.Warn("Server native red-shift is not 16");
847        }
848        if (blue_shift !== 0) {
849            Util.Warn("Server native blue-shift is not 0");
850        }
851
852        /* Connection name/title */
853        name_length   = ws.rQshift32();
854        fb_name = ws.rQshiftStr(name_length);
855
856        if (conf.true_color && fb_name === "Intel(r) AMT KVM")
857        {
858            Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color");
859            conf.true_color = false;
860        }
861
862        display.set_true_color(conf.true_color);
863        display.resize(fb_width, fb_height);
864        keyboard.grab();
865        mouse.grab();
866
867        if (conf.true_color) {
868            fb_Bpp           = 4;
869            fb_depth         = 3;
870        } else {
871            fb_Bpp           = 1;
872            fb_depth         = 1;
873        }
874
875        response = pixelFormat();
876        response = response.concat(clientEncodings());
877        response = response.concat(fbUpdateRequests());
878        timing.fbu_rt_start = (new Date()).getTime();
879        ws.send(response);
880
881        /* Start pushing/polling */
882        setTimeout(checkEvents, conf.check_rate);
883        setTimeout(scan_tight_imgQ, scan_imgQ_rate);
884
885        if (conf.encrypt) {
886            updateState('normal', "Connected (encrypted) to: " + fb_name);
887        } else {
888            updateState('normal', "Connected (unencrypted) to: " + fb_name);
889        }
890        break;
891    }
892    //Util.Debug("<< init_msg");
893};
894
895
896/* Normal RFB/VNC server message handler */
897normal_msg = function() {
898    //Util.Debug(">> normal_msg");
899
900    var ret = true, msg_type, length, text,
901        c, first_colour, num_colours, red, green, blue;
902
903    if (FBU.rects > 0) {
904        msg_type = 0;
905    } else {
906        msg_type = ws.rQshift8();
907    }
908    switch (msg_type) {
909    case 0:  // FramebufferUpdate
910        ret = framebufferUpdate(); // false means need more data
911        break;
912    case 1:  // SetColourMapEntries
913        Util.Debug("SetColourMapEntries");
914        ws.rQshift8();  // Padding
915        first_colour = ws.rQshift16(); // First colour
916        num_colours = ws.rQshift16();
917        if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; }
918
919        for (c=0; c < num_colours; c+=1) {
920            red = ws.rQshift16();
921            //Util.Debug("red before: " + red);
922            red = parseInt(red / 256, 10);
923            //Util.Debug("red after: " + red);
924            green = parseInt(ws.rQshift16() / 256, 10);
925            blue = parseInt(ws.rQshift16() / 256, 10);
926            display.set_colourMap([blue, green, red], first_colour + c);
927        }
928        Util.Debug("colourMap: " + display.get_colourMap());
929        Util.Info("Registered " + num_colours + " colourMap entries");
930        //Util.Debug("colourMap: " + display.get_colourMap());
931        break;
932    case 2:  // Bell
933        Util.Debug("Bell");
934        conf.onBell(that);
935        break;
936    case 3:  // ServerCutText
937        Util.Debug("ServerCutText");
938        if (ws.rQwait("ServerCutText header", 7, 1)) { return false; }
939        ws.rQshiftBytes(3);  // Padding
940        length = ws.rQshift32();
941        if (ws.rQwait("ServerCutText", length, 8)) { return false; }
942
943        text = ws.rQshiftStr(length);
944        conf.clipboardReceive(that, text); // Obsolete
945        conf.onClipboard(that, text);
946        break;
947    default:
948        fail("Disconnected: illegal server message type " + msg_type);
949        Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
950        break;
951    }
952    //Util.Debug("<< normal_msg");
953    return ret;
954};
955
956framebufferUpdate = function() {
957    var now, hdr, fbu_rt_diff, ret = true;
958
959    if (FBU.rects === 0) {
960        //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20));
961        if (ws.rQwait("FBU header", 3)) {
962            ws.rQunshift8(0);  // FBU msg_type
963            return false;
964        }
965        ws.rQshift8();  // padding
966        FBU.rects = ws.rQshift16();
967        //Util.Debug("FramebufferUpdate, rects:" + FBU.rects);
968        FBU.bytes = 0;
969        timing.cur_fbu = 0;
970        if (timing.fbu_rt_start > 0) {
971            now = (new Date()).getTime();
972            Util.Info("First FBU latency: " + (now - timing.fbu_rt_start));
973        }
974    }
975
976    while (FBU.rects > 0) {
977        if (rfb_state !== "normal") {
978            return false;
979        }
980        if (ws.rQwait("FBU", FBU.bytes)) { return false; }
981        if (FBU.bytes === 0) {
982            if (ws.rQwait("rect header", 12)) { return false; }
983            /* New FramebufferUpdate */
984
985            hdr = ws.rQshiftBytes(12);
986            FBU.x      = (hdr[0] << 8) + hdr[1];
987            FBU.y      = (hdr[2] << 8) + hdr[3];
988            FBU.width  = (hdr[4] << 8) + hdr[5];
989            FBU.height = (hdr[6] << 8) + hdr[7];
990            FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
991                                    (hdr[10] << 8) +  hdr[11], 10);
992
993            conf.onFBUReceive(that,
994                    {'x': FBU.x, 'y': FBU.y,
995                     'width': FBU.width, 'height': FBU.height,
996                     'encoding': FBU.encoding,
997                     'encodingName': encNames[FBU.encoding]});
998
999            if (encNames[FBU.encoding]) {
1000                // Debug:
1001                /*
1002                var msg =  "FramebufferUpdate rects:" + FBU.rects;
1003                msg += " x: " + FBU.x + " y: " + FBU.y;
1004                msg += " width: " + FBU.width + " height: " + FBU.height;
1005                msg += " encoding:" + FBU.encoding;
1006                msg += "(" + encNames[FBU.encoding] + ")";
1007                msg += ", ws.rQlen(): " + ws.rQlen();
1008                Util.Debug(msg);
1009                */
1010            } else {
1011                fail("Disconnected: unsupported encoding " +
1012                    FBU.encoding);
1013                return false;
1014            }
1015        }
1016
1017        timing.last_fbu = (new Date()).getTime();
1018
1019        ret = encHandlers[FBU.encoding]();
1020
1021        now = (new Date()).getTime();
1022        timing.cur_fbu += (now - timing.last_fbu);
1023
1024        if (ret) {
1025            encStats[FBU.encoding][0] += 1;
1026            encStats[FBU.encoding][1] += 1;
1027            timing.pixels += FBU.width * FBU.height;
1028        }
1029
1030        if (FBU.rects === 0 || (timing.pixels >= (fb_width * fb_height))) {
1031            if (((FBU.width === fb_width) &&
1032                        (FBU.height === fb_height)) ||
1033                    (timing.fbu_rt_start > 0)) {
1034                timing.full_fbu_total += timing.cur_fbu;
1035                timing.full_fbu_cnt += 1;
1036                Util.Info("Timing of full FBU, cur: " +
1037                          timing.cur_fbu + ", total: " +
1038                          timing.full_fbu_total + ", cnt: " +
1039                          timing.full_fbu_cnt + ", avg: " +
1040                          (timing.full_fbu_total /
1041                              timing.full_fbu_cnt));
1042            }
1043            if (timing.fbu_rt_start > 0) {
1044                fbu_rt_diff = now - timing.fbu_rt_start;
1045                timing.fbu_rt_total += fbu_rt_diff;
1046                timing.fbu_rt_cnt += 1;
1047                Util.Info("full FBU round-trip, cur: " +
1048                          fbu_rt_diff + ", total: " +
1049                          timing.fbu_rt_total + ", cnt: " +
1050                          timing.fbu_rt_cnt + ", avg: " +
1051                          (timing.fbu_rt_total /
1052                              timing.fbu_rt_cnt));
1053                timing.fbu_rt_start = 0;
1054            }
1055        }
1056        if (! ret) {
1057            return ret; // false ret means need more data
1058        }
1059    }
1060
1061    conf.onFBUComplete(that,
1062            {'x': FBU.x, 'y': FBU.y,
1063                'width': FBU.width, 'height': FBU.height,
1064                'encoding': FBU.encoding,
1065                'encodingName': encNames[FBU.encoding]});
1066
1067    return true; // We finished this FBU
1068};
1069
1070//
1071// FramebufferUpdate encodings
1072//
1073
1074encHandlers.RAW = function display_raw() {
1075    //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)");
1076
1077    var cur_y, cur_height;
1078
1079    if (FBU.lines === 0) {
1080        FBU.lines = FBU.height;
1081    }
1082    FBU.bytes = FBU.width * fb_Bpp; // At least a line
1083    if (ws.rQwait("RAW", FBU.bytes)) { return false; }
1084    cur_y = FBU.y + (FBU.height - FBU.lines);
1085    cur_height = Math.min(FBU.lines,
1086                          Math.floor(ws.rQlen()/(FBU.width * fb_Bpp)));
1087    display.blitImage(FBU.x, cur_y, FBU.width, cur_height,
1088            ws.get_rQ(), ws.get_rQi());
1089    ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp);
1090    FBU.lines -= cur_height;
1091
1092    if (FBU.lines > 0) {
1093        FBU.bytes = FBU.width * fb_Bpp; // At least another line
1094    } else {
1095        FBU.rects -= 1;
1096        FBU.bytes = 0;
1097    }
1098    //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)");
1099    return true;
1100};
1101
1102encHandlers.COPYRECT = function display_copy_rect() {
1103    //Util.Debug(">> display_copy_rect");
1104
1105    var old_x, old_y;
1106
1107    if (ws.rQwait("COPYRECT", 4)) { return false; }
1108    old_x = ws.rQshift16();
1109    old_y = ws.rQshift16();
1110    display.copyImage(old_x, old_y, FBU.x, FBU.y, FBU.width, FBU.height);
1111    FBU.rects -= 1;
1112    FBU.bytes = 0;
1113    return true;
1114};
1115
1116encHandlers.RRE = function display_rre() {
1117    //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)");
1118    var color, x, y, width, height, chunk;
1119
1120    if (FBU.subrects === 0) {
1121        if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; }
1122        FBU.subrects = ws.rQshift32();
1123        color = ws.rQshiftBytes(fb_Bpp); // Background
1124        display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
1125    }
1126    while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) {
1127        color = ws.rQshiftBytes(fb_Bpp);
1128        x = ws.rQshift16();
1129        y = ws.rQshift16();
1130        width = ws.rQshift16();
1131        height = ws.rQshift16();
1132        display.fillRect(FBU.x + x, FBU.y + y, width, height, color);
1133        FBU.subrects -= 1;
1134    }
1135    //Util.Debug("   display_rre: rects: " + FBU.rects +
1136    //           ", FBU.subrects: " + FBU.subrects);
1137
1138    if (FBU.subrects > 0) {
1139        chunk = Math.min(rre_chunk_sz, FBU.subrects);
1140        FBU.bytes = (fb_Bpp + 8) * chunk;
1141    } else {
1142        FBU.rects -= 1;
1143        FBU.bytes = 0;
1144    }
1145    //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes);
1146    return true;
1147};
1148
1149encHandlers.HEXTILE = function display_hextile() {
1150    //Util.Debug(">> display_hextile");
1151    var subencoding, subrects, color, cur_tile,
1152        tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh,
1153        rQ = ws.get_rQ(), rQi = ws.get_rQi();
1154
1155    if (FBU.tiles === 0) {
1156        FBU.tiles_x = Math.ceil(FBU.width/16);
1157        FBU.tiles_y = Math.ceil(FBU.height/16);
1158        FBU.total_tiles = FBU.tiles_x * FBU.tiles_y;
1159        FBU.tiles = FBU.total_tiles;
1160    }
1161
1162    /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */
1163    while (FBU.tiles > 0) {
1164        FBU.bytes = 1;
1165        if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; }
1166        subencoding = rQ[rQi];  // Peek
1167        if (subencoding > 30) { // Raw
1168            fail("Disconnected: illegal hextile subencoding " + subencoding);
1169            //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
1170            return false;
1171        }
1172        subrects = 0;
1173        cur_tile = FBU.total_tiles - FBU.tiles;
1174        tile_x = cur_tile % FBU.tiles_x;
1175        tile_y = Math.floor(cur_tile / FBU.tiles_x);
1176        x = FBU.x + tile_x * 16;
1177        y = FBU.y + tile_y * 16;
1178        w = Math.min(16, (FBU.x + FBU.width) - x);
1179        h = Math.min(16, (FBU.y + FBU.height) - y);
1180
1181        /* Figure out how much we are expecting */
1182        if (subencoding & 0x01) { // Raw
1183            //Util.Debug("   Raw subencoding");
1184            FBU.bytes += w * h * fb_Bpp;
1185        } else {
1186            if (subencoding & 0x02) { // Background
1187                FBU.bytes += fb_Bpp;
1188            }
1189            if (subencoding & 0x04) { // Foreground
1190                FBU.bytes += fb_Bpp;
1191            }
1192            if (subencoding & 0x08) { // AnySubrects
1193                FBU.bytes += 1;   // Since we aren't shifting it off
1194                if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; }
1195                subrects = rQ[rQi + FBU.bytes-1]; // Peek
1196                if (subencoding & 0x10) { // SubrectsColoured
1197                    FBU.bytes += subrects * (fb_Bpp + 2);
1198                } else {
1199                    FBU.bytes += subrects * 2;
1200                }
1201            }
1202        }
1203
1204        /*
1205        Util.Debug("   tile:" + cur_tile + "/" + (FBU.total_tiles - 1) +
1206              " (" + tile_x + "," + tile_y + ")" +
1207              " [" + x + "," + y + "]@" + w + "x" + h +
1208              ", subenc:" + subencoding +
1209              "(last: " + FBU.lastsubencoding + "), subrects:" +
1210              subrects +
1211              ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes +
1212              " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) +
1213              " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10));
1214        */
1215        if (ws.rQwait("hextile", FBU.bytes)) { return false; }
1216
1217        /* We know the encoding and have a whole tile */
1218        FBU.subencoding = rQ[rQi];
1219        rQi += 1;
1220        if (FBU.subencoding === 0) {
1221            if (FBU.lastsubencoding & 0x01) {
1222                /* Weird: ignore blanks after RAW */
1223                Util.Debug("     Ignoring blank after RAW");
1224            } else {
1225                display.fillRect(x, y, w, h, FBU.background);
1226            }
1227        } else if (FBU.subencoding & 0x01) { // Raw
1228            display.blitImage(x, y, w, h, rQ, rQi);
1229            rQi += FBU.bytes - 1;
1230        } else {
1231            if (FBU.subencoding & 0x02) { // Background
1232                FBU.background = rQ.slice(rQi, rQi + fb_Bpp);
1233                rQi += fb_Bpp;
1234            }
1235            if (FBU.subencoding & 0x04) { // Foreground
1236                FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp);
1237                rQi += fb_Bpp;
1238            }
1239
1240            display.startTile(x, y, w, h, FBU.background);
1241            if (FBU.subencoding & 0x08) { // AnySubrects
1242                subrects = rQ[rQi];
1243                rQi += 1;
1244                for (s = 0; s < subrects; s += 1) {
1245                    if (FBU.subencoding & 0x10) { // SubrectsColoured
1246                        color = rQ.slice(rQi, rQi + fb_Bpp);
1247                        rQi += fb_Bpp;
1248                    } else {
1249                        color = FBU.foreground;
1250                    }
1251                    xy = rQ[rQi];
1252                    rQi += 1;
1253                    sx = (xy >> 4);
1254                    sy = (xy & 0x0f);
1255
1256                    wh = rQ[rQi];
1257                    rQi += 1;
1258                    sw = (wh >> 4)   + 1;
1259                    sh = (wh & 0x0f) + 1;
1260
1261                    display.subTile(sx, sy, sw, sh, color);
1262                }
1263            }
1264            display.finishTile();
1265        }
1266        ws.set_rQi(rQi);
1267        FBU.lastsubencoding = FBU.subencoding;
1268        FBU.bytes = 0;
1269        FBU.tiles -= 1;
1270    }
1271
1272    if (FBU.tiles === 0) {
1273        FBU.rects -= 1;
1274    }
1275
1276    //Util.Debug("<< display_hextile");
1277    return true;
1278};
1279
1280
1281// Get 'compact length' header and data size
1282getTightCLength = function (arr) {
1283    var header = 1, data = 0;
1284    data += arr[0] & 0x7f;
1285    if (arr[0] & 0x80) {
1286        header += 1;
1287        data += (arr[1] & 0x7f) << 7;
1288        if (arr[1] & 0x80) {
1289            header += 1;
1290            data += arr[2] << 14;
1291        }
1292    }
1293    return [header, data];
1294};
1295
1296function display_tight(isTightPNG) {
1297    //Util.Debug(">> display_tight");
1298
1299    if (fb_depth === 1) {
1300        fail("Tight protocol handler only implements true color mode");
1301    }
1302
1303    var ctl, cmode, clength, color, img, data;
1304    var filterId = -1, resetStreams = 0, streamId = -1;
1305    var rQ = ws.get_rQ(), rQi = ws.get_rQi();
1306
1307    FBU.bytes = 1; // compression-control byte
1308    if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; }
1309
1310    var checksum = function(data) {
1311        var sum=0, i;
1312        for (i=0; i<data.length;i++) {
1313            sum += data[i];
1314            if (sum > 65536) sum -= 65536;
1315        }
1316        return sum;
1317    }
1318
1319    var decompress = function(data) {
1320        for (var i=0; i<4; i++) {
1321            if ((resetStreams >> i) & 1) {
1322                FBU.zlibs[i].reset();
1323                Util.Info("Reset zlib stream " + i);
1324            }
1325        }
1326        var uncompressed = FBU.zlibs[streamId].uncompress(data, 0);
1327        if (uncompressed.status !== 0) {
1328            Util.Error("Invalid data in zlib stream");
1329        }
1330        //Util.Warn("Decompressed " + data.length + " to " +
1331        //    uncompressed.data.length + " checksums " +
1332        //    checksum(data) + ":" + checksum(uncompressed.data));
1333
1334        return uncompressed.data;
1335    }
1336
1337    var handlePalette = function() {
1338        var numColors = rQ[rQi + 2] + 1;
1339        var paletteSize = numColors * fb_depth;
1340        FBU.bytes += paletteSize;
1341        if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; }
1342
1343        var bpp = (numColors <= 2) ? 1 : 8;
1344        var rowSize = Math.floor((FBU.width * bpp + 7) / 8);
1345        var raw = false;
1346        if (rowSize * FBU.height < 12) {
1347            raw = true;
1348            clength = [0, rowSize * FBU.height];
1349        } else {
1350            clength = getTightCLength(ws.rQslice(3 + paletteSize,
1351                                                 3 + paletteSize + 3));
1352        }
1353        FBU.bytes += clength[0] + clength[1];
1354        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
1355
1356        // Shift ctl, filter id, num colors, palette entries, and clength off
1357        ws.rQshiftBytes(3);
1358        var palette = ws.rQshiftBytes(paletteSize);
1359        ws.rQshiftBytes(clength[0]);
1360
1361        if (raw) {
1362            data = ws.rQshiftBytes(clength[1]);
1363        } else {
1364            data = decompress(ws.rQshiftBytes(clength[1]));
1365        }
1366
1367        // Convert indexed (palette based) image data to RGB
1368        // TODO: reduce number of calculations inside loop
1369        var dest = [];
1370        var x, y, b, w, w1, dp, sp;
1371        if (numColors === 2) {
1372            w = Math.floor((FBU.width + 7) / 8);
1373            w1 = Math.floor(FBU.width / 8);
1374            for (y = 0; y < FBU.height; y++) {
1375                for (x = 0; x < w1; x++) {
1376                    for (b = 7; b >= 0; b--) {
1377                        dp = (y*FBU.width + x*8 + 7-b) * 3;
1378                        sp = (data[y*w + x] >> b & 1) * 3;
1379                        dest[dp  ] = palette[sp  ];
1380                        dest[dp+1] = palette[sp+1];
1381                        dest[dp+2] = palette[sp+2];
1382                    }
1383                }
1384                for (b = 7; b >= 8 - FBU.width % 8; b--) {
1385                    dp = (y*FBU.width + x*8 + 7-b) * 3;
1386                    sp = (data[y*w + x] >> b & 1) * 3;
1387                    dest[dp  ] = palette[sp  ];
1388                    dest[dp+1] = palette[sp+1];
1389                    dest[dp+2] = palette[sp+2];
1390                }
1391            }
1392        } else {
1393            for (y = 0; y < FBU.height; y++) {
1394                for (x = 0; x < FBU.width; x++) {
1395                    dp = (y*FBU.width + x) * 3;
1396                    sp = data[y*FBU.width + x] * 3;
1397                    dest[dp  ] = palette[sp  ];
1398                    dest[dp+1] = palette[sp+1];
1399                    dest[dp+2] = palette[sp+2];
1400                }
1401            }
1402        }
1403
1404        FBU.imgQ.push({
1405                'type': 'rgb',
1406                'img':  {'complete': true, 'data': dest},
1407                'x': FBU.x,
1408                'y': FBU.y,
1409                'width': FBU.width,
1410                'height': FBU.height});
1411        return true;
1412    }
1413
1414    var handleCopy = function() {
1415        var raw = false;
1416        var uncompressedSize = FBU.width * FBU.height * fb_depth;
1417        if (uncompressedSize < 12) {
1418            raw = true;
1419            clength = [0, uncompressedSize];
1420        } else {
1421            clength = getTightCLength(ws.rQslice(1, 4));
1422        }
1423        FBU.bytes = 1 + clength[0] + clength[1];
1424        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
1425
1426        // Shift ctl, clength off
1427        ws.rQshiftBytes(1 + clength[0]);
1428
1429        if (raw) {
1430            data = ws.rQshiftBytes(clength[1]);
1431        } else {
1432            data = decompress(ws.rQshiftBytes(clength[1]));
1433        }
1434
1435        FBU.imgQ.push({
1436                'type': 'rgb',
1437                'img':  {'complete': true, 'data': data},
1438                'x': FBU.x,
1439                'y': FBU.y,
1440                'width': FBU.width,
1441                'height': FBU.height});
1442        return true;
1443    }
1444
1445    ctl = ws.rQpeek8();
1446
1447    // Keep tight reset bits
1448    resetStreams = ctl & 0xF;
1449
1450    // Figure out filter
1451    ctl = ctl >> 4;
1452    streamId = ctl & 0x3;
1453
1454    if (ctl === 0x08)      cmode = "fill";
1455    else if (ctl === 0x09) cmode = "jpeg";
1456    else if (ctl === 0x0A) cmode = "png";
1457    else if (ctl & 0x04)   cmode = "filter";
1458    else if (ctl < 0x04)   cmode = "copy";
1459    else throw("Illegal tight compression received, ctl: " + ctl);
1460
1461    if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
1462        throw("filter/copy received in tightPNG mode");
1463    }
1464
1465    switch (cmode) {
1466        // fill uses fb_depth because TPIXELs drop the padding byte
1467        case "fill":   FBU.bytes += fb_depth; break; // TPIXEL
1468        case "jpeg":   FBU.bytes += 3;        break; // max clength
1469        case "png":    FBU.bytes += 3;        break; // max clength
1470        case "filter": FBU.bytes += 2;        break; // filter id + num colors if palette
1471        case "copy":                          break;
1472    }
1473
1474    if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
1475
1476    //Util.Debug("   ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
1477    //Util.Debug("   cmode: " + cmode);
1478
1479    // Determine FBU.bytes
1480    switch (cmode) {
1481    case "fill":
1482        ws.rQshift8(); // shift off ctl
1483        color = ws.rQshiftBytes(fb_depth);
1484        FBU.imgQ.push({
1485                'type': 'fill',
1486                'img': {'complete': true},
1487                'x': FBU.x,
1488                'y': FBU.y,
1489                'width': FBU.width,
1490                'height': FBU.height,
1491                'color': [color[2], color[1], color[0]] });
1492        break;
1493    case "png":
1494    case "jpeg":
1495        clength = getTightCLength(ws.rQslice(1, 4));
1496        FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
1497        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }
1498
1499        // We have everything, render it
1500        //Util.Debug("   jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " +
1501        //           clength[0] + ", clength[1]: " + clength[1]);
1502        ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length
1503        img = new Image();
1504        //img.onload = scan_tight_imgQ;
1505        FBU.imgQ.push({
1506                'type': 'img',
1507                'img': img,
1508                'x': FBU.x,
1509                'y': FBU.y});
1510        img.src = "data:image/" + cmode +
1511            extract_data_uri(ws.rQshiftBytes(clength[1]));
1512        img = null;
1513        break;
1514    case "filter":
1515        filterId = rQ[rQi + 1];
1516        if (filterId === 1) {
1517            if (!handlePalette()) { return false; }
1518        } else {
1519            // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
1520            // Filter 2, Gradient is valid but not used if jpeg is enabled
1521            throw("Unsupported tight subencoding received, filter: " + filterId);
1522        }
1523        break;
1524    case "copy":
1525        if (!handleCopy()) { return false; }
1526        break;
1527    }
1528
1529    FBU.bytes = 0;
1530    FBU.rects -= 1;
1531    //Util.Debug("   ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
1532    //Util.Debug("<< display_tight_png");
1533    return true;
1534}
1535
1536extract_data_uri = function(arr) {
1537    //var i, stra = [];
1538    //for (i=0; i< arr.length; i += 1) {
1539    //    stra.push(String.fromCharCode(arr[i]));
1540    //}
1541    //return "," + escape(stra.join(''));
1542    return ";base64," + Base64.encode(arr);
1543};
1544
1545scan_tight_imgQ = function() {
1546    var data, imgQ, ctx;
1547    ctx = display.get_context();
1548    if (rfb_state === 'normal') {
1549        imgQ = FBU.imgQ;
1550        while ((imgQ.length > 0) && (imgQ[0].img.complete)) {
1551            data = imgQ.shift();
1552            if (data.type === 'fill') {
1553                display.fillRect(data.x, data.y, data.width, data.height, data.color);
1554            } else if (data.type === 'rgb') {
1555                display.blitRgbImage(data.x, data.y, data.width, data.height, data.img.data, 0);
1556            } else {
1557                ctx.drawImage(data.img, data.x, data.y);
1558            }
1559        }
1560        setTimeout(scan_tight_imgQ, scan_imgQ_rate);
1561    }
1562};
1563
1564encHandlers.TIGHT = function () { return display_tight(false); };
1565encHandlers.TIGHT_PNG = function () { return display_tight(true); };
1566
1567encHandlers.last_rect = function last_rect() {
1568    Util.Debug(">> set_desktopsize");
1569    FBU.rects = 0;
1570    Util.Debug("<< set_desktopsize");
1571    return true;
1572};
1573
1574encHandlers.DesktopSize = function set_desktopsize() {
1575    Util.Debug(">> set_desktopsize");
1576    fb_width = FBU.width;
1577    fb_height = FBU.height;
1578    display.resize(fb_width, fb_height);
1579    timing.fbu_rt_start = (new Date()).getTime();
1580    // Send a new non-incremental request
1581    ws.send(fbUpdateRequests());
1582
1583    FBU.bytes = 0;
1584    FBU.rects -= 1;
1585
1586    Util.Debug("<< set_desktopsize");
1587    return true;
1588};
1589
1590encHandlers.Cursor = function set_cursor() {
1591    var x, y, w, h, pixelslength, masklength;
1592    //Util.Debug(">> set_cursor");
1593    x = FBU.x;  // hotspot-x
1594    y = FBU.y;  // hotspot-y
1595    w = FBU.width;
1596    h = FBU.height;
1597
1598    pixelslength = w * h * fb_Bpp;
1599    masklength = Math.floor((w + 7) / 8) * h;
1600
1601    FBU.bytes = pixelslength + masklength;
1602    if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; }
1603
1604    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);
1605
1606    display.changeCursor(ws.rQshiftBytes(pixelslength),
1607                            ws.rQshiftBytes(masklength),
1608                            x, y, w, h);
1609
1610    FBU.bytes = 0;
1611    FBU.rects -= 1;
1612
1613    //Util.Debug("<< set_cursor");
1614    return true;
1615};
1616
1617encHandlers.JPEG_quality_lo = function set_jpeg_quality() {
1618    Util.Error("Server sent jpeg_quality pseudo-encoding");
1619};
1620
1621encHandlers.compress_lo = function set_compress_level() {
1622    Util.Error("Server sent compress level pseudo-encoding");
1623};
1624
1625/*
1626 * Client message routines
1627 */
1628
1629pixelFormat = function() {
1630    //Util.Debug(">> pixelFormat");
1631    var arr;
1632    arr = [0];     // msg-type
1633    arr.push8(0);  // padding
1634    arr.push8(0);  // padding
1635    arr.push8(0);  // padding
1636
1637    arr.push8(fb_Bpp * 8); // bits-per-pixel
1638    arr.push8(fb_depth * 8); // depth
1639    arr.push8(0);  // little-endian
1640    arr.push8(conf.true_color ? 1 : 0);  // true-color
1641
1642    arr.push16(255);  // red-max
1643    arr.push16(255);  // green-max
1644    arr.push16(255);  // blue-max
1645    arr.push8(16);    // red-shift
1646    arr.push8(8);     // green-shift
1647    arr.push8(0);     // blue-shift
1648
1649    arr.push8(0);     // padding
1650    arr.push8(0);     // padding
1651    arr.push8(0);     // padding
1652    //Util.Debug("<< pixelFormat");
1653    return arr;
1654};
1655
1656clientEncodings = function() {
1657    //Util.Debug(">> clientEncodings");
1658    var arr, i, encList = [];
1659
1660    for (i=0; i<encodings.length; i += 1) {
1661        if ((encodings[i][0] === "Cursor") &&
1662            (! conf.local_cursor)) {
1663            Util.Debug("Skipping Cursor pseudo-encoding");
1664        } else {
1665            //Util.Debug("Adding encoding: " + encodings[i][0]);
1666            encList.push(encodings[i][1]);
1667        }
1668    }
1669
1670    arr = [2];     // msg-type
1671    arr.push8(0);  // padding
1672
1673    arr.push16(encList.length); // encoding count
1674    for (i=0; i < encList.length; i += 1) {
1675        arr.push32(encList[i]);
1676    }
1677    //Util.Debug("<< clientEncodings: " + arr);
1678    return arr;
1679};
1680
1681fbUpdateRequest = function(incremental, x, y, xw, yw) {
1682    //Util.Debug(">> fbUpdateRequest");
1683    if (typeof(x) === "undefined") { x = 0; }
1684    if (typeof(y) === "undefined") { y = 0; }
1685    if (typeof(xw) === "undefined") { xw = fb_width; }
1686    if (typeof(yw) === "undefined") { yw = fb_height; }
1687    var arr;
1688    arr = [3];  // msg-type
1689    arr.push8(incremental);
1690    arr.push16(x);
1691    arr.push16(y);
1692    arr.push16(xw);
1693    arr.push16(yw);
1694    //Util.Debug("<< fbUpdateRequest");
1695    return arr;
1696};
1697
1698// Based on clean/dirty areas, generate requests to send
1699fbUpdateRequests = function() {
1700    var cleanDirty = display.getCleanDirtyReset(),
1701        arr = [], i, cb, db;
1702
1703    cb = cleanDirty.cleanBox;
1704    if (cb.w > 0 && cb.h > 0) {
1705        // Request incremental for clean box
1706        arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h));
1707    }
1708    for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
1709        db = cleanDirty.dirtyBoxes[i];
1710        // Force all (non-incremental for dirty box
1711        arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h));
1712    }
1713    return arr;
1714};
1715
1716
1717
1718keyEvent = function(keysym, down) {
1719    //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down);
1720    var arr;
1721    arr = [4];  // msg-type
1722    arr.push8(down);
1723    arr.push16(0);
1724    arr.push32(keysym);
1725    //Util.Debug("<< keyEvent");
1726    return arr;
1727};
1728
1729pointerEvent = function(x, y) {
1730    //Util.Debug(">> pointerEvent, x,y: " + x + "," + y +
1731    //           " , mask: " + mouse_buttonMask);
1732    var arr;
1733    arr = [5];  // msg-type
1734    arr.push8(mouse_buttonMask);
1735    arr.push16(x);
1736    arr.push16(y);
1737    //Util.Debug("<< pointerEvent");
1738    return arr;
1739};
1740
1741clientCutText = function(text) {
1742    //Util.Debug(">> clientCutText");
1743    var arr, i, n;
1744    arr = [6];     // msg-type
1745    arr.push8(0);  // padding
1746    arr.push8(0);  // padding
1747    arr.push8(0);  // padding
1748    arr.push32(text.length);
1749    n = text.length;
1750    for (i=0; i < n; i+=1) {
1751        arr.push(text.charCodeAt(i));
1752    }
1753    //Util.Debug("<< clientCutText:" + arr);
1754    return arr;
1755};
1756
1757
1758
1759//
1760// Public API interface functions
1761//
1762
1763that.connect = function(host, port, password, path) {
1764    //Util.Debug(">> connect");
1765
1766    rfb_host       = host;
1767    rfb_port       = port;
1768    rfb_password   = (password !== undefined)   ? password : "";
1769    rfb_path       = (path !== undefined) ? path : "";
1770
1771    if ((!rfb_host) || (!rfb_port)) {
1772        return fail("Must set host and port");
1773    }
1774
1775    updateState('connect');
1776    //Util.Debug("<< connect");
1777
1778};
1779
1780that.disconnect = function() {
1781    //Util.Debug(">> disconnect");
1782    updateState('disconnect', 'Disconnecting');
1783    //Util.Debug("<< disconnect");
1784};
1785
1786that.sendPassword = function(passwd) {
1787    rfb_password = passwd;
1788    rfb_state = "Authentication";
1789    setTimeout(init_msg, 1);
1790};
1791
1792that.sendCtrlAltDel = function() {
1793    if (rfb_state !== "normal" || conf.view_only) { return false; }
1794    Util.Info("Sending Ctrl-Alt-Del");
1795    var arr = [];
1796    arr = arr.concat(keyEvent(0xFFE3, 1)); // Control
1797    arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt
1798    arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete
1799    arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete
1800    arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt
1801    arr = arr.concat(keyEvent(0xFFE3, 0)); // Control
1802    arr = arr.concat(fbUpdateRequests());
1803    ws.send(arr);
1804};
1805
1806// Send a key press. If 'down' is not specified then send a down key
1807// followed by an up key.
1808that.sendKey = function(code, down) {
1809    if (rfb_state !== "normal" || conf.view_only) { return false; }
1810    var arr = [];
1811    if (typeof down !== 'undefined') {
1812        Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
1813        arr = arr.concat(keyEvent(code, down ? 1 : 0));
1814    } else {
1815        Util.Info("Sending key code (down + up): " + code);
1816        arr = arr.concat(keyEvent(code, 1));
1817        arr = arr.concat(keyEvent(code, 0));
1818    }
1819    arr = arr.concat(fbUpdateRequests());
1820    ws.send(arr);
1821};
1822
1823that.clipboardPasteFrom = function(text) {
1824    if (rfb_state !== "normal") { return; }
1825    //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "...");
1826    ws.send(clientCutText(text));
1827    //Util.Debug("<< clipboardPasteFrom");
1828};
1829
1830// Override internal functions for testing
1831that.testMode = function(override_send) {
1832    test_mode = true;
1833    that.recv_message = ws.testMode(override_send);
1834
1835    checkEvents = function () { /* Stub Out */ };
1836    that.connect = function(host, port, password) {
1837            rfb_host = host;
1838            rfb_port = port;
1839            rfb_password = password;
1840            updateState('ProtocolVersion', "Starting VNC handshake");
1841        };
1842};
1843
1844
1845return constructor();  // Return the public API interface
1846
1847}  // End of RFB()
1848