/*
* noVNC: HTML5 VNC client
* Copyright (C) 2012 Joel Martin
* Copyright (C) 2013 Samuel Mannehed for Cendio AB
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
* TIGHT decoder portion:
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
*/
/*jslint white: false, browser: true */
/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */
var RFB;
(function () {
"use strict";
RFB = function (defaults) {
if (!defaults) {
defaults = {};
}
this._rfb_host = '';
this._rfb_port = 5900;
this._rfb_password = '';
this._rfb_path = '';
this._rfb_state = 'disconnected';
this._rfb_version = 0;
this._rfb_max_version = 3.8;
this._rfb_auth_scheme = '';
this._rfb_tightvnc = false;
this._rfb_atenikvm = false;
this._rfb_xvp_ver = 0;
// In preference order
this._encodings = [
['COPYRECT', 0x01 ],
['TIGHT', 0x07 ],
['TIGHT_PNG', -260 ],
['HEXTILE', 0x05 ],
['RRE', 0x02 ],
['RAW', 0x00 ],
['ATEN', 0x59 ],
['DesktopSize', -223 ],
['Cursor', -239 ],
// Psuedo-encoding settings
//['JPEG_quality_lo', -32 ],
['JPEG_quality_med', -26 ],
//['JPEG_quality_hi', -23 ],
//['compress_lo', -255 ],
['compress_hi', -247 ],
['last_rect', -224 ],
['xvp', -309 ]
];
this._encHandlers = {};
this._encNames = {};
this._encStats = {};
this._sock = null; // Websock object
this._display = null; // Display object
this._keyboard = null; // Keyboard input handler object
this._mouse = null; // Mouse input handler object
this._sendTimer = null; // Send Queue check timer
this._disconnTimer = null; // disconnection timer
this._msgTimer = null; // queued handle_msg timer
// Frame buffer update state
this._FBU = {
rects: 0,
subrects: 0, // RRE
lines: 0, // RAW
tiles: 0, // HEXTILE
aten_len: -1, // ATEN
aten_type: -1, // ATEN
bytes: 0,
x: 0,
y: 0,
width: 0,
height: 0,
encoding: 0,
subencoding: -1,
background: null,
zlib: [] // TIGHT zlib streams
};
this._pixelFormat = {};
this._fb_width = 0;
this._fb_height = 0;
this._fb_name = "";
this._rre_chunk_sz = 100;
this._timing = {
last_fbu: 0,
fbu_total: 0,
fbu_total_cnt: 0,
full_fbu_total: 0,
full_fbu_cnt: 0,
fbu_rt_start: 0,
fbu_rt_total: 0,
fbu_rt_cnt: 0,
pixels: 0
};
// Mouse state
this._mouse_buttonMask = 0;
this._mouse_arr = [];
this._viewportDragging = false;
this._viewportDragPos = {};
// set the default value on user-facing properties
Util.set_defaults(this, defaults, {
'target': 'null', // VNC display rendering Canvas object
'focusContainer': document, // DOM element that captures keyboard input
'encrypt': false, // Use TLS/SSL/wss encryption
'convertColor': false, // Client will not honor request for native color
'local_cursor': false, // Request locally rendered cursor
'shared': true, // Request shared mode
'view_only': false, // Disable client mouse/keyboard
'aten_password_sep': ':', // Separator for ATEN iKVM password fields
'xvp_password_sep': '@', // Separator for XVP password fields
'disconnectTimeout': 3, // Time (s) to wait for disconnection
'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection
'repeaterID': '', // [UltraVNC] RepeaterID to connect to
'viewportDrag': false, // Move the viewport on mouse drags
// Callback functions
'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change
'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required
'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received
'onBell': function () { }, // onBell(rfb): RFB Bell message received
'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed
'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized
'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received
'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection
});
// main setup
Util.Debug(">> RFB.constructor");
// populate encHandlers with bound versions
Object.keys(RFB.encodingHandlers).forEach(function (encName) {
this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this);
}.bind(this));
// Create lookup tables based on encoding number
for (var i = 0; i < this._encodings.length; i++) {
this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]];
this._encNames[this._encodings[i][1]] = this._encodings[i][0];
this._encStats[this._encodings[i][1]] = [0, 0];
}
try {
this._display = new Display({target: this._target});
} catch (exc) {
Util.Error("Display exception: " + exc);
this._updateState('fatal', "No working Display");
}
this._keyboard = new Keyboard({target: this._focusContainer,
onKeyPress: this._handleKeyPress.bind(this)});
this._mouse = new Mouse({target: this._target,
onMouseButton: this._handleMouseButton.bind(this),
onMouseMove: this._handleMouseMove.bind(this),
notify: this._keyboard.sync.bind(this._keyboard)});
this._sock = new Websock();
this._sock.on('message', this._handle_message.bind(this));
this._sock.on('open', function () {
if (this._rfb_state === 'connect') {
this._updateState('ProtocolVersion', "Starting VNC handshake");
} else {
this._fail("Got unexpected WebSocket connection");
}
}.bind(this));
this._sock.on('close', function (e) {
Util.Warn("WebSocket on-close event");
var msg = "";
if (e.code) {
msg = " (code: " + e.code;
if (e.reason) {
msg += ", reason: " + e.reason;
}
msg += ")";
}
if (this._rfb_state === 'disconnect') {
this._updateState('disconnected', 'VNC disconnected' + msg);
} else if (this._rfb_state === 'ProtocolVersion') {
this._fail('Failed to connect to server' + msg);
} else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) {
Util.Error("Received onclose while disconnected" + msg);
} else {
this._fail("Server disconnected" + msg);
}
this._sock.off('close');
}.bind(this));
this._sock.on('error', function (e) {
Util.Warn("WebSocket on-error event");
});
this._init_vars();
var rmode = this._display.get_render_mode();
if (Websock_native) {
Util.Info("Using native WebSockets");
this._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) {
this._updateState('fatal', "WebSockets or Adobe Flash is required");
} else if (document.location.href.substr(0, 7) === 'file://') {
this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash");
} else {
this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
}
}
Util.Debug("<< RFB.constructor");
};
RFB.prototype = {
// Public methods
connect: function (host, port, password, path) {
this._rfb_host = host;
this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : "";
this._rfb_path = (path !== undefined) ? path : "";
if (!this._rfb_host || !this._rfb_port) {
return this._fail("Must set host and port");
}
this._updateState('connect');
},
disconnect: function () {
this._updateState('disconnect', 'Disconnecting');
this._sock.off('error');
this._sock.off('message');
this._sock.off('open');
},
sendPassword: function (passwd) {
this._rfb_password = passwd;
this._rfb_state = 'Authentication';
setTimeout(this._init_msg.bind(this), 1);
},
sendCtrlAltDel: function () {
if (this._rfb_state !== 'normal' || this._view_only) { return false; }
Util.Info("Sending Ctrl-Alt-Del");
var arr = [];
arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1));
arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0));
arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0));
arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0));
this._sock.send(arr);
},
xvpOp: function (ver, op) {
if (this._rfb_xvp_ver < ver) { return false; }
Util.Info("Sending XVP operation " + op + " (version " + ver + ")");
this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op));
return true;
},
xvpShutdown: function () {
return this.xvpOp(1, 2);
},
xvpReboot: function () {
return this.xvpOp(1, 3);
},
xvpReset: function () {
return this.xvpOp(1, 4);
},
// Send a key press. If 'down' is not specified then send a down key
// followed by an up key.
sendKey: function (code, down) {
if (this._rfb_state !== "normal" || this._view_only) { return false; }
var arr = [];
if (typeof down !== 'undefined') {
Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0));
} else {
Util.Info("Sending key code (down + up): " + code);
arr = arr.concat(RFB.messages.keyEvent(code, 1));
arr = arr.concat(RFB.messages.keyEvent(code, 0));
}
this._sock.send(arr);
},
clipboardPasteFrom: function (text) {
if (this._rfb_state !== 'normal') { return; }
this._sock.send(RFB.messages.clientCutText(text));
},
// Private methods
_connect: function () {
Util.Debug(">> RFB.connect");
var uri;
if (typeof UsingSocketIO !== 'undefined') {
uri = 'http';
} else {
uri = this._encrypt ? 'wss' : 'ws';
}
uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path;
Util.Info("connecting to " + uri);
this._sock.open(uri, this._wsProtocols);
Util.Debug("<< RFB.connect");
},
_init_vars: function () {
// reset state
this._sock.init();
this._FBU.rects = 0;
this._FBU.subrects = 0; // RRE and HEXTILE
this._FBU.lines = 0; // RAW
this._FBU.tiles = 0; // HEXTILE
this._FBU.zlibs = []; // TIGHT zlib encoders
this._FBU.aten_len = -1 // ATEN
this._FBU.aten_type = -1; // ATEN
this._mouse_buttonMask = 0;
this._mouse_arr = [];
this._rfb_tightvnc = false;
this._rfb_atenikvm = false;
// Clear the per connection encoding stats
var i;
for (i = 0; i < this._encodings.length; i++) {
this._encStats[this._encodings[i][1]][0] = 0;
}
for (i = 0; i < 4; i++) {
this._FBU.zlibs[i] = new TINF();
this._FBU.zlibs[i].init();
}
},
_print_stats: function () {
Util.Info("Encoding stats for this connection:");
var i, s;
for (i = 0; i < this._encodings.length; i++) {
s = this._encStats[this._encodings[i][1]];
if (s[0] + s[1] > 0) {
Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects");
}
}
Util.Info("Encoding stats since page load:");
for (i = 0; i < this._encodings.length; i++) {
s = this._encStats[this._encodings[i][1]];
Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects");
}
},
/*
* 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 oldstate = this._rfb_state;
if (state === oldstate) {
// Already here, ignore
Util.Debug("Already in state '" + state + "', ignoring");
}
/*
* 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 (this._sendTimer) {
clearInterval(this._sendTimer);
this._sendTimer = null;
}
if (this._msgTimer) {
clearInterval(this._msgTimer);
this._msgTimer = null;
}
if (this._display && this._display.get_context()) {
this._keyboard.ungrab();
this._mouse.ungrab();
if (state !== 'connect' && state !== 'loaded') {
this._display.defaultCursor();
}
if (Util.get_logging() !== 'debug' || state === 'loaded') {
// Show noVNC logo on load and when disconnected, unless in
// debug mode
this._display.clear();
}
}
this._sock.close();
}
if (oldstate === 'fatal') {
Util.Error('Fatal error, cannot continue');
}
var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg;
if (state === 'failed' || state === 'fatal') {
Util.Error(cmsg);
} else {
Util.Warn(cmsg);
}
if (oldstate === 'failed' && state === 'disconnected') {
// do disconnect action, but stay in failed state
this._rfb_state = 'failed';
} else {
this._rfb_state = state;
}
if (this._disconnTimer && this._rfb_state !== 'disconnect') {
Util.Debug("Clearing disconnect timer");
clearTimeout(this._disconnTimer);
this._disconnTimer = null;
}
switch (state) {
case 'normal':
if (oldstate === 'disconnected' || oldstate === 'failed') {
Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
}
break;
case 'connect':
this._init_vars();
this._connect();
// WebSocket.onopen transitions to 'ProtocolVersion'
break;
case 'disconnect':
this._disconnTimer = setTimeout(function () {
this._fail("Disconnect timeout");
}.bind(this), this._disconnectTimeout * 1000);
this._print_stats();
// WebSocket.onclose transitions to 'disconnected'
break;
case 'failed':
if (oldstate === 'disconnected') {
Util.Error("Invalid transition from 'disconnected' to 'failed'");
} else if (oldstate === 'normal') {
Util.Error("Error while connected.");
} else if (oldstate === 'init') {
Util.Error("Error while initializing.");
}
// Make sure we transition to disconnected
setTimeout(function () {
this._updateState('disconnected');
}.bind(this), 50);
break;
default:
// No state change action to take
}
if (oldstate === 'failed' && state === 'disconnected') {
this._onUpdateState(this, state, oldstate);
} else {
this._onUpdateState(this, state, oldstate, statusMsg);
}
},
_fail: function (msg) {
this._updateState('failed', msg);
return false;
},
_handle_message: function () {
if (this._sock.rQlen() === 0) {
Util.Warn("handle_message called on an empty receive queue");
return;
}
switch (this._rfb_state) {
case 'disconnected':
case 'failed':
Util.Error("Got data while disconnected");
break;
case 'normal':
if (this._normal_msg() && this._sock.rQlen() > 0) {
// true means we can continue processing
// Give other events a chance to run
if (this._msgTimer === null) {
Util.Debug("More data to process, creating timer");
this._msgTimer = setTimeout(function () {
this._msgTimer = null;
this._handle_message();
}.bind(this), 10);
} else {
Util.Debug("More data to process, existing timer");
}
}
break;
default:
this._init_msg();
break;
}
},
_checkEvents: function () {
if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) {
this._sock.send(this._mouse_arr);
this._mouse_arr = [];
}
},
_handleKeyPress: function (keysym, down) {
if (this._view_only) { return; } // View only, skip keyboard, events
this._sock.send(RFB.messages.keyEvent(keysym, down));
},
_handleMouseButton: function (x, y, down, bmask) {
if (down) {
this._mouse_buttonMask |= bmask;
} else {
this._mouse_buttonMask ^= bmask;
}
if (this._viewportDrag) {
if (down && !this._viewportDragging) {
this._viewportDragging = true;
this._viewportDragPos = {'x': x, 'y': y};
// Skip sending mouse events
return;
} else {
this._viewportDragging = false;
}
}
if (this._view_only) { return; } // View only, skip mouse events
this._mouse_arr = this._mouse_arr.concat(
RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
this._sock.send(this._mouse_arr);
this._mouse_arr = [];
},
_handleMouseMove: function (x, y) {
if (this._viewportDragging) {
var deltaX = this._viewportDragPos.x - x;
var deltaY = this._viewportDragPos.y - y;
this._viewportDragPos = {'x': x, 'y': y};
this._display.viewportChange(deltaX, deltaY);
// Skip sending mouse events
return;
}
if (this._view_only) { return; } // View only, skip mouse events
this._mouse_arr = this._mouse_arr.concat(
RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
this._checkEvents();
},
// Message Handlers
_negotiate_protocol_version: function () {
if (this._sock.rQlen() < 12) {
return this._fail("Incomplete protocol version");
}
var sversion = this._sock.rQshiftStr(12).substr(4, 7);
Util.Info("Server ProtocolVersion: " + sversion);
var is_repeater = 0;
switch (sversion) {
case "000.000": // UltraVNC repeater
is_repeater = 1;
break;
case "003.003":
case "003.006": // UltraVNC
case "003.889": // Apple Remote Desktop
this._rfb_version = 3.3;
break;
case "003.007":
this._rfb_version = 3.7;
break;
case "003.008":
case "004.000": // Intel AMT KVM
case "004.001": // RealVNC 4.6
this._rfb_version = 3.8;
break;
default:
return this._fail("Invalid server version " + sversion);
}
if (is_repeater) {
var repeaterID = this._repeaterID;
while (repeaterID.length < 250) {
repeaterID += "\0";
}
this._sock.send_string(repeaterID);
return true;
}
if (this._rfb_version > this._rfb_max_version) {
this._rfb_version = this._rfb_max_version;
}
// Send updates either at a rate of 1 update per 50ms, or
// whatever slower rate the network can handle
this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50);
var cversion = "00" + parseInt(this._rfb_version, 10) +
".00" + ((this._rfb_version * 10) % 10);
this._sock.send_string("RFB " + cversion + "\n");
this._updateState('Security', 'Sent ProtocolVersion: ' + cversion);
},
_negotiate_security: function () {
if (this._rfb_version >= 3.7) {
// Server sends supported list, client decides
var num_types = this._sock.rQshift8();
if (this._sock.rQwait("security type", num_types, 1)) { return false; }
if (num_types === 0) {
var strlen = this._sock.rQshift32();
var reason = this._sock.rQshiftStr(strlen);
return this._fail("Security failure: " + reason);
}
this._rfb_auth_scheme = 0;
var types = this._sock.rQshiftBytes(num_types);
Util.Debug("Server security types: " + types);
for (var i = 0; i < types.length; i++) {
if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) {
this._rfb_auth_scheme = types[i];
}
}
if (this._rfb_auth_scheme === 0) {
return this._fail("Unsupported security types: " + types);
}
this._sock.send([this._rfb_auth_scheme]);
} else {
// Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; }
this._rfb_auth_scheme = this._sock.rQshift32();
}
this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme);
return this._init_msg(); // jump to authentication
},
// authentication
_negotiate_aten_auth: function () {
var aten_sep = this._aten_password_sep;
var aten_auth = this._rfb_password.split(aten_sep);
if (aten_auth.length < 2) {
this._updateState('password', 'ATEN iKVM credentials required (user' + aten_sep +
'password) -- got only ' + this._rfb_password);
this._onPasswordRequired(this);
return false;
}
this._rfb_atenikvm = true;
if (this._rfb_tightvnc) {
this._rfb_tightvnc = false;
} else {
this._sock.rQskipBytes(4);
}
this._sock.rQskipBytes(16);
var username = aten_auth[0];
username += Array(24-username.length+1).join("\x00");
var password = aten_auth.slice(1).join(aten_sep);
password += Array(24-password.length+1).join("\x00");
this._sock.send_string(username+password);
this._updateState("SecurityResult");
return true;
},
_negotiate_xvp_auth: function () {
var xvp_sep = this._xvp_password_sep;
var xvp_auth = this._rfb_password.split(xvp_sep);
if (xvp_auth.length < 3) {
this._updateState('password', 'XVP credentials required (user' + xvp_sep +
'target' + xvp_sep + 'password) -- got only ' + this._rfb_password);
this._onPasswordRequired(this);
return false;
}
var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) +
String.fromCharCode(xvp_auth[1].length) +
xvp_auth[0] +
xvp_auth[1];
this._sock.send_string(xvp_auth_str);
this._rfb_password = xvp_auth.slice(2).join(xvp_sep);
this._rfb_auth_scheme = 2;
return this._negotiate_authentication();
},
_negotiate_std_vnc_auth: function () {
if (this._rfb_password.length === 0) {
// Notify via both callbacks since it's kind of
// an RFB state change and a UI interface issue
this._updateState('password', "Password Required");
this._onPasswordRequired(this);
}
if (this._sock.rQwait("auth challenge", 16)) { return false; }
var challenge = this._sock.rQshiftBytes(16);
var response = RFB.genDES(this._rfb_password, challenge);
this._sock.send(response);
this._updateState("SecurityResult");
return true;
},
_negotiate_tight_tunnels: function (numTunnels) {
var clientSupportedTunnelTypes = {
0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
};
var serverSupportedTunnelTypes = {};
// receive tunnel capabilities
for (var i = 0; i < numTunnels; i++) {
var cap_code = this._sock.rQshift32();
var cap_vendor = this._sock.rQshiftStr(4);
var cap_signature = this._sock.rQshiftStr(8);
serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
}
// choose the notunnel type
if (serverSupportedTunnelTypes[0]) {
if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
return this._fail("Client's tunnel type had the incorrect vendor or signature");
}
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
return false; // wait until we receive the sub auth count to continue
} else {
return this._fail("Server wanted tunnels, but doesn't support the notunnel type");
}
},
_negotiate_tight_auth: function () {
if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation
if (this._sock.rQwait("num tunnels", 4)) { return false; }
var numTunnels = this._sock.rQshift32();
if (this._rfb_version === 3.8 && (numTunnels & 0xffff0ff0) >>> 0 === 0xaff90fb0) { return this._negotiate_aten_auth(); }
if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
this._rfb_tightvnc = true;
if (numTunnels > 0) {
this._negotiate_tight_tunnels(numTunnels);
return false; // wait until we receive the sub auth to continue
}
}
// second pass, do the sub-auth negotiation
if (this._sock.rQwait("sub auth count", 4)) { return false; }
var subAuthCount = this._sock.rQshift32();
if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
// Newer X10 Supermicro motherboards get here
if (this._rfb_version === 3.8 && numTunnels === 0 && subAuthCount === 0) {
Util.Warn("Newer ATEN iKVM detected, you may get an 'unsupported encoding 87'");
return this._negotiate_aten_auth();
}
var clientSupportedTypes = {
'STDVNOAUTH__': 1,
'STDVVNCAUTH_': 2
};
var serverSupportedTypes = [];
for (var i = 0; i < subAuthCount; i++) {
var capNum = this._sock.rQshift32();
var capabilities = this._sock.rQshiftStr(12);
serverSupportedTypes.push(capabilities);
}
for (var authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) {
this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
switch (authType) {
case 'STDVNOAUTH__': // no auth
this._updateState('SecurityResult');
return true;
case 'STDVVNCAUTH_': // VNC auth
this._rfb_auth_scheme = 2;
return this._init_msg();
default:
return this._fail("Unsupported tiny auth scheme: " + authType);
}
}
}
this._fail("No supported sub-auth types!");
},
_negotiate_authentication: function () {
switch (this._rfb_auth_scheme) {
case 0: // connection failed
if (this._sock.rQwait("auth reason", 4)) { return false; }
var strlen = this._sock.rQshift32();
var reason = this._sock.rQshiftStr(strlen);
return this._fail("Auth failure: " + reason);
case 1: // no auth
if (this._rfb_version >= 3.8) {
this._updateState('SecurityResult');
return true;
}
this._updateState('ClientInitialisation', "No auth required");
return this._init_msg();
case 22: // XVP auth
return this._negotiate_xvp_auth();
case 2: // VNC authentication
return this._negotiate_std_vnc_auth();
case 16: // TightVNC Security Type
return this._negotiate_tight_auth();
default:
return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme);
}
},
_handle_security_result: function () {
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
switch (this._sock.rQshift32()) {
case 0: // OK
this._updateState('ClientInitialisation', 'Authentication OK');
return this._init_msg();
case 1: // failed
if (this._rfb_version >= 3.8) {
var length = this._sock.rQshift32();
if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; }
var reason = this._sock.rQshiftStr(length);
return this._fail(reason);
} else {
return this._fail("Authentication failure");
}
return false;
case 2:
return this._fail("Too many auth attempts");
}
},
_negotiate_server_init: function () {
if (this._sock.rQwait("server initialization", 24)) { return false; }
if (this._rfb_atenikvm && this._sock.rQwait("ATEN server initialization", 36)) { return false; }
/* Screen size */
this._fb_width = this._sock.rQshift16();
this._fb_height = this._sock.rQshift16();
/* PIXEL_FORMAT */
this._pixelFormat.bpp = this._sock.rQshift8();
this._pixelFormat.depth = this._sock.rQshift8();
this._pixelFormat.big_endian = (this._sock.rQshift8() === 1) ? true : false;
this._pixelFormat.true_color = (this._sock.rQshift8() === 1) ? true : false;
this._pixelFormat.red_max = this._sock.rQshift16();
this._pixelFormat.green_max = this._sock.rQshift16();
this._pixelFormat.blue_max = this._sock.rQshift16();
this._pixelFormat.red_shift = this._sock.rQshift8();
this._pixelFormat.green_shift = this._sock.rQshift8();
this._pixelFormat.blue_shift = this._sock.rQshift8();
this._sock.rQskipBytes(3); // padding
// NB(directxman12): we don't want to call any callbacks or print messages until
// *after* we're past the point where we could backtrack
/* Connection name/title */
var name_length = this._sock.rQshift32();
if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length));
if (this._rfb_atenikvm) {
this._sock.rQskipBytes(8); // unknown
this._sock.rQskip8(); // IKVMVideoEnable
this._sock.rQskip8(); // IKVMKMEnable
this._sock.rQskip8(); // IKVMKickEnable
this._sock.rQskip8(); // VUSBEnable
}
if (this._rfb_tightvnc) {
if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
// In TightVNC mode, ServerInit message is extended
var numServerMessages = this._sock.rQshift16();
var numClientMessages = this._sock.rQshift16();
var numEncodings = this._sock.rQshift16();
this._sock.rQskipBytes(2); // padding
var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
var i;
for (i = 0; i < numServerMessages; i++) {
var srvMsg = this._sock.rQshiftStr(16);
}
for (i = 0; i < numClientMessages; i++) {
var clientMsg = this._sock.rQshiftStr(16);
}
for (i = 0; i < numEncodings; i++) {
var encoding = this._sock.rQshiftStr(16);
}
}
// NB(directxman12): these are down here so that we don't run them multiple times
// if we backtrack
Util.Info("Screen: " + this._fb_width + "x" + this._fb_height +
", bpp: " + this._pixelFormat.bpp + ", depth: " + this._pixelFormat.depth +
", big_endian: " + this._pixelFormat.big_endian +
", true_color: " + this._pixelFormat.true_color +
", red_max: " + this._pixelFormat.red_max +
", green_max: " + this._pixelFormat.green_max +
", blue_max: " + this._pixelFormat.blue_max +
", red_shift: " + this._pixelFormat.red_shift +
", green_shift: " + this._pixelFormat.green_shift +
", blue_shift: " + this._pixelFormat.blue_shift);
// we're past the point where we could backtrack, so it's safe to call this
this._onDesktopName(this, this._fb_name);
if (this._fb_name === "Intel(r) AMT KVM") {
Util.Warn("Intel AMT KVM only supports 8/16 bit depths, using server pixel format");
this._convertColor = true;
}
// ATEN 'wisdom' from chicken-aten-ikvm:lens/lens.rb
// tested against the following Supermicro motherboards
// (use 'dmidecode -s baseboard-product-name' for model):
// - X7SPA-F
// - X8DTL
// - X8SIE-F
// - X9SCL/X9SCM
// - X9SCM-F
// - X9DRD-iF
// - X9SRE/X9SRE-3F/X9SRi/X9SRi-3F
// - X9DRL-3F/X9DRL-6F
// - X10SLD
//
// Not supported (uses encoding 87):
// - X10SL7-F
// - X10SLD-F
// - X10SLM-F
// - X10SLE
//
// Simply does not work:
// Hermon (WPMC450) [hangs at login]:
// - X7SB3-F
// - X8DTU-F
// - X8STi-3F
// Peppercon (Raritan/Kira100) [connection refused]:
// - X7SBi
//
// Thanks to Brian Rak and Erik Smit for testing
if (this._rfb_atenikvm) {
// we do not know the resolution till the first fbupdate so go large
// although, not necessary, saves a pointless full screen refresh
this._fb_width = 10000;
this._fb_height = 10000;
// lies about what it supports
Util.Warn("ATEN iKVM lies and only does 15 bit depth with RGB555");
this._convertColor = true;
this._pixelFormat.bpp = 16;
this._pixelFormat.depth = 15;
this._pixelFormat.red_max = (1 << 5) - 1;
this._pixelFormat.green_max = (1 << 5) - 1;
this._pixelFormat.blue_max = (1 << 5) - 1;
this._pixelFormat.red_shift = 10;
this._pixelFormat.green_shift = 5;
this._pixelFormat.blue_shift = 0;
// changes to protocol format
RFB.messages.keyEvent = function (keysym, down) {
if (XK2HID[keysym] === undefined) {
Util.Warn("XK2HID["+keysym+"] returns undefined");
return;
}
var arr = [4];
arr.push8(0);
arr.push8(down);
arr.push16(0);
arr.push32(XK2HID[keysym]);
arr.push32(0);
arr.push32(0);
arr.push8(0);
return arr;
};
RFB.messages.pointerEvent = function (x, y, mask) {
var arr = [5];
arr.push8(0);
arr.push8(mask);
arr.push16(x);
arr.push16(y);
arr.push32(0);
arr.push32(0);
arr.push16(0);
arr.push8(0);
return arr;
};
}
if (this._convertColor)
this._display.set_true_color(this._pixelFormat.true_color);
this._onFBResize(this, this._fb_width, this._fb_height);
this._display.resize(this._fb_width, this._fb_height);
this._keyboard.grab();
this._mouse.grab();
var response = [];
// only send if not native, and we think the server will honor the conversion
if (!this._convertColor) {
if (this._pixelFormat.big_endian !== false
|| this._pixelFormat.red_max !== 255
|| this._pixelFormat.green_max !== 255
|| this._pixelFormat.blue_max !== 255
|| this._pixelFormat.red_shift !== 16
|| this._pixelFormat.green_shift !== 8
|| this._pixelFormat.blue_shift !== 0
|| !( this._pixelFormat.bpp === 32
&& this._pixelFormat.depth === 24
&& this._pixelFormat.true_color === true )
|| !( this._pixelFormat.bpp === 8
&& this._pixelFormat.depth === 8
&& this._pixelFormat.true_color === false )) {
this._pixelFormat.big_endian = false;
this._pixelFormat.red_max = 255;
this._pixelFormat.green_max = 255;
this._pixelFormat.blue_max = 255;
this._pixelFormat.red_shift = 16;
this._pixelFormat.green_shift = 8;
this._pixelFormat.blue_shift = 0;
if (this._pixelFormat.true_color) {
this._pixelFormat.bpp = 32;
this._pixelFormat.depth = 24;
} else {
this._pixelFormat.bpp = 8;
this._pixelFormat.depth = 8;
}
response = response.concat(RFB.messages.pixelFormat(this._pixelFormat));
} else {
Util.Warn("Server pixel format matches our preferred native, disabling color conversion");
this._convertColor = false;
}
}
this._pixelFormat.Bpp = this._pixelFormat.bpp / 8;
this._pixelFormat.Bdepth = Math.ceil(this._pixelFormat.depth / 8);
if (this._pixelFormat.bpp < this._pixelFormat.depth) {
return this._fail('server claims greater depth than bpp');
}
if (this._pixelFormat.true_color
&& this._pixelFormat.depth >
Math.ceil(Math.log(this._pixelFormat.red_max)/Math.LN2)
+ Math.ceil(Math.log(this._pixelFormat.green_max)/Math.LN2)
+ Math.ceil(Math.log(this._pixelFormat.blue_max)/Math.LN2)) {
return this._fail('server claims greater depth than sum of RGB maximums');
}
response = response.concat(RFB.messages.clientEncodings(
this._encodings, this._local_cursor,
this._pixelFormat.true_color));
response = response.concat(RFB.messages.fbUpdateRequests(
this._display.getCleanDirtyReset(),
this._fb_width, this._fb_height));
this._timing.fbu_rt_start = (new Date()).getTime();
this._timing.pixels = 0;
this._sock.send(response);
this._checkEvents();
if (this._encrypt) {
this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name);
} else {
this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name);
}
},
_init_msg: function () {
switch (this._rfb_state) {
case 'ProtocolVersion':
return this._negotiate_protocol_version();
case 'Security':
return this._negotiate_security();
case 'Authentication':
return this._negotiate_authentication();
case 'SecurityResult':
return this._handle_security_result();
case 'ClientInitialisation':
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
this._updateState('ServerInitialisation', "Authentication OK");
return true;
case 'ServerInitialisation':
return this._negotiate_server_init();
}
},
_handle_set_colour_map_msg: function () {
Util.Debug("SetColorMapEntries");
this._sock.rQskip8(); // Padding
var first_colour = this._sock.rQshift16();
var num_colours = this._sock.rQshift16();
if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; }
for (var c = 0; c < num_colours; c++) {
var red = parseInt(this._sock.rQshift16() / 256, 10);
var green = parseInt(this._sock.rQshift16() / 256, 10);
var blue = parseInt(this._sock.rQshift16() / 256, 10);
this._display.set_colourMap([blue, green, red], first_colour + c);
}
Util.Debug("colourMap: " + this._display.get_colourMap());
Util.Info("Registered " + num_colours + " colourMap entries");
return true;
},
_handle_server_cut_text: function () {
Util.Debug("ServerCutText");
if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
this._sock.rQskipBytes(3); // Padding
var length = this._sock.rQshift32();
if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
var text = this._sock.rQshiftStr(length);
this._onClipboard(this, text);
return true;
},
_handle_xvp_msg: function () {
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
this._sock.rQskip8(); // Padding
var xvp_ver = this._sock.rQshift8();
var xvp_msg = this._sock.rQshift8();
switch (xvp_msg) {
case 0: // XVP_FAIL
this._updateState(this._rfb_state, "Operation Failed");
break;
case 1: // XVP_INIT
this._rfb_xvp_ver = xvp_ver;
Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
this._onXvpInit(this._rfb_xvp_ver);
break;
default:
this._fail("Disconnected: illegal server XVP message " + xvp_msg);
break;
}
return true;
},
_normal_msg: function () {
var msg_type;
if (this._FBU.rects > 0) {
msg_type = 0;
} else {
msg_type = this._sock.rQshift8();
}
if (this._rfb_atenikvm) {
switch (msg_type) {
case 4: // Front Ground Event
Util.Debug("ATEN iKVM Front Ground Event");
this._sock.rQskipBytes(20);
return true;
case 22: // Keep Alive Event
Util.Debug("ATEN iKVM Keep Alive Event");
this._sock.rQskipBytes(1);
return true;
case 51: // Video Get Info
Util.Debug("ATEN iKVM Video Get Info");
this._sock.rQskipBytes(4);
return true;
case 55: // Mouse Get Info
Util.Debug("ATEN iKVM Mouse Get Info");
this._sock.rQskipBytes(2);
return true;
case 57: // Session Message
Util.Debug("ATEN iKVM Session Message");
this._sock.rQskipBytes(4); // u32
this._sock.rQskipBytes(4); // u32
this._sock.rQskipBytes(256);
return true;
case 60: // Get Viewer Lang
Util.Debug("ATEN iKVM Get Viewer Lang");
this._sock.rQskipBytes(8);
return true;
}
}
switch (msg_type) {
case 0: // FramebufferUpdate
var ret = this._framebufferUpdate();
if (ret) {
this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(),
this._fb_width, this._fb_height));
}
return ret;
case 1: // SetColorMapEntries
return this._handle_set_colour_map_msg();
case 2: // Bell
Util.Debug("Bell");
this._onBell(this);
return true;
case 3: // ServerCutText
return this._handle_server_cut_text();
case 250: // XVP
return this._handle_xvp_msg();
default:
this._fail("Disconnected: illegal server message type " + msg_type);
Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
return true;
}
},
_framebufferUpdate: function () {
var ret = true;
var now;
if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
this._sock.rQskip8(); // Padding
this._FBU.rects = this._sock.rQshift16();
this._FBU.bytes = 0;
this._timing.cur_fbu = 0;
if (this._timing.fbu_rt_start > 0) {
now = (new Date()).getTime();
Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start));
}
}
while (this._FBU.rects > 0) {
if (this._rfb_state !== "normal") { return false; }
if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; }
if (this._FBU.bytes === 0) {
if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */
var hdr = this._sock.rQshiftBytes(12);
this._FBU.x = (hdr[0] << 8) + hdr[1];
this._FBU.y = (hdr[2] << 8) + hdr[3];
this._FBU.width = (hdr[4] << 8) + hdr[5];
this._FBU.height = (hdr[6] << 8) + hdr[7];
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
(hdr[10] << 8) + hdr[11], 10);
this._onFBUReceive(this,
{'x': this._FBU.x, 'y': this._FBU.y,
'width': this._FBU.width, 'height': this._FBU.height,
'encoding': this._FBU.encoding,
'encodingName': this._encNames[this._FBU.encoding]});
if (!this._encNames[this._FBU.encoding]) {
this._fail("Disconnected: unsupported encoding " +
this._FBU.encoding);
return false;
}
// ATEN uses 0x00 even when it is meant to be 0x59
if (this._rfb_atenikvm && this._FBU.encoding === 0x00) {
this._FBU.encoding = 0x59;
}
}
this._timing.last_fbu = (new Date()).getTime();
ret = this._encHandlers[this._FBU.encoding]();
now = (new Date()).getTime();
this._timing.cur_fbu += (now - this._timing.last_fbu);
if (ret) {
this._encStats[this._FBU.encoding][0]++;
this._encStats[this._FBU.encoding][1]++;
this._timing.pixels += this._FBU.width * this._FBU.height;
}
if (this._timing.pixels >= (this._fb_width * this._fb_height)) {
if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) ||
this._timing.fbu_rt_start > 0) {
this._timing.full_fbu_total += this._timing.cur_fbu;
this._timing.full_fbu_cnt++;
Util.Info("Timing of full FBU, curr: " +
this._timing.cur_fbu + ", total: " +
this._timing.full_fbu_total + ", cnt: " +
this._timing.full_fbu_cnt + ", avg: " +
(this._timing.full_fbu_total / this._timing.full_fbu_cnt));
}
if (this._timing.fbu_rt_start > 0) {
var fbu_rt_diff = now - this._timing.fbu_rt_start;
this._timing.fbu_rt_total += fbu_rt_diff;
this._timing.fbu_rt_cnt++;
Util.Info("full FBU round-trip, cur: " +
fbu_rt_diff + ", total: " +
this._timing.fbu_rt_total + ", cnt: " +
this._timing.fbu_rt_cnt + ", avg: " +
(this._timing.fbu_rt_total / this._timing.fbu_rt_cnt));
this._timing.fbu_rt_start = 0;
}
}
if (!ret) { return ret; } // need more data
}
this._onFBUComplete(this,
{'x': this._FBU.x, 'y': this._FBU.y,
'width': this._FBU.width, 'height': this._FBU.height,
'encoding': this._FBU.encoding,
'encodingName': this._encNames[this._FBU.encoding]});
return true; // We finished this FBU
},
// input: byte stream in pixel format
// output: RGB
_convert_color: function (arr, Bpp) {
if (Bpp === undefined)
Bpp = this._pixelFormat.Bpp;
if (!this._convertColor
// HACK? Xtightvnc needs this and I have no idea why
|| (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) {
switch (Bpp) {
case 4:
for (var i = arr.length; i > 0; i -= 4) {
arr.splice(i - 1, 1); // convert from RGBX to RGB
}
case 3:
return arr;
default:
Util.Error('convert color disabled, but Bpp is not 3 or 4!');
}
}
var data = [];
for (var i = 0; i < arr.length; i += Bpp) {
var pix = 0;
for (var j = 0; j < Bpp; j++) {
if (this._pixelFormat.big_endian) {
pix = (pix << 8) | arr[i + j];
} else {
pix = (arr[i + j] << (j*8)) | pix;
}
}
data.push(((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max)
* (256/(this._pixelFormat.red_max + 1)));
data.push(((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max)
* (256/(this._pixelFormat.green_max + 1)));
data.push(((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max)
* (256/(this._pixelFormat.blue_max + 1)));
}
return data;
},
};
Util.make_properties(RFB, [
['target', 'wo', 'dom'], // VNC display rendering Canvas object
['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input
['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption
['convertColor', 'rw', 'bool'], // Client will not honor request for native color
['local_cursor', 'rw', 'bool'], // Request locally rendered cursor
['shared', 'rw', 'bool'], // Request shared mode
['view_only', 'rw', 'bool'], // Disable client mouse/keyboard
['aten_password_sep', 'rw', 'str'], // Separator for ATEN iKVM password fields
['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields
['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection
['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection
['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to
['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags
// Callback functions
['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change
['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required
['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received
['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received
['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed
['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized
['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received
['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection
]);
RFB.prototype.set_local_cursor = function (cursor) {
if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) {
this._local_cursor = false;
this._display.disableLocalCursor(); //Only show server-side cursor
} else {
if (this._display.get_cursor_uri()) {
this._local_cursor = true;
} else {
Util.Warn("Browser does not support local cursor");
}
}
};
RFB.prototype.get_display = function () { return this._display; };
RFB.prototype.get_keyboard = function () { return this._keyboard; };
RFB.prototype.get_mouse = function () { return this._mouse; };
// Class Methods
RFB.messages = {
keyEvent: function (keysym, down) {
var arr = [4];
arr.push8(down);
arr.push16(0);
arr.push32(keysym);
return arr;
},
pointerEvent: function (x, y, mask) {
var arr = [5]; // msg-type
arr.push8(mask);
arr.push16(x);
arr.push16(y);
return arr;
},
// TODO(directxman12): make this unicode compatible?
clientCutText: function (text) {
var arr = [6]; // msg-type
arr.push8(0); // padding
arr.push8(0); // padding
arr.push8(0); // padding
arr.push32(text.length);
var n = text.length;
for (var i = 0; i < n; i++) {
arr.push(text.charCodeAt(i));
}
return arr;
},
pixelFormat: function (pf) {
var arr = [0]; // msg-type
arr.push8(0); // padding
arr.push8(0); // padding
arr.push8(0); // padding
arr.push8(pf.bpp);
arr.push8(pf.depth);
arr.push8(pf.big_endian ? 1 : 0);
arr.push8(pf.true_color ? 1 : 0);
arr.push16(pf.red_max);
arr.push16(pf.green_max);
arr.push16(pf.blue_max);
arr.push8(pf.red_shift);
arr.push8(pf.green_shift);
arr.push8(pf.blue_shift);
arr.push8(0); // padding
arr.push8(0); // padding
arr.push8(0); // padding
return arr;
},
clientEncodings: function (encodings, local_cursor, true_color) {
var i, encList = [];
for (i = 0; i < encodings.length; i++) {
if (encodings[i][0] === "Cursor" && !local_cursor) {
Util.Debug("Skipping Cursor pseudo-encoding");
} else if (encodings[i][0] === "TIGHT" && !true_color) {
// TODO: remove this when we have tight+non-true-color
Util.Warn("Skipping tight as it is only supported with true color");
} else {
encList.push(encodings[i][1]);
}
}
var arr = [2]; // msg-type
arr.push8(0); // padding
arr.push16(encList.length); // encoding count
for (i = 0; i < encList.length; i++) {
arr.push32(encList[i]);
}
return arr;
},
fbUpdateRequests: function (cleanDirty, fb_width, fb_height) {
var arr = [];
var cb = cleanDirty.cleanBox;
var w, h;
if (cb.w > 0 && cb.h > 0) {
w = typeof cb.w === "undefined" ? fb_width : cb.w;
h = typeof cb.h === "undefined" ? fb_height : cb.h;
// Request incremental for clean box
arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h));
}
for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
var db = cleanDirty.dirtyBoxes[i];
// Force all (non-incremental) for dirty box
w = typeof db.w === "undefined" ? fb_width : db.w;
h = typeof db.h === "undefined" ? fb_height : db.h;
arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h));
}
return arr;
},
fbUpdateRequest: function (incremental, x, y, w, h) {
if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; }
var arr = [3]; // msg-type
arr.push8(incremental);
arr.push16(x);
arr.push16(y);
arr.push16(w);
arr.push16(h);
return arr;
}
};
RFB.genDES = function (password, challenge) {
var passwd = [];
for (var i = 0; i < password.length; i++) {
passwd.push(password.charCodeAt(i));
}
return (new DES(passwd)).encrypt(challenge);
};
RFB.extract_data_uri = function (arr) {
return ";base64," + Base64.encode(arr);
};
RFB.encodingHandlers = {
RAW: function () {
if (this._FBU.lines === 0) {
this._FBU.lines = this._FBU.height;
}
this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // at least a line
if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; }
var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines);
var curr_height = Math.min(this._FBU.lines,
Math.floor(this._sock.rQlen() / (this._FBU.width * this._pixelFormat.Bpp)));
this._display.renderQ_push({
'type': 'blit',
'data': this._convert_color(this._sock.rQshiftBytes(curr_height * this._FBU.width * this._pixelFormat.Bpp)),
'x': this._FBU.x,
'y': cur_y,
'width': this._FBU.width,
'height': curr_height
});
this._FBU.lines -= curr_height;
if (this._FBU.lines > 0) {
this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // At least another line
} else {
this._FBU.rects--;
this._FBU.bytes = 0;
}
return true;
},
COPYRECT: function () {
this._FBU.bytes = 4;
if (this._sock.rQwait("COPYRECT", 4)) { return false; }
this._display.renderQ_push({
'type': 'copy',
'old_x': this._sock.rQshift16(),
'old_y': this._sock.rQshift16(),
'x': this._FBU.x,
'y': this._FBU.y,
'width': this._FBU.width,
'height': this._FBU.height
});
this._FBU.rects--;
this._FBU.bytes = 0;
return true;
},
RRE: function () {
var color;
if (this._FBU.subrects === 0) {
this._FBU.bytes = 4 + this._pixelFormat.Bpp;
if (this._sock.rQwait("RRE", 4 + this._pixelFormat.Bpp)) { return false; }
this._FBU.subrects = this._sock.rQshift32();
color = this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)).reverse(); // Background
this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color);
}
while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._pixelFormat.Bpp + 8)) {
color = this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)).reverse();
var x = this._sock.rQshift16();
var y = this._sock.rQshift16();
var width = this._sock.rQshift16();
var height = this._sock.rQshift16();
this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color);
this._FBU.subrects--;
}
if (this._FBU.subrects > 0) {
var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects);
this._FBU.bytes = (this._pixelFormat.Bpp + 8) * chunk;
} else {
this._FBU.rects--;
this._FBU.bytes = 0;
}
return true;
},
HEXTILE: function () {
var rQ = this._sock.get_rQ();
var rQi = this._sock.get_rQi();
if (this._FBU.tiles === 0) {
this._FBU.tiles_x = Math.ceil(this._FBU.width / 16);
this._FBU.tiles_y = Math.ceil(this._FBU.height / 16);
this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y;
this._FBU.tiles = this._FBU.total_tiles;
}
while (this._FBU.tiles > 0) {
this._FBU.bytes = 1;
if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
var subencoding = rQ[rQi]; // Peek
if (subencoding > 30) { // Raw
this._fail("Disconnected: illegal hextile subencoding " + subencoding);
return false;
}
var subrects = 0;
var curr_tile = this._FBU.total_tiles - this._FBU.tiles;
var tile_x = curr_tile % this._FBU.tiles_x;
var tile_y = Math.floor(curr_tile / this._FBU.tiles_x);
var x = this._FBU.x + tile_x * 16;
var y = this._FBU.y + tile_y * 16;
var w = Math.min(16, (this._FBU.x + this._FBU.width) - x);
var h = Math.min(16, (this._FBU.y + this._FBU.height) - y);
// Figure out how much we are expecting
if (subencoding & 0x01) { // Raw
this._FBU.bytes += w * h * this._pixelFormat.Bpp;
} else {
if (subencoding & 0x02) { // Background
this._FBU.bytes += this._pixelFormat.Bpp;
}
if (subencoding & 0x04) { // Foreground
this._FBU.bytes += this._pixelFormat.Bpp;
}
if (subencoding & 0x08) { // AnySubrects
this._FBU.bytes++; // Since we aren't shifting it off
if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; }
subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek
if (subencoding & 0x10) { // SubrectsColoured
this._FBU.bytes += subrects * (this._pixelFormat.Bpp + 2);
} else {
this._FBU.bytes += subrects * 2;
}
}
}
if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; }
// We know the encoding and have a whole tile
this._FBU.subencoding = rQ[rQi];
rQi++;
if (this._FBU.subencoding === 0) {
if (this._FBU.lastsubencoding & 0x01) {
// Weird: ignore blanks are RAW
Util.Debug(" Ignoring blank after RAW");
} else {
this._display.fillRect(x, y, w, h, this._FBU.background);
}
} else if (this._FBU.subencoding & 0x01) { // Raw
this._display.renderQ_push({
'type': 'blit',
'data': this._convert_color(rQ.slice(rQi, rQi + this._FBU.bytes - 1)),
'x': x,
'y': y,
'width': w,
'height': h
});
rQi += this._FBU.bytes - 1;
} else {
if (this._FBU.subencoding & 0x02) { // Background
this._FBU.background = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse();
rQi += this._pixelFormat.Bpp;
}
if (this._FBU.subencoding & 0x04) { // Foreground
this._FBU.foreground = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse();
rQi += this._pixelFormat.Bpp;
}
this._display.startTile(x, y, w, h, this._FBU.background);
if (this._FBU.subencoding & 0x08) { // AnySubrects
subrects = rQ[rQi];
rQi++;
for (var s = 0; s < subrects; s++) {
var color;
if (this._FBU.subencoding & 0x10) { // SubrectsColoured
color = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse();
rQi += this._pixelFormat.Bpp;
} else {
color = this._FBU.foreground;
}
var xy = rQ[rQi];
rQi++;
var sx = (xy >> 4);
var sy = (xy & 0x0f);
var wh = rQ[rQi];
rQi++;
var sw = (wh >> 4) + 1;
var sh = (wh & 0x0f) + 1;
this._display.subTile(sx, sy, sw, sh, color);
}
}
this._display.finishTile();
}
this._sock.set_rQi(rQi);
this._FBU.lastsubencoding = this._FBU.subencoding;
this._FBU.bytes = 0;
this._FBU.tiles--;
}
if (this._FBU.tiles === 0) {
this._FBU.rects--;
}
return true;
},
getTightCLength: function (arr) {
var header = 1, data = 0;
data += arr[0] & 0x7f;
if (arr[0] & 0x80) {
header++;
data += (arr[1] & 0x7f) << 7;
if (arr[1] & 0x80) {
header++;
data += arr[2] << 14;
}
}
return [header, data];
},
display_tight: function (isTightPNG) {
if (this._pixelFormat.Bdepth === 1) {
this._fail("Tight protocol handler only implements true color mode");
}
this._FBU.bytes = 1; // compression-control byte
if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; }
var checksum = function (data) {
var sum = 0;
for (var i = 0; i < data.length; i++) {
sum += data[i];
if (sum > 65536) sum -= 65536;
}
return sum;
};
var resetStreams = 0;
var streamId = -1;
var decompress = function (data) {
for (var i = 0; i < 4; i++) {
if ((resetStreams >> i) & 1) {
this._FBU.zlibs[i].reset();
Util.Info("Reset zlib stream " + i);
}
}
var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0);
if (uncompressed.status !== 0) {
Util.Error("Invalid data in zlib stream");
}
return uncompressed.data;
}.bind(this);
var indexedToRGB = function (data, numColors, palette, width, height) {
// Convert indexed (palette based) image data to RGB
// TODO: reduce number of calculations inside loop
var dest = [];
var x, y, dp, sp;
if (numColors === 2) {
var w = Math.floor((width + 7) / 8);
var w1 = Math.floor(width / 8);
for (y = 0; y < height; y++) {
var b;
for (x = 0; x < w1; x++) {
for (b = 7; b >= 0; b--) {
dp = (y * width + x * 8 + 7 - b) * 3;
sp = (data[y * w + x] >> b & 1) * 3;
dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
}
}
for (b = 7; b >= 8 - width % 8; b--) {
dp = (y * width + x * 8 + 7 - b) * 3;
sp = (data[y * w + x] >> b & 1) * 3;
dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
}
}
} else {
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
dp = (y * width + x) * 3;
sp = data[y * width + x] * 3;
dest[dp] = palette[sp];
dest[dp + 1] = palette[sp + 1];
dest[dp + 2] = palette[sp + 2];
}
}
}
return dest;
}.bind(this);
var rQ = this._sock.get_rQ();
var rQi = this._sock.get_rQi();
var cmode, clength, data;
var handlePalette = function () {
var numColors = rQ[rQi + 2] + 1;
var paletteSize = numColors * this._pixelFormat.Bdepth;
this._FBU.bytes += paletteSize;
if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; }
var bpp = (numColors <= 2) ? 1 : 8;
var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8);
var raw = false;
if (rowSize * this._FBU.height < 12) {
raw = true;
clength = [0, rowSize * this._FBU.height];
} else {
clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize,
3 + paletteSize + 3));
}
this._FBU.bytes += clength[0] + clength[1];
if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
// Shift ctl, filter id, num colors, palette entries, and clength off
this._sock.rQskipBytes(3);
var palette = this._convert_color(this._sock.rQshiftBytes(paletteSize), this._pixelFormat.Bdepth);
this._sock.rQskipBytes(clength[0]);
if (raw) {
data = this._sock.rQshiftBytes(clength[1]);
} else {
data = decompress(this._sock.rQshiftBytes(clength[1]));
}
// Convert indexed (palette based) image data to RGB
var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height);
this._display.renderQ_push({
'type': 'blit',
'data': rgb,
'x': this._FBU.x,
'y': this._FBU.y,
'width': this._FBU.width,
'height': this._FBU.height
});
return true;
}.bind(this);
var handleCopy = function () {
var raw = false;
var uncompressedSize = this._FBU.width * this._FBU.height * this._pixelFormat.Bdepth;
if (uncompressedSize < 12) {
raw = true;
clength = [0, uncompressedSize];
} else {
clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
}
this._FBU.bytes = 1 + clength[0] + clength[1];
if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
// Shift ctl, clength off
this._sock.rQshiftBytes(1 + clength[0]);
if (raw) {
data = this._sock.rQshiftBytes(clength[1]);
} else {
data = decompress(this._sock.rQshiftBytes(clength[1]));
}
this._display.renderQ_push({
'type': 'blit',
'data': this._convert_color(data, this._pixelFormat.Bdepth),
'x': this._FBU.x,
'y': this._FBU.y,
'width': this._FBU.width,
'height': this._FBU.height
});
return true;
}.bind(this);
var ctl = this._sock.rQpeek8();
// Keep tight reset bits
resetStreams = ctl & 0xF;
// Figure out filter
ctl = ctl >> 4;
streamId = ctl & 0x3;
if (ctl === 0x08) cmode = "fill";
else if (ctl === 0x09) cmode = "jpeg";
else if (ctl === 0x0A) cmode = "png";
else if (ctl & 0x04) cmode = "filter";
else if (ctl < 0x04) cmode = "copy";
else return this._fail("Illegal tight compression received, ctl: " + ctl);
if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
return this._fail("filter/copy received in tightPNG mode");
}
switch (cmode) {
// fill use fb_depth because TPIXELs drop the padding byte
case "fill": // TPIXEL
this._FBU.bytes += this._pixelFormat.Bdepth;
break;
case "jpeg": // max clength
this._FBU.bytes += 3;
break;
case "png": // max clength
this._FBU.bytes += 3;
break;
case "filter": // filter id + num colors if palette
this._FBU.bytes += 2;
break;
case "copy":
break;
}
if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
// Determine FBU.bytes
switch (cmode) {
case "fill":
this._sock.rQskip8(); // shift off ctl
this._display.renderQ_push({
'type': 'fill',
'x': this._FBU.x,
'y': this._FBU.y,
'width': this._FBU.width,
'height': this._FBU.height,
'color': this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bdepth), this._pixelFormat.Bdepth).reverse(),
});
break;
case "png":
case "jpeg":
clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
// We have everything, render it
this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length
var img = new Image();
img.src = "data: image/" + cmode +
RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1]));
this._display.renderQ_push({
'type': 'img',
'img': img,
'x': this._FBU.x,
'y': this._FBU.y
});
img = null;
break;
case "filter":
var filterId = rQ[rQi + 1];
if (filterId === 1) {
if (!handlePalette()) { return false; }
} else {
// Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
// Filter 2, Gradient is valid but not use if jpeg is enabled
// TODO(directxman12): why aren't we just calling '_fail' here
throw new Error("Unsupported tight subencoding received, filter: " + filterId);
}
break;
case "copy":
if (!handleCopy()) { return false; }
break;
}
this._FBU.bytes = 0;
this._FBU.rects--;
return true;
},
TIGHT: function () { return this._encHandlers.display_tight(false); },
TIGHT_PNG: function () { return this._encHandlers.display_tight(true); },
last_rect: function () {
this._FBU.rects = 0;
return true;
},
DesktopSize: function () {
Util.Debug(">> set_desktopsize");
this._fb_width = this._FBU.width;
this._fb_height = this._FBU.height;
this._onFBResize(this, this._fb_width, this._fb_height);
this._display.resize(this._fb_width, this._fb_height);
this._timing.fbu_rt_start = (new Date()).getTime();
this._FBU.bytes = 0;
this._FBU.rects--;
Util.Debug("<< set_desktopsize");
return true;
},
Cursor: function () {
Util.Debug(">> set_cursor");
var x = this._FBU.x; // hotspot-x
var y = this._FBU.y; // hotspot-y
var w = this._FBU.width;
var h = this._FBU.height;
var pixelslength = w * h * this._pixelFormat.Bpp;
var masklength = Math.floor((w + 7) / 8) * h;
this._FBU.bytes = pixelslength + masklength;
if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; }
this._display.changeCursor(this._sock.rQshiftBytes(pixelslength),
this._sock.rQshiftBytes(masklength),
x, y, w, h);
this._FBU.bytes = 0;
this._FBU.rects--;
Util.Debug("<< set_cursor");
return true;
},
JPEG_quality_lo: function () {
Util.Error("Server sent jpeg_quality pseudo-encoding");
},
compress_lo: function () {
Util.Error("Server sent compress level pseudo-encoding");
},
ATEN: function () {
if (this._FBU.aten_len === - 1) {
this._FBU.bytes = 8;
if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; }
this._FBU.bytes = 0;
this._sock.rQskipBytes(4);
this._FBU.aten_len = this._sock.rQshift32();
if (this._FBU.width === 64896 && this._FBU.height === 65056) {
Util.Info("ATEN iKVM screen is probably off");
if (this._FBU.aten_len !== 10) {
Util.Debug(">> ATEN iKVM screen off (aten_len="+this._FBU.aten_len+")");
this._fail('expected aten_len to be 10 when screen is off');
}
this._FBU.aten_len -= 10;
return true;
}
if (this._fb_width !== this._FBU.width && this._fb_height !== this._FBU.height) {
Util.Debug(">> ATEN resize desktop");
this._fb_width = this._FBU.width;
this._fb_height = this._FBU.height;
this._onFBResize(this, this._fb_width, this._fb_height);
this._display.resize(this._fb_width, this._fb_height);
Util.Debug("<< ATEN resize desktop");
}
}
if (this._FBU.aten_type === -1) {
this._FBU.bytes = 10;
if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; }
this._FBU.bytes = 0;
this._FBU.aten_type = this._sock.rQshift8();
this._sock.rQskip8();
this._sock.rQshift32(); // number of subrects
if (this._FBU.aten_len !== this._sock.rQshift32()) {
return this._fail('ATEN RAW len mis-match');
}
this._FBU.aten_len -= 10;
}
while (this._FBU.aten_len > 0) {
switch (this._FBU.aten_type) {
case 0: // Subrects
this._FBU.bytes = 6 + (16 * 16 * this._pixelFormat.Bpp); // at least a subrect
if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; }
var a = this._sock.rQshift16();
var b = this._sock.rQshift16();
var y = this._sock.rQshift8();
var x = this._sock.rQshift8();
this._display.renderQ_push({
'type': 'blit',
'data': this._convert_color(this._sock.rQshiftBytes(this._FBU.bytes - 6)),
'x': x * 16,
'y': y * 16,
'width': 16,
'height': 16
});
this._FBU.aten_len -= this._FBU.bytes;
this._FBU.bytes = 0;
break;
case 1: // RAW
var olines = (this._FBU.lines === 0) ? this._FBU.height : this._FBU.lines;
this._encHandlers.RAW();
this._FBU.aten_len -= (olines - this._FBU.lines) * this._FBU.width * this._pixelFormat.Bpp;
if (this._FBU.bytes > 0) return false;
break;
default:
return this._fail('unknown ATEN type: '+this._FBU.aten_type);
}
}
if (this._FBU.aten_len < 0) {
this._fail('aten_len dropped below zero');
}
if (this._FBU.aten_type === 0) {
this._FBU.rects--;
}
this._FBU.aten_len = -1;
this._FBU.aten_type = -1;
return true;
}
};
})();