Files
noVNC/include/rfb.js
Joel Martin e79917c3db Merge remote branch 'origin/issue-70'
Conflicts:
	include/display.js
	include/rfb.js

This merges in the fix for https://github.com/kanaka/noVNC/issues/70

This changes noVNC to use the preferred color ordering that most VNC
server prefer and that VMWare VNC requires. It's possible this may
break some VNC servers out there in which case we might have to do
something a bit more subtle such as having alternate render functions
for little and big endian color ordering.
2012-01-12 17:17:11 -06:00

1645 lines
52 KiB
JavaScript

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