From 8db09746b7b96da6bf48d0d7ef22f40df7205f7d Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Mon, 2 Aug 2010 17:07:27 -0500 Subject: [PATCH] New API. Refactor Canvas and RFB objects. New API: To use the RFB object, you now must instantiate it (this allows more than one instance of it on the same page). rfb = new RFB(settings); The 'settings' variable is a namespace that contains initial default settings. These can also be set and read using 'rfb.set_FOO()' and 'rfb.get_FOO()' where FOO is the setting name. The current settings are (and defaults) are: - target: the DOM Canvas element to use ('VNC_canvas'). - encrypt: whether to encrypt the connection (false) - true_color: true_color or palette (true) - b64encode: base64 encode the WebSockets data (true) - local_cursor: use local cursor rendering (true if supported) - connectTimeout: milliseconds to wait for connect (2000) - updateState: callback when RFB state changes (none) - clipboardReceive: callback when clipboard data received (none) The parameters to the updateState callback have also changed. The function spec is now updateState(rfb, state, oldstate, msg): - rfb: the RFB object that this state change is for. - state: the new state - oldstate: the previous state - msg: a message associate with the state (not always set). The clipboardReceive spec is clipboardReceive(rfb, text): - rfb: the RFB object that this text is from. - text: the clipboard text received. Changes: - The RFB and Canvas namespaces are now more proper objects. Private implementation is no longer exposed and the public API has been made explicit. Also, instantiation allows more than one VNC connection on the same page (to complete this, DefaultControls will also need this same refactoring). - Added 'none' logging level. - Removed automatic stylesheet selection workaround in util.js and move it to defaultcontrols so that it doesn't interfere with intergration. - Also, some major JSLinting. - Fix input, canvas, and cursor tests to work with new model. --- README.md | 13 +- include/canvas.js | 923 +++++++++--------- include/default_controls.js | 78 +- include/rfb.js | 1774 ++++++++++++++++++----------------- include/util.js | 97 +- tests/canvas.html | 56 +- tests/cursor.html | 6 +- tests/input.html | 7 +- vnc.html | 10 +- vnc_auto.html | 53 +- 10 files changed, 1569 insertions(+), 1448 deletions(-) diff --git a/README.md b/README.md index 1ffbd12..4426c89 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ The client is designed to be easily integrated with existing web structure and style. At a minimum you must include the `vnc.js` and `default_controls.js` -scripts and call their load() functions. For example: +scripts and call DefaultControls.load(). For example: @@ -203,12 +203,13 @@ scripts and call their load() functions. For example:
Loading
+ + - See `vnc.html` and `vnc_auto.html` for examples. The file `include/plain.css` has a list of stylable elements. diff --git a/include/canvas.js b/include/canvas.js index b0f7b64..7b02b3e 100644 --- a/include/canvas.js +++ b/include/canvas.js @@ -7,183 +7,142 @@ */ "use strict"; -/*jslint white: false, bitwise: false */ -/*global window, $, Util, Base64 */ +/*jslint browser: true, white: false, bitwise: false */ +/*global window, Util, Base64 */ -// Globals defined here -var Canvas; +function Canvas(conf) { -// Everything namespaced inside Canvas -Canvas = { +conf = conf || {}; // Configuration +var that = {}, // Public API interface -prefer_js : false, // make private -force_canvas : false, // make private -cursor_uri : true, // make private + // Pre-declare functions used before definitions (jslint)jslint + setFillColor, fillRect, -true_color : true, -colourMap : [], + // Private Canvas namespace variables + c_forceCanvas = false, -scale : 1, -c_wx : 0, -c_wy : 0, -ctx : null, + c_width = 0, + c_height = 0, -prevStyle : "", + c_prevStyle = "", -focused : true, -keyPress : null, -mouseButton : null, -mouseMove : null, - -onMouseButton: function(e, down) { - var evt, pos, bmask; - if (! Canvas.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, $(Canvas.id), Canvas.scale); - bmask = 1 << evt.button; - //Util.Debug('mouse ' + pos.x + "," + pos.y + " down: " + down + " bmask: " + bmask); - if (Canvas.mouseButton) { - Canvas.mouseButton(pos.x, pos.y, down, bmask); - } - Util.stopEvent(e); - return false; -}, - -onMouseDown: function (e) { - Canvas.onMouseButton(e, 1); -}, - -onMouseUp: function (e) { - Canvas.onMouseButton(e, 0); -}, - -onMouseWheel: function (e) { - var evt, pos, bmask, wheelData; - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, $(Canvas.id), Canvas.scale); - wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; - if (wheelData > 0) { - bmask = 1 << 3; - } else { - bmask = 1 << 4; - } - //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); - if (Canvas.mouseButton) { - Canvas.mouseButton(pos.x, pos.y, 1, bmask); - Canvas.mouseButton(pos.x, pos.y, 0, bmask); - } - Util.stopEvent(e); - return false; -}, + c_keyPress = null, + c_mouseButton = null, + c_mouseMove = null; -onMouseMove: function (e) { - var evt, pos; - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, $(Canvas.id), Canvas.scale); - //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); - if (Canvas.mouseMove) { - Canvas.mouseMove(pos.x, pos.y); - } -}, +// Capability settings, default can be overridden +Util.conf_default(conf, that, 'prefer_js', null); +Util.conf_default(conf, that, 'cursor_uri', null); -onKeyDown: function (e) { - //Util.Debug("keydown: " + Canvas.getKeysym(e)); - if (! Canvas.focused) { - return true; - } - if (Canvas.keyPress) { - Canvas.keyPress(Canvas.getKeysym(e), 1); - } - Util.stopEvent(e); - return false; -}, +// Configuration settings +Util.conf_default(conf, that, 'target', null); +Util.conf_default(conf, that, 'true_color', true); +Util.conf_default(conf, that, 'focused', true); +Util.conf_default(conf, that, 'colourMap', []); +Util.conf_default(conf, that, 'scale', 1); -onKeyUp : function (e) { - //Util.Debug("keyup: " + Canvas.getKeysym(e)); - if (! Canvas.focused) { - return true; - } - if (Canvas.keyPress) { - Canvas.keyPress(Canvas.getKeysym(e), 0); - } - Util.stopEvent(e); - return false; -}, - -onMouseDisable: function (e) { - var evt, pos; - if (! Canvas.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getPosition($(Canvas.id)); - /* Stop propagation if inside canvas area */ - if ((evt.clientX >= pos.x) && - (evt.clientY >= pos.y) && - (evt.clientX < (pos.x + Canvas.c_wx)) && - (evt.clientY < (pos.y + Canvas.c_wy))) { - //Util.Debug("mouse event disabled"); - Util.stopEvent(e); +// Override some specific getters/setters +that.set_prefer_js = function(val) { + if (val && c_forceCanvas) { + Util.Warn("Preferring Javascript to Canvas ops is not supported"); return false; } - //Util.Debug("mouse event not disabled"); + conf.prefer_js = val; return true; -}, +}; + +that.get_colourMap = function(idx) { + if (typeof idx === 'undefined') { + return conf.colourMap; + } else { + return conf.colourMap[idx]; + } +}; + +that.set_colourMap = function(val, idx) { + if (typeof idx === 'undefined') { + conf.colourMap = val; + } else { + conf.colourMap[idx] = val; + } +}; + +// Add some other getters/setters +that.get_width = function() { + return c_width; +}; +that.get_height = function() { + return c_height; +}; -init: function (id) { - var c, imgTest, tval, i, curDat, curSave; + +// +// Private functions +// + +// Create the public API interface +function constructor() { Util.Debug(">> Canvas.init"); - Canvas.id = id; - c = $(Canvas.id); + var c, ctx, imgTest, tval, i, curDat, curSave, + has_imageData = false; - if (! c.getContext) { throw("No getContext method"); } - Canvas.ctx = c.getContext('2d'); + if (! conf.target) { throw("target must be set"); } - Canvas.clear(); + if (typeof conf.target === 'string') { + conf.target = window.$(conf.target); + } + + c = conf.target; + + if (! c.getContext) { throw("no getContext method"); } + + if (! conf.ctx) { conf.ctx = c.getContext('2d'); } + ctx = conf.ctx; + + that.clear(); /* * Determine browser Canvas feature support * and select fastest rendering methods */ tval = 0; - Canvas.has_imageData = false; try { - imgTest = Canvas.ctx.getImageData(0, 0, 1,1); + imgTest = ctx.getImageData(0, 0, 1,1); imgTest.data[0] = 123; imgTest.data[3] = 255; - Canvas.ctx.putImageData(imgTest, 0, 0); - tval = Canvas.ctx.getImageData(0, 0, 1, 1).data[0]; + ctx.putImageData(imgTest, 0, 0); + tval = ctx.getImageData(0, 0, 1, 1).data[0]; if (tval === 123) { - Canvas.has_imageData = true; + has_imageData = true; } - } catch (exc) {} + } catch (exc1) {} - if (Canvas.has_imageData) { + if (has_imageData) { Util.Info("Canvas supports imageData"); - Canvas.force_canvas = false; - if (Canvas.ctx.createImageData) { + c_forceCanvas = false; + if (ctx.createImageData) { // If it's there, it's faster Util.Info("Using Canvas createImageData"); - Canvas._imageData = Canvas._imageDataCreate; - } else if (Canvas.ctx.getImageData) { + that.imageData = that.imageDataCreate; + } else if (ctx.getImageData) { Util.Info("Using Canvas getImageData"); - Canvas._imageData = Canvas._imageDataGet; + that.imageData = that.imageDataGet; } Util.Info("Prefering javascript operations"); - Canvas.prefer_js = true; - Canvas._rgbxImage = Canvas._rgbxImageData; - Canvas._cmapImage = Canvas._cmapImageData; + if (conf.prefer_js === null) { + conf.prefer_js = true; + } + that.rgbxImage = that.rgbxImageData; + that.cmapImage = that.cmapImageData; } else { Util.Warn("Canvas lacks imageData, using fillRect (slow)"); - Canvas.force_canvas = true; - Canvas.prefer_js = false; - Canvas._rgbxImage = Canvas._rgbxImageFill; - Canvas._cmapImage = Canvas._cmapImageFill; + c_forceCanvas = true; + conf.prefer_js = false; + that.rgbxImage = that.rgbxImageFill; + that.cmapImage = that.cmapImageFill; } /* @@ -191,300 +150,37 @@ init: function (id) { * scheme */ curDat = []; - for (i=0; i < 8 * 8 * 4; i++) { + for (i=0; i < 8 * 8 * 4; i += 1) { curDat.push(255); } try { curSave = c.style.cursor; - Canvas.changeCursor(curDat, curDat, 2, 2, 8, 8); + that.changeCursor(curDat, curDat, 2, 2, 8, 8); if (c.style.cursor) { + if (conf.cursor_uri === null) { + conf.cursor_uri = true; + } Util.Info("Data URI scheme cursor supported"); } else { - Canvas.cursor_uri = false; + if (conf.cursor_uri === null) { + conf.cursor_uri = false; + } Util.Warn("Data URI scheme cursor not supported"); } c.style.cursor = curSave; - } catch (exc2) { + } catch (exc2) { Util.Error("Data URI scheme cursor test exception: " + exc2); conf.cursor_uri = false; } - Canvas.colourMap = []; - Canvas.prevStyle = ""; - Canvas.focused = true; + conf.focused = true; Util.Debug("<< Canvas.init"); - return true; -}, - - -start: function (keyPress, mouseButton, mouseMove) { - var c; - Util.Debug(">> Canvas.start"); - - c = $(Canvas.id); - Canvas.keyPress = keyPress || null; - Canvas.mouseButton = mouseButton || null; - Canvas.mouseMove = mouseMove || null; - - Util.addEvent(document, 'keydown', Canvas.onKeyDown); - Util.addEvent(document, 'keyup', Canvas.onKeyUp); - Util.addEvent(c, 'mousedown', Canvas.onMouseDown); - Util.addEvent(c, 'mouseup', Canvas.onMouseUp); - Util.addEvent(c, 'mousemove', Canvas.onMouseMove); - Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - Canvas.onMouseWheel); - - /* Work around right and middle click browser behaviors */ - Util.addEvent(document, 'click', Canvas.onMouseDisable); - Util.addEvent(document.body, 'contextmenu', Canvas.onMouseDisable); - - Util.Debug("<< Canvas.start"); -}, - -clear: function () { - Canvas.resize(640, 20); - Canvas.ctx.clearRect(0, 0, Canvas.c_wx, Canvas.c_wy); -}, - -resize: function (width, height, true_color) { - var c = $(Canvas.id); - - if (typeof true_color !== "undefined") { - Canvas.true_color = true_color; - } - - c.width = width; - c.height = height; - - Canvas.c_wx = c.offsetWidth; - Canvas.c_wy = c.offsetHeight; - - //Canvas.rescale(Canvas.scale); -}, - -rescale: function (factor) { - var c, tp, x, y, - properties = ['transform', 'WebkitTransform', 'MozTransform', null]; - c = $(Canvas.id); - while (tp = properties.shift()) { - if (typeof c.style[tp] != 'undefined') { - break; - } - } - - if (tp === null) { - Util.Debug("No scaling support"); - return; - } - - if (Canvas.scale === factor) { - //Util.Debug("Canvas already scaled to '" + factor + "'"); - return; - } - - Canvas.scale = factor; - x = c.width - c.width * factor; - y = c.height - c.height * factor; - c.style[tp] = "scale(" + Canvas.scale + ") translate(-" + x + "px, -" + y + "px)"; -}, - -stop: function () { - var c = $(Canvas.id); - Util.removeEvent(document, 'keydown', Canvas.onKeyDown); - Util.removeEvent(document, 'keyup', Canvas.onKeyUp); - Util.removeEvent(c, 'mousedown', Canvas.onMouseDown); - Util.removeEvent(c, 'mouseup', Canvas.onMouseUp); - Util.removeEvent(c, 'mousemove', Canvas.onMouseMove); - Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - Canvas.onMouseWheel); - - /* Work around right and middle click browser behaviors */ - Util.removeEvent(document, 'click', Canvas.onMouseDisable); - Util.removeEvent(document.body, 'contextmenu', Canvas.onMouseDisable); - - // Turn off cursor rendering - if (Canvas.cursor_uri) { - c.style.cursor = "default"; - } -}, - -/* - * Tile rendering functions optimized for rendering engines. - * - * - In Chrome/webkit, Javascript image data array manipulations are - * faster than direct Canvas fillStyle, fillRect rendering. In - * gecko, Javascript array handling is much slower. - */ -getTile: function(x, y, width, height, color) { - var img, data, p, rgb, red, green, blue, j, i; - img = {'x': x, 'y': y, 'width': width, 'height': height, - 'data': []}; - if (Canvas.prefer_js) { - data = img.data; - if (Canvas.true_color) { - rgb = color; - } else { - rgb = Canvas.colourMap[color[0]]; - } - red = rgb[0]; - green = rgb[1]; - blue = rgb[2]; - for (j = 0; j < height; j += 1) { - for (i = 0; i < width; i += 1) { - p = (i + (j * width) ) * 4; - data[p + 0] = red; - data[p + 1] = green; - data[p + 2] = blue; - //data[p + 3] = 255; // Set Alpha - } - } - } else { - Canvas.fillRect(x, y, width, height, color); - } - return img; -}, - -setSubTile: function(img, x, y, w, h, color) { - var data, p, rgb, red, green, blue, width, j, i; - if (Canvas.prefer_js) { - data = img.data; - width = img.width; - if (Canvas.true_color) { - rgb = color; - } else { - rgb = Canvas.colourMap[color[0]]; - } - red = rgb[0]; - green = rgb[1]; - blue = rgb[2]; - for (j = 0; j < h; j += 1) { - for (i = 0; i < w; i += 1) { - p = (x + i + ((y + j) * width) ) * 4; - data[p + 0] = red; - data[p + 1] = green; - data[p + 2] = blue; - //img.data[p + 3] = 255; // Set Alpha - } - } - } else { - Canvas.fillRect(img.x + x, img.y + y, w, h, color); - } -}, - -putTile: function(img) { - if (Canvas.prefer_js) { - Canvas._rgbxImage(img.x, img.y, img.width, img.height, img.data, 0); - } else { - // No-op, under gecko already done by setSubTile - } -}, - -_imageDataGet: function(width, height) { - return Canvas.ctx.getImageData(0, 0, width, height); -}, -_imageDataCreate: function(width, height) { - return Canvas.ctx.createImageData(width, height); -}, -_imageDataRaw: function(width, height) { - return {'data': [], 'width': width, 'height': height}; -}, - -_rgbxImageData: function(x, y, width, height, arr, offset) { - var img, i, j, data; - img = Canvas._imageData(width, height); - data = img.data; - for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { - data[i + 0] = arr[j + 0]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j + 2]; - data[i + 3] = 255; // Set Alpha - } - Canvas.ctx.putImageData(img, x, y); -}, - -// really slow fallback if we don't have imageData -_rgbxImageFill: function(x, y, width, height, arr, offset) { - var i, j, sx = 0, sy = 0; - for (i=0, j=offset; i < (width * height); i+=1, j+=4) { - Canvas.fillRect(x+sx, y+sy, 1, 1, [arr[j+0], arr[j+1], arr[j+2]]); - sx += 1; - if ((sx % width) === 0) { - sx = 0; - sy += 1; - } - } -}, - -_cmapImageData: function(x, y, width, height, arr, offset) { - var img, i, j, data, rgb, cmap; - img = Canvas._imageData(width, height); - data = img.data; - cmap = Canvas.colourMap; - for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { - rgb = cmap[arr[j]]; - data[i + 0] = rgb[0]; - data[i + 1] = rgb[1]; - data[i + 2] = rgb[2]; - data[i + 3] = 255; // Set Alpha - } - Canvas.ctx.putImageData(img, x, y); -}, - -_cmapImageFill: function(x, y, width, height, arr, offset) { - var i, j, sx = 0, sy = 0, cmap; - cmap = Canvas.colourMap; - for (i=0, j=offset; i < (width * height); i+=1, j+=1) { - Canvas.fillRect(x+sx, y+sy, 1, 1, [arr[j]]); - sx += 1; - if ((sx % width) === 0) { - sx = 0; - sy += 1; - } - } -}, - - -blitImage: function(x, y, width, height, arr, offset) { - if (Canvas.true_color) { - Canvas._rgbxImage(x, y, width, height, arr, offset); - } else { - Canvas._cmapImage(x, y, width, height, arr, offset); - } -}, - -blitStringImage: function(str, x, y) { - var img = new Image(); - img.onload = function () { Canvas.ctx.drawImage(img, x, y); }; - img.src = str; -}, - -setFillColor: function(color) { - var rgb, newStyle; - if (Canvas.true_color) { - rgb = color; - } else { - rgb = Canvas.colourMap[color[0]]; - } - if (newStyle !== Canvas.prevStyle) { - newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; - Canvas.ctx.fillStyle = newStyle; - Canvas.prevStyle = newStyle; - } -}, - -fillRect: function(x, y, width, height, color) { - Canvas.setFillColor(color); - Canvas.ctx.fillRect(x, y, width, height); -}, - -copyImage: function(old_x, old_y, new_x, new_y, width, height) { - Canvas.ctx.drawImage($(Canvas.id), old_x, old_y, width, height, - new_x, new_y, width, height); -}, + return that ; +} /* Translate DOM key event to keysym value */ -getKeysym: function(e) { +function getKeysym(e) { var evt, keysym; evt = (e ? e : window.event); @@ -574,22 +270,389 @@ getKeysym: function(e) { } return keysym; -}, +} + +function onMouseButton(e, down) { + var evt, pos, bmask; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + bmask = 1 << evt.button; + //Util.Debug('mouse ' + pos.x + "," + pos.y + " down: " + down + " bmask: " + bmask); + if (c_mouseButton) { + c_mouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; +} + +function onMouseDown(e) { + onMouseButton(e, 1); +} + +function onMouseUp(e) { + onMouseButton(e, 0); +} + +function onMouseWheel(e) { + var evt, pos, bmask, wheelData; + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } + //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); + if (c_mouseButton) { + c_mouseButton(pos.x, pos.y, 1, bmask); + c_mouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; +} + +function onMouseMove(e) { + var evt, pos; + evt = (e ? e : window.event); + pos = Util.getEventPosition(e, conf.target, conf.scale); + //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); + if (c_mouseMove) { + c_mouseMove(pos.x, pos.y); + } +} + +function onKeyDown(e) { + //Util.Debug("keydown: " + getKeysym(e)); + if (! conf.focused) { + return true; + } + if (c_keyPress) { + c_keyPress(getKeysym(e), 1); + } + Util.stopEvent(e); + return false; +} + +function onKeyUp(e) { + //Util.Debug("keyup: " + getKeysym(e)); + if (! conf.focused) { + return true; + } + if (c_keyPress) { + c_keyPress(getKeysym(e), 0); + } + Util.stopEvent(e); + return false; +} + +function onMouseDisable(e) { + var evt, pos; + if (! conf.focused) { + return true; + } + evt = (e ? e : window.event); + pos = Util.getPosition(conf.target); + /* Stop propagation if inside canvas area */ + if ((evt.clientX >= pos.x) && + (evt.clientY >= pos.y) && + (evt.clientX < (pos.x + c_width)) && + (evt.clientY < (pos.y + c_height))) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } + //Util.Debug("mouse event not disabled"); + return true; +} + +// +// Public API interface functions +// + +that.getContext = function () { + return conf.ctx; +}; + +that.start = function(keyPressFunc, mouseButtonFunc, mouseMoveFunc) { + var c; + Util.Debug(">> Canvas.start"); + + c = conf.target; + c_keyPress = keyPressFunc || null; + c_mouseButton = mouseButtonFunc || null; + c_mouseMove = mouseMoveFunc || null; + + Util.addEvent(document, 'keydown', onKeyDown); + Util.addEvent(document, 'keyup', onKeyUp); + Util.addEvent(c, 'mousedown', onMouseDown); + Util.addEvent(c, 'mouseup', onMouseUp); + Util.addEvent(c, 'mousemove', onMouseMove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + onMouseWheel); + + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', onMouseDisable); + Util.addEvent(document.body, 'contextmenu', onMouseDisable); + + Util.Debug("<< Canvas.start"); +}; + +that.rescale = function(factor) { + var c, tp, x, y, + properties = ['transform', 'WebkitTransform', 'MozTransform', null]; + c = conf.target; + tp = properties.shift(); + while (tp) { + if (typeof c.style[tp] !== 'undefined') { + break; + } + tp = properties.shift(); + } + + if (tp === null) { + Util.Debug("No scaling support"); + return; + } + + if (conf.scale === factor) { + //Util.Debug("Canvas already scaled to '" + factor + "'"); + return; + } + + conf.scale = factor; + x = c.width - c.width * factor; + y = c.height - c.height * factor; + c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)"; +}; + +that.resize = function(width, height, true_color) { + var c = conf.target; + + if (typeof true_color !== "undefined") { + conf.true_color = true_color; + } + + c.width = width; + c.height = height; + + c_width = c.offsetWidth; + c_height = c.offsetHeight; + + that.rescale(conf.scale); +}; + +that.clear = function() { + that.resize(640, 20); + conf.ctx.clearRect(0, 0, c_width, c_height); +}; + +that.stop = function() { + var c = conf.target; + Util.removeEvent(document, 'keydown', onKeyDown); + Util.removeEvent(document, 'keyup', onKeyUp); + Util.removeEvent(c, 'mousedown', onMouseDown); + Util.removeEvent(c, 'mouseup', onMouseUp); + Util.removeEvent(c, 'mousemove', onMouseMove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + onMouseWheel); + + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', onMouseDisable); + Util.removeEvent(document.body, 'contextmenu', onMouseDisable); + + // Turn off cursor rendering + if (conf.cursor_uri) { + c.style.cursor = "default"; + } +}; + +setFillColor = function(color) { + var rgb, newStyle; + if (conf.true_color) { + rgb = color; + } else { + rgb = conf.colourMap[color[0]]; + } + if (newStyle !== c_prevStyle) { + newStyle = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; + conf.ctx.fillStyle = newStyle; + c_prevStyle = newStyle; + } +}; +that.setFillColor = setFillColor; + +fillRect = function(x, y, width, height, color) { + setFillColor(color); + conf.ctx.fillRect(x, y, width, height); +}; +that.fillRect = fillRect; + +that.copyImage = function(old_x, old_y, new_x, new_y, width, height) { + conf.ctx.drawImage(conf.target, old_x, old_y, width, height, + new_x, new_y, width, height); +}; + +/* + * Tile rendering functions optimized for rendering engines. + * + * - In Chrome/webkit, Javascript image data array manipulations are + * faster than direct Canvas fillStyle, fillRect rendering. In + * gecko, Javascript array handling is much slower. + */ +that.getTile = function(x, y, width, height, color) { + var img, data, p, rgb, red, green, blue, j, i; + img = {'x': x, 'y': y, 'width': width, 'height': height, + 'data': []}; + if (conf.prefer_js) { + data = img.data; + if (conf.true_color) { + rgb = color; + } else { + rgb = conf.colourMap[color[0]]; + } + red = rgb[0]; + green = rgb[1]; + blue = rgb[2]; + for (j = 0; j < height; j += 1) { + for (i = 0; i < width; i += 1) { + p = (i + (j * width) ) * 4; + data[p + 0] = red; + data[p + 1] = green; + data[p + 2] = blue; + //data[p + 3] = 255; // Set Alpha + } + } + } else { + fillRect(x, y, width, height, color); + } + return img; +}; + +that.setSubTile = function(img, x, y, w, h, color) { + var data, p, rgb, red, green, blue, width, j, i; + if (conf.prefer_js) { + data = img.data; + width = img.width; + if (conf.true_color) { + rgb = color; + } else { + rgb = conf.colourMap[color[0]]; + } + red = rgb[0]; + green = rgb[1]; + blue = rgb[2]; + for (j = 0; j < h; j += 1) { + for (i = 0; i < w; i += 1) { + p = (x + i + ((y + j) * width) ) * 4; + data[p + 0] = red; + data[p + 1] = green; + data[p + 2] = blue; + //img.data[p + 3] = 255; // Set Alpha + } + } + } else { + fillRect(img.x + x, img.y + y, w, h, color); + } +}; + +that.putTile = function(img) { + if (conf.prefer_js) { + that.rgbxImage(img.x, img.y, img.width, img.height, img.data, 0); + } else { + // No-op, under gecko already done by setSubTile + } +}; + +that.imageDataGet = function(width, height) { + return conf.ctx.getImageData(0, 0, width, height); +}; +that.imageDataCreate = function(width, height) { + return conf.ctx.createImageData(width, height); +}; + +that.rgbxImageData = function(x, y, width, height, arr, offset) { + var img, i, j, data; + img = that.imageData(width, height); + data = img.data; + for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { + data[i + 0] = arr[j + 0]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Set Alpha + } + conf.ctx.putImageData(img, x, y); +}; + +// really slow fallback if we don't have imageData +that.rgbxImageFill = function(x, y, width, height, arr, offset) { + var i, j, sx = 0, sy = 0; + for (i=0, j=offset; i < (width * height); i+=1, j+=4) { + fillRect(x+sx, y+sy, 1, 1, [arr[j+0], arr[j+1], arr[j+2]]); + sx += 1; + if ((sx % width) === 0) { + sx = 0; + sy += 1; + } + } +}; + +that.cmapImageData = function(x, y, width, height, arr, offset) { + var img, i, j, data, rgb, cmap; + img = that.imageData(width, height); + data = img.data; + cmap = conf.colourMap; + for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { + rgb = cmap[arr[j]]; + data[i + 0] = rgb[0]; + data[i + 1] = rgb[1]; + data[i + 2] = rgb[2]; + data[i + 3] = 255; // Set Alpha + } + conf.ctx.putImageData(img, x, y); +}; + +that.cmapImageFill = function(x, y, width, height, arr, offset) { + var i, j, sx = 0, sy = 0, cmap; + cmap = conf.colourMap; + for (i=0, j=offset; i < (width * height); i+=1, j+=1) { + fillRect(x+sx, y+sy, 1, 1, [arr[j]]); + sx += 1; + if ((sx % width) === 0) { + sx = 0; + sy += 1; + } + } +}; -isCursor: function() { - return Canvas.cursor_uri; -}, -changeCursor: function(pixels, mask, hotx, hoty, w, h) { +that.blitImage = function(x, y, width, height, arr, offset) { + if (conf.true_color) { + that.rgbxImage(x, y, width, height, arr, offset); + } else { + that.cmapImage(x, y, width, height, arr, offset); + } +}; + +that.blitStringImage = function(str, x, y) { + var img = new Image(); + img.onload = function () { conf.ctx.drawImage(img, x, y); }; + img.src = str; +}; + +that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { var cur = [], cmap, rgb, IHDRsz, ANDsz, XORsz, url, idx, alpha, x, y; //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w: " + w + ", h: " + h); - if (!Canvas.cursor_uri) { + if (conf.cursor_uri === false) { Util.Warn("changeCursor called but no cursor data URI support"); return; } - cmap = Canvas.colourMap; + cmap = conf.colourMap; IHDRsz = 40; ANDsz = w * h * 4; XORsz = Math.ceil( (w * h) / 8.0 ); @@ -623,12 +686,12 @@ changeCursor: function(pixels, mask, hotx, hoty, w, h) { cur.push32le(0); // XOR/color data - for (y = h-1; y >= 0; y--) { - for (x = 0; x < w; x++) { + for (y = h-1; y >= 0; y -= 1) { + for (x = 0; x < w; x += 1) { idx = y * Math.ceil(w / 8) + Math.floor(x/8); alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - if (Canvas.true_color) { + if (conf.true_color) { idx = ((w * y) + x) * 4; cur.push(pixels[idx + 2]); // blue cur.push(pixels[idx + 1]); // green @@ -646,16 +709,20 @@ changeCursor: function(pixels, mask, hotx, hoty, w, h) { } // AND/bitmask data (ignored, just needs to be right size) - for (y = 0; y < h; y++) { - for (x = 0; x < Math.ceil(w / 8); x++) { + for (y = 0; y < h; y += 1) { + for (x = 0; x < Math.ceil(w / 8); x += 1) { cur.push(0x00); } } url = "data:image/x-icon;base64," + Base64.encode(cur); - $(Canvas.id).style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; + conf.target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; //Util.Debug("<< changeCursor, cur.length: " + cur.length); -} - }; + + +return constructor(); // Return the public API interface + +} // End of Canvas() + diff --git a/include/default_controls.js b/include/default_controls.js index dd2e97d..3be50ed 100644 --- a/include/default_controls.js +++ b/include/default_controls.js @@ -6,6 +6,7 @@ * See README.md for usage and integration instructions. */ "use strict"; +/*jslint white: false */ /*global $, Util, RFB, Canvas, VNC_uri_prefix, Element, Fx */ var DefaultControls = { @@ -16,10 +17,6 @@ settingsOpen : false, load: function(target) { var html, i, DC = DefaultControls, sheet, sheets, llevels; - /* Handle state updates */ - RFB.setUpdateState(DC.updateState); - RFB.setClipboardReceive(DC.clipReceive); - /* Populate the 'target' DOM element with default controls */ if (!target) { target = 'vnc'; } @@ -61,7 +58,7 @@ load: function(target) { html += ' '; sheet = Util.selectStylesheet(); sheets = Util.getStylesheets(); - for (i = 0; i < sheets.length; i++) { + for (i = 0; i < sheets.length; i += 1) { html += ''; } html += ' Style'; @@ -69,7 +66,7 @@ load: function(target) { // Logging selection dropdown html += '
  • Logging
  • '; @@ -107,6 +104,8 @@ load: function(target) { DC.initSetting('logging', 'warn'); Util.init_logging(DC.getSetting('logging')); DC.initSetting('stylesheet', 'default'); + + Util.selectStylesheet(null); // call twice to get around webkit bug Util.selectStylesheet(DC.getSetting('stylesheet')); /* Populate the controls if defaults are provided in the URL */ @@ -118,12 +117,19 @@ load: function(target) { DC.initSetting('true_color', true); DC.initSetting('cursor', true); + DC.rfb = RFB({'target': 'VNC_canvas', + 'updateState': DC.updateState, + 'clipboardReceive': DC.clipReceive}); + DC.rfb.init(); + + // Unfocus clipboard when over the VNC area $('VNC_screen').onmousemove = function () { - // Unfocus clipboard when over the VNC area - if (! Canvas.focused) { + var canvas = DC.rfb.get_canvas(); + if ((! canvas) || (! canvas.get_focused())) { $('VNC_clipboard_text').blur(); } }; + }, // Read form control compatible setting from cookie @@ -154,7 +160,7 @@ updateSetting: function(name, value) { if (ctrl.type === 'checkbox') { ctrl.checked = value; } else if (typeof ctrl.options !== 'undefined') { - for (i = 0; i < ctrl.options.length; i++) { + for (i = 0; i < ctrl.options.length; i += 1) { if (ctrl.options[i].value === value) { ctrl.selectedIndex = i; break; @@ -176,7 +182,7 @@ saveSetting: function(name) { val = ctrl.value; } Util.createCookie(name, val); - Util.Debug("Setting saved '" + name + "=" + val + "'"); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); return val; }, @@ -190,7 +196,7 @@ initSetting: function(name, defVal) { val = Util.readCookie(name, defVal); } DefaultControls.updateSetting(name, val); - Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); + //Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); return val; }, @@ -208,7 +214,7 @@ clickSettingsMenu: function() { DC.updateSetting('encrypt'); DC.updateSetting('base64'); DC.updateSetting('true_color'); - if (Canvas.isCursor()) { + if (DC.rfb.get_canvas().get_cursor_uri()) { DC.updateSetting('cursor'); } else { DC.updateSetting('cursor', false); @@ -235,10 +241,11 @@ closeSettingsMenu: function() { // Disable/enable controls depending on connection state settingsDisabled: function(disabled) { + var DC = DefaultControls; $('VNC_encrypt').disabled = disabled; $('VNC_base64').disabled = disabled; $('VNC_true_color').disabled = disabled; - if (Canvas.isCursor()) { + if (DC.rfb && DC.rfb.get_canvas().get_cursor_uri()) { $('VNC_cursor').disabled = disabled; } else { DefaultControls.updateSetting('cursor', false); @@ -248,12 +255,12 @@ settingsDisabled: function(disabled) { // Save/apply settings when 'Apply' button is pressed settingsApply: function() { - Util.Debug(">> settingsApply"); + //Util.Debug(">> settingsApply"); var DC = DefaultControls; DC.saveSetting('encrypt'); DC.saveSetting('base64'); DC.saveSetting('true_color'); - if (Canvas.isCursor()) { + if (DC.rfb.get_canvas().get_cursor_uri()) { DC.saveSetting('cursor'); } DC.saveSetting('stylesheet'); @@ -263,21 +270,21 @@ settingsApply: function() { Util.selectStylesheet(DC.getSetting('stylesheet')); Util.init_logging(DC.getSetting('logging')); - Util.Debug("<< settingsApply"); + //Util.Debug("<< settingsApply"); }, setPassword: function() { - RFB.sendPassword($('VNC_password').value); + DefaultControls.rfb.sendPassword($('VNC_password').value); return false; }, sendCtrlAltDel: function() { - RFB.sendCtrlAltDel(); + DefaultControls.rfb.sendCtrlAltDel(); }, -updateState: function(state, msg) { +updateState: function(rfb, state, oldstate, msg) { var s, sb, c, cad, klass; s = $('VNC_status'); sb = $('VNC_status_bar'); @@ -334,6 +341,13 @@ updateState: function(state, msg) { }, +clipReceive: function(rfb, text) { + Util.Debug(">> DefaultControls.clipReceive: " + text.substr(0,40) + "..."); + $('VNC_clipboard_text').value = text; + Util.Debug("<< DefaultControls.clipReceive"); +}, + + connect: function() { var host, port, password, DC = DefaultControls; @@ -346,43 +360,37 @@ connect: function() { throw("Must set host and port"); } - RFB.setEncrypt(DC.getSetting('encrypt')); - RFB.setBase64(DC.getSetting('base64')); - RFB.setTrueColor(DC.getSetting('true_color')); - RFB.setCursor(DC.getSetting('cursor')); + DC.rfb.set_encrypt(DC.getSetting('encrypt')); + DC.rfb.set_b64encode(DC.getSetting('base64')); + DC.rfb.set_true_color(DC.getSetting('true_color')); + DC.rfb.set_local_cursor(DC.getSetting('cursor')); - RFB.connect(host, port, password); + DC.rfb.connect(host, port, password); }, disconnect: function() { DefaultControls.closeSettingsMenu(); - RFB.disconnect(); + DefaultControls.rfb.disconnect(); }, canvasBlur: function() { - Canvas.focused = false; + DefaultControls.rfb.get_canvas().set_focused(false); }, canvasFocus: function() { - Canvas.focused = true; + DefaultControls.rfb.get_canvas().set_focused(true); }, clipClear: function() { $('VNC_clipboard_text').value = ""; - RFB.clipboardPasteFrom(""); -}, - -clipReceive: function(text) { - Util.Debug(">> DefaultControls.clipReceive: " + text.substr(0,40) + "..."); - $('VNC_clipboard_text').value = text; - Util.Debug("<< DefaultControls.clipReceive"); + DefaultControls.rfb.clipboardPasteFrom(""); }, clipSend: function() { var text = $('VNC_clipboard_text').value; Util.Debug(">> DefaultControls.clipSend: " + text.substr(0,40) + "..."); - RFB.clipboardPasteFrom(text); + DefaultControls.rfb.clipboardPasteFrom(text); Util.Debug("<< DefaultControls.clipSend"); } diff --git a/include/rfb.js b/include/rfb.js index fee6f27..766f6eb 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -7,361 +7,743 @@ */ "use strict"; -/*jslint white: false, nomen: false, browser: true, bitwise: false */ +/*jslint white: false, browser: true, bitwise: false */ /*global window, WebSocket, Util, Canvas, VNC_native_ws, Base64, DES */ -// Globals defined here -var RFB; -/* - * RFB namespace - */ +function RFB(conf) { -RFB = { +conf = conf || {}; // Configuration +var that = {}, // Public API interface -/* - * External interface variables and methods - */ -host : '', -port : 5900, -password : '', + // Pre-declare private functions used before definitions (jslint) + init_vars, updateState, init_msg, normal_msg, recv_message, + framebufferUpdate, show_timings, -encrypt : true, -true_color : false, -b64encode : true, // false means UTF-8 on the wire -local_cursor : true, -connectTimeout : 2000, // time to wait for connection + pixelFormat, clientEncodings, fbUpdateRequest, + keyEvent, pointerEvent, clientCutText, + + extract_data_uri, scan_tight_imgs, + + send_array, checkEvents, // Overridable for testing -// In preference order -encodings : [ - ['COPYRECT', 0x01, 'display_copy_rect'], - ['TIGHT_PNG', -260, 'display_tight_png'], - ['HEXTILE', 0x05, 'display_hextile'], - ['RRE', 0x02, 'display_rre'], - ['RAW', 0x00, 'display_raw'], - ['DesktopSize', -223, 'set_desktopsize'], - ['Cursor', -239, 'set_cursor'], + // + // Private RFB namespace variables + // + rfb_host = '', + rfb_port = 5900, + rfb_password = '', - // Psuedo-encoding settings - ['JPEG_quality_lo', -32, 'set_jpeg_quality'], -// ['JPEG_quality_hi', -23, 'set_jpeg_quality'], - ['compress_lo', -255, 'set_compress_level'] -// ['compress_hi', -247, 'set_compress_level'] - ], + rfb_state = 'disconnected', + rfb_version = 0, + rfb_max_version= 3.8, + rfb_auth_scheme= '', + rfb_shared = 1, -setUpdateState: function(externalUpdateState) { - RFB.externalUpdateState = externalUpdateState; -}, + // In preference order + encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], -setClipboardReceive: function(clipReceive) { - RFB.clipboardCopyTo = clipReceive; -}, + // Psuedo-encoding settings + ['JPEG_quality_lo', -32 ], + //['JPEG_quality_hi', -23 ], + ['compress_lo', -255 ] + //['compress_hi', -247 ] + ], -setCanvasID: function(canvasID) { - RFB.canvasID = canvasID; -}, + encHandlers = {}, + encNames = {}, -setEncrypt: function(encrypt) { - if ((!encrypt) || (encrypt in {'0':1, 'no':1, 'false':1})) { - RFB.encrypt = false; - } else { - RFB.encrypt = true; - } -}, + ws = null, // Web Socket object + canvas = null, // Canvas object + sendID = null, // Send Queue check timer -setBase64: function(b64) { - if ((!b64) || (b64 in {'0':1, 'no':1, 'false':1})) { - RFB.b64encode = false; - } else { - RFB.b64encode = true; - } - Util.Debug("Set b64encode to: " + RFB.b64encode); -}, + // Receive and send queues + RQ = [], // Receive Queue + SQ = "", // Send Queue -setTrueColor: function(trueColor) { - if ((!trueColor) || (trueColor in {'0':1, 'no':1, 'false':1})) { - RFB.true_color = false; - } else { - RFB.true_color = true; - } -}, + // Frame buffer update state + FBU = { + rects : 0, + subrects : 0, // RRE and HEXTILE + lines : 0, // RAW + tiles : 0, // HEXTILE + bytes : 0, + x : 0, + y : 0, + width : 0, + height : 0, + encoding : 0, + subencoding : -1, + background : null, + imgs : [] // TIGHT_PNG image queue + }, -setCursor: function(cursor) { + fb_Bpp = 4, + fb_depth = 3, + fb_width = 0, + fb_height = 0, + fb_name = "", + + cuttext = 'none', // ServerCutText wait state + cuttext_length = 0, + + scan_imgs_rate = 100, + last_req_time = 0, + rre_chunk_sz = 100, + + timing = { + last_fbu : 0, + fbu_total : 0, + fbu_total_cnt : 0, + full_fbu_total : 0, + full_fbu_cnt : 0, + + fbu_rt_start : 0, + fbu_rt_total : 0, + fbu_rt_cnt : 0, + + history : [], + history_start : 0, + h_time : 0, + h_rects : 0, + h_fbus : 0, + h_bytes : 0, + h_pixels : 0 + }, + + test_mode = false, + + /* Mouse state */ + mouse_buttonMask = 0, + mouse_arr = []; + + +// +// Configuration settings +// + +// VNC viewport rendering Canvas +Util.conf_default(conf, that, 'target', 'VNC_canvas'); + +Util.conf_default(conf, that, 'encrypt', false, true); +Util.conf_default(conf, that, 'true_color', true, true); +// false means UTF-8 on the wire +Util.conf_default(conf, that, 'b64encode', true, true); +Util.conf_default(conf, that, 'local_cursor', true, true); + +// time to wait for connection +Util.conf_default(conf, that, 'connectTimeout', 2000); +// frequency to check for send/receive +Util.conf_default(conf, that, 'check_rate', 217); +// frequency to send frameBufferUpdate requests +Util.conf_default(conf, that, 'fbu_req_rate', 1413); + +// state update callback +Util.conf_default(conf, that, 'updateState', function () { + Util.Debug(">> externalUpdateState stub"); }); +// clipboard contents received callback +Util.conf_default(conf, that, 'clipboardReceive', function () { + Util.Debug(">> clipboardReceive stub"); }); + + +// Override/add some specific getters/setters +that.set_local_cursor = function(cursor) { if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { - RFB.local_cursor = false; + conf.local_cursor = false; } else { - if (Canvas.isCursor()) { - RFB.local_cursor = true; + if (canvas.get_cursor_uri()) { + conf.local_cursor = true; } else { Util.Warn("Browser does not support local cursor"); } } -}, +}; -sendPassword: function(passwd) { - RFB.password = passwd; - RFB.state = "Authentication"; - setTimeout(RFB.init_msg, 1); -}, +that.get_canvas = function() { + return canvas; +}; -sendCtrlAltDel: function() { - if (RFB.state !== "normal") { return false; } - Util.Info("Sending Ctrl-Alt-Del"); - var arr = []; - arr = arr.concat(RFB.keyEvent(0xFFE3, 1)); // Control - arr = arr.concat(RFB.keyEvent(0xFFE9, 1)); // Alt - arr = arr.concat(RFB.keyEvent(0xFFFF, 1)); // Delete - arr = arr.concat(RFB.keyEvent(0xFFFF, 0)); // Delete - arr = arr.concat(RFB.keyEvent(0xFFE9, 0)); // Alt - arr = arr.concat(RFB.keyEvent(0xFFE3, 0)); // Control - arr = arr.concat(RFB.fbUpdateRequest(1)); - RFB.send_array(arr); -}, -load: function () { + + +// +// Private functions +// + +// +// Setup routines +// + +// Create the public API interface +function constructor() { var i; - //Util.Debug(">> load"); + //Util.Debug(">> init"); - /* Load web-socket-js if no builtin WebSocket support */ - if (VNC_native_ws) { - Util.Info("Using native WebSockets"); - RFB.updateState('loaded', 'noVNC ready (using native WebSockets)'); - } else { - Util.Warn("Using web-socket-js flash bridge"); - if ((! Util.Flash) || - (Util.Flash.version < 9)) { - RFB.updateState('fatal', "WebSockets or Adobe Flash is required"); - } else if (document.location.href.substr(0, 7) === "file://") { - RFB.updateState('fatal', - "'file://' URL is incompatible with Adobe Flash"); - } else { - RFB.updateState('loaded', 'noVNC ready (using Flash WebSockets emulation)'); - } + // Create lookup tables based encoding number + for (i=0; i < encodings.length; i+=1) { + encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]]; + encNames[encodings[i][1]] = encodings[i][0]; } - - // Initialize canvas/fxcanvas + // Initialize canvas try { - Canvas.init(RFB.canvasID); + canvas = new Canvas({'target': conf.target}); } catch (exc) { - RFB.updateState('fatal', "No working Canvas"); + Util.Error("Canvas exception: " + exc); + updateState('fatal', "No working Canvas"); } - // Populate encoding lookup tables - RFB.encHandlers = {}; - RFB.encNames = {}; - for (i=0; i < RFB.encodings.length; i+=1) { - RFB.encHandlers[RFB.encodings[i][1]] = RFB[RFB.encodings[i][2]]; - RFB.encNames[RFB.encodings[i][1]] = RFB.encodings[i][0]; + //Util.Debug("<< init"); + return that; // Return the public API interface +} + +function init_ws() { + //Util.Debug(">> init_ws"); + + var uri = "", vars = []; + if (conf.encrypt) { + uri = "wss://"; + } else { + uri = "ws://"; } - //Util.Debug("<< load"); -}, + uri += rfb_host + ":" + rfb_port + "/"; + if (conf.b64encode) { + vars.push("b64encode"); + } + if (vars.length > 0) { + uri += "?" + vars.join("&"); + } + Util.Info("connecting to " + uri); + ws = new WebSocket(uri); -connect: function (host, port, password) { - //Util.Debug(">> connect"); + ws.onmessage = recv_message; + ws.onopen = function(e) { + Util.Debug(">> WebSocket.onopen"); + if (rfb_state === "connect") { + updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + updateState('failed', "Got unexpected WebSockets connection"); + } + Util.Debug("<< WebSocket.onopen"); + }; + ws.onclose = function(e) { + Util.Debug(">> WebSocket.onclose"); + if (rfb_state === 'normal') { + updateState('failed', 'Server disconnected'); + } else if (rfb_state === 'ProtocolVersion') { + updateState('failed', 'Failed to connect to server'); + } else { + updateState('disconnected', 'VNC disconnected'); + } + Util.Debug("<< WebSocket.onclose"); + }; + ws.onerror = function(e) { + Util.Debug(">> WebSocket.onerror"); + updateState('failed', "WebSocket error"); + Util.Debug("<< WebSocket.onerror"); + }; - RFB.host = host; - RFB.port = port; - RFB.password = (password !== undefined) ? password : ""; + setTimeout(function () { + if (ws.readyState === WebSocket.CONNECTING) { + updateState('failed', "Connect timeout"); + } + }, conf.connectTimeout); - if ((!RFB.host) || (!RFB.port)) { - RFB.updateState('failed', "Must set host and port"); + //Util.Debug("<< init_ws"); +} + +init_vars = function() { + /* Reset state */ + cuttext = 'none'; + cuttext_length = 0; + RQ = []; + SQ = ""; + FBU.rects = 0; + FBU.subrects = 0; // RRE and HEXTILE + FBU.lines = 0; // RAW + FBU.tiles = 0; // HEXTILE + FBU.imgs = []; // TIGHT_PNG image queue + mouse_buttonMask = 0; + mouse_arr = []; + + timing.history_start = 0; + timing.history = []; + timing.h_fbus = 0; + timing.h_rects = 0; + timing.h_bytes = 0; + timing.h_pixels = 0; +}; + +// +// Utility routines +// + + +/* + * Running states: + * disconnected - idle state + * normal - connected + * + * Page states: + * loaded - page load, equivalent to disconnected + * connect - starting initialization + * password - waiting for password + * failed - abnormal transition to disconnected + * fatal - failed to load page, or fatal error + * + * VNC initialization states: + * ProtocolVersion + * Security + * Authentication + * SecurityResult + * ServerInitialization + */ +updateState = function(state, statusMsg) { + var func, cmsg, oldstate = rfb_state; + if (state === oldstate) { + /* Already here, ignore */ + Util.Debug("Already in state '" + state + "', ignoring."); return; } - RFB.updateState('connect'); - //Util.Debug("<< connect"); + if (oldstate === 'fatal') { + Util.Error("Fatal error, cannot continue"); + } -}, + if ((state === 'failed') || (state === 'fatal')) { + func = Util.Error; + } else { + func = Util.Warn; + } -disconnect: function () { - //Util.Debug(">> disconnect"); - RFB.updateState('disconnected', 'Disconnected'); - //Util.Debug("<< disconnect"); -}, + cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + func("New state '" + state + "', was '" + oldstate + "'." + cmsg); -clipboardPasteFrom: function (text) { - if (RFB.state !== "normal") { return; } - //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); - RFB.send_array(RFB.clientCutText(text)); - //Util.Debug("<< clipboardPasteFrom"); -}, + if ((oldstate === 'failed') && (state === 'disconnected')) { + // Do disconnect action, but stay in failed state. + rfb_state = 'failed'; + } else { + rfb_state = state; + } + + switch (state) { + case 'loaded': + case 'disconnected': + + if (sendID) { + clearInterval(sendID); + sendID = null; + } + + if (ws) { + if (ws.readyState === WebSocket.OPEN) { + ws.close(); + } + ws.onmessage = function (e) { return; }; + } + + if (canvas && canvas.getContext()) { + canvas.stop(); + if (! /__debug__$/i.test(document.location.href)) { + canvas.clear(); + } + } + + show_timings(); + + break; -/* - * Private variables and methods - */ + case 'connect': + init_vars(); -ws : null, // Web Socket object -sendID : null, -scanID : null, // TIGHT_PNG render image scanner + if ((ws) && (ws.readyState === WebSocket.OPEN)) { + ws.close(); + } + init_ws(); // onopen transitions to 'ProtocolVersion' -// Receive and send queues -RQ : [], // Receive Queue -SQ : "", // Send Queue + break; -encHandlers : {}, -encNames : {}, -// Frame buffer update state -FBU : { - rects : 0, - subrects : 0, // RRE and HEXTILE - lines : 0, // RAW - tiles : 0, // HEXTILE - bytes : 0, - x : 0, - y : 0, - width : 0, - height : 0, - encoding : 0, - subencoding : -1, - background : null, - imgs : [] // TIGHT_PNG image queue -}, + case 'password': + // Ignore password state by default + break; -fb_Bpp : 4, -fb_depth : 3, -max_version : 3.8, -version : 0, -auth_scheme : '', -state : 'disconnected', -cuttext : 'none', // ServerCutText wait state -ct_length : 0, + case 'normal': + if ((oldstate === 'disconnected') || (oldstate === 'failed')) { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } -shared : 1, -check_rate : 217, -scan_imgs_rate : 100, -req_rate : 1413, -last_req : 0, + break; -canvasID : 'VNC_canvas', -fb_width : 0, -fb_height : 0, -fb_name : "", -rre_chunk : 100, -timing : { - last_fbu : 0, - fbu_total : 0, - fbu_total_cnt : 0, - full_fbu_total : 0, - full_fbu_cnt : 0, + 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."); + } - fbu_rt_start : 0, - fbu_rt_total : 0, - fbu_rt_cnt : 0, + if ((ws) && (ws.readyState === WebSocket.OPEN)) { + ws.close(); + } + // Make sure we transition to disconnected + setTimeout(function() { updateState('disconnected'); }, 50); - history : [], - history_start : 0, - h_time : 0, - h_rects : 0, - h_fbus : 0, - h_bytes : 0, - h_pixels : 0 -}, + break; -/* Mouse state */ -mouse_buttonmask : 0, -mouse_arr : [], -/* - * Server message handlers - */ + default: + // Invalid state transition -/* RFB/VNC initialisation */ -init_msg: function () { - //Util.Debug(">> init_msg [RFB.state '" + RFB.state + "']"); + } - var RQ = RFB.RQ, strlen, reason, reason_len, sversion, cversion, + if ((oldstate === 'failed') && (state === 'disconnected')) { + // Leave the failed message + conf.updateState(that, state, oldstate); + } else { + conf.updateState(that, state, oldstate, statusMsg); + } +}; + +function encode_message(arr) { + if (conf.b64encode) { + /* base64 encode */ + SQ = SQ + Base64.encode(arr); + } else { + /* UTF-8 encode. 0 -> 256 to avoid WebSockets framing */ + SQ = SQ + arr.map(function (num) { + if (num === 0) { + return String.fromCharCode(256); + } else { + return String.fromCharCode(num); + } + } ).join(''); + } +} + +function decode_message(data) { + var i, length; + //Util.Debug(">> decode_message: " + data); + if (conf.b64encode) { + /* base64 decode */ + RQ = RQ.concat(Base64.decode(data, 0)); + } else { + /* UTF-8 decode. 256 -> 0 to WebSockets framing */ + length = data.length; + for (i=0; i < length; i += 1) { + RQ.push(data.charCodeAt(i) % 256); + } + } + //Util.Debug(">> decode_message, RQ: " + RQ); +} + +function handle_message() { + //Util.Debug("RQ.slice(0,20): " + RQ.slice(0,20) + " (" + RQ.length + ")"); + if (RQ.length == 0) { + Util.Warn("handle_message called on empty receive queue"); + return; + } + switch (rfb_state) { + case 'disconnected': + Util.Error("Got data while disconnected"); + break; + case 'failed': + Util.Warn("Giving up!"); + that.disconnect(); + break; + case 'normal': + if (normal_msg() && RQ.length > 0) { + // true means we can continue processing + Util.Debug("More data to process"); + // Give other events a chance to run + setTimeout(handle_message, 10); + } + break; + default: + init_msg(); + break; + } +} + +recv_message = function(e) { + //Util.Debug(">> recv_message"); + + try { + decode_message(e.data); + if (RQ.length > 0) { + handle_message(); + } else { + Util.Debug("Ignoring empty message"); + } + } catch (exc) { + if (typeof exc.stack !== 'undefined') { + Util.Warn("recv_message, caught exception: " + exc.stack); + } else if (typeof exc.description !== 'undefined') { + Util.Warn("recv_message, caught exception: " + exc.description); + } else { + Util.Warn("recv_message, caught exception:" + exc); + } + if (typeof exc.name !== 'undefined') { + updateState('failed', exc.name + ": " + exc.message); + } else { + updateState('failed', exc); + } + } + //Util.Debug("<< recv_message"); +}; + +// overridable for testing +send_array = function(arr) { + //Util.Debug(">> send_array: " + arr); + encode_message(arr); + if (ws.bufferedAmount === 0) { + //Util.Debug("arr: " + arr); + //Util.Debug("SQ: " + SQ); + ws.send(SQ); + SQ = ""; + } else { + Util.Debug("Delaying send"); + } +}; + +function send_string(str) { + //Util.Debug(">> send_string: " + str); + send_array(str.split('').map( + function (chr) { return chr.charCodeAt(0); } ) ); +} + +function genDES(password, challenge) { + var i, passwd, response; + passwd = []; + response = challenge.slice(); + for (i=0; i < password.length; i += 1) { + passwd.push(password.charCodeAt(i)); + } + + DES.setKeys(passwd); + DES.encrypt(response, 0, response, 0); + DES.encrypt(response, 8, response, 8); + return response; +} + +function flushClient() { + if (mouse_arr.length > 0) { + //send_array(mouse_arr.concat(fbUpdateRequest(1))); + send_array(mouse_arr); + setTimeout(function() { + send_array(fbUpdateRequest(1)); + }, 50); + + mouse_arr = []; + return true; + } else { + return false; + } +} + +// overridable for testing +checkEvents = function() { + var now; + if (rfb_state === 'normal') { + if (! flushClient()) { + now = new Date().getTime(); + if (now > last_req_time + conf.fbu_req_rate) { + last_req_time = now; + send_array(fbUpdateRequest(1)); + } + } + } + setTimeout(checkEvents, conf.check_rate); +}; + +function keyPress(keysym, down) { + var arr; + arr = keyEvent(keysym, down); + arr = arr.concat(fbUpdateRequest(1)); + send_array(arr); +} + +function mouseButton(x, y, down, bmask) { + if (down) { + mouse_buttonMask |= bmask; + } else { + mouse_buttonMask ^= bmask; + } + mouse_arr = mouse_arr.concat( pointerEvent(x, y) ); + flushClient(); +} + +function mouseMove(x, y) { + //Util.Debug('>> mouseMove ' + x + "," + y); + mouse_arr = mouse_arr.concat( pointerEvent(x, y) ); +} + + +function update_timings() { + var now, offset; + now = (new Date()).getTime(); + timing.history.push([now, + timing.h_fbus, + timing.h_rects, + timing.h_bytes, + timing.h_pixels]); + timing.h_fbus = 0; + timing.h_rects = 0; + timing.h_bytes = 0; + timing.h_pixels = 0; + if ((rfb_state !== 'disconnected') && (rfb_state !== 'failed')) { + // Try for every second + offset = (now - timing.history_start) % 1000; + if (offset < 500) { + setTimeout(update_timings, 1000 - offset); + } else { + setTimeout(update_timings, 2000 - offset); + } + } +} + +show_timings = function() { + var i, history, msg, + delta, tot_time = 0, tot_fbus = 0, tot_rects = 0, + tot_bytes = 0, tot_pixels = 0; + if (timing.history_start === 0) { return; } + //Util.Debug(">> show_timings"); + update_timings(); // Final accumulate + msg = "\nTimings\n"; + msg += " time: fbus,rects,bytes,pixels\n"; + for (i=0; i < timing.history.length; i += 1) { + history = timing.history[i]; + delta = ((history[0]-timing.history_start)/1000); + tot_time = delta; + tot_fbus += history[1]; + tot_rects += history[2]; + tot_bytes += history[3]; + tot_pixels += history[4]; + + msg += " " + delta.toFixed(3); + msg += ": " + history.slice(1) + "\n"; + } + msg += "\nTotals:\n"; + msg += " time: fbus,rects,bytes,pixels\n"; + msg += " " + tot_time.toFixed(3); + msg += ": " + tot_fbus + "," + tot_rects; + msg += "," + tot_bytes + "," + tot_pixels; + Util.Info(msg); + //Util.Debug("<< show_timings"); +}; + +// +// Server message handlers +// + +// RFB/VNC initialisation message handler +init_msg = function() { + //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); + + var strlen, reason, reason_len, sversion, cversion, i, types, num_types, challenge, response, bpp, depth, big_endian, true_color, name_length; //Util.Debug("RQ (" + RQ.length + ") " + RQ); - switch (RFB.state) { + switch (rfb_state) { case 'ProtocolVersion' : if (RQ.length < 12) { - RFB.updateState('failed', + updateState('failed', "Disconnected: incomplete protocol version"); return; } sversion = RQ.shiftStr(12).substr(4,7); Util.Info("Server ProtocolVersion: " + sversion); switch (sversion) { - case "003.003": RFB.version = 3.3; break; - case "003.007": RFB.version = 3.7; break; - case "003.008": RFB.version = 3.8; break; + case "003.003": rfb_version = 3.3; break; + case "003.007": rfb_version = 3.7; break; + case "003.008": rfb_version = 3.8; break; default: - RFB.updateState('failed', + updateState('failed', "Invalid server version " + sversion); return; } - if (RFB.version > RFB.max_version) { - RFB.version = RFB.max_version; + if (rfb_version > rfb_max_version) { + rfb_version = rfb_max_version; } - RFB.sendID = setInterval(function() { - /* - * Send updates either at a rate of one update every 50ms, - * or whatever slower rate the network can handle - */ - if (RFB.ws.bufferedAmount === 0) { - if (RFB.SQ) { - RFB.ws.send(RFB.SQ); - RFB.SQ = ""; + if (! test_mode) { + sendID = setInterval(function() { + // Send updates either at a rate of one update + // every 50ms, or whatever slower rate the network + // can handle. + if (ws.bufferedAmount === 0) { + if (SQ) { + ws.send(SQ); + SQ = ""; + } + } else { + Util.Debug("Delaying send"); } - } else { - Util.Debug("Delaying send"); - } - }, 50); + }, 50); + } - cversion = "00" + parseInt(RFB.version,10) + - ".00" + ((RFB.version * 10) % 10); - RFB.send_string("RFB " + cversion + "\n"); - RFB.updateState('Security', "Sent ProtocolVersion: " + sversion); + cversion = "00" + parseInt(rfb_version,10) + + ".00" + ((rfb_version * 10) % 10); + send_string("RFB " + cversion + "\n"); + updateState('Security', "Sent ProtocolVersion: " + sversion); break; case 'Security' : - if (RFB.version >= 3.7) { + if (rfb_version >= 3.7) { num_types = RQ.shift8(); if (num_types === 0) { strlen = RQ.shift32(); reason = RQ.shiftStr(strlen); - RFB.updateState('failed', + updateState('failed', "Disconnected: security failure: " + reason); return; } - RFB.auth_scheme = 0; + rfb_auth_scheme = 0; types = RQ.shiftBytes(num_types); + Util.Debug("Server security types: " + types); for (i=0; i < types.length; i+=1) { - if ((types[i] > RFB.auth_scheme) && (types[i] < 3)) { - RFB.auth_scheme = types[i]; + if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) { + rfb_auth_scheme = types[i]; } } - if (RFB.auth_scheme === 0) { - RFB.updateState('failed', + if (rfb_auth_scheme === 0) { + updateState('failed', "Disconnected: unsupported security types: " + types); return; } - RFB.send_array([RFB.auth_scheme]); + send_array([rfb_auth_scheme]); } else { if (RQ.length < 4) { - RFB.updateState('failed', "Invalid security frame"); + updateState('failed', "Invalid security frame"); return; } - RFB.auth_scheme = RQ.shift32(); + rfb_auth_scheme = RQ.shift32(); } - RFB.updateState('Authentication', - "Authenticating using scheme: " + RFB.auth_scheme); - // Fall through + updateState('Authentication', + "Authenticating using scheme: " + rfb_auth_scheme); + init_msg(); // Recursive fallthrough (workaround JSLint complaint) + break; case 'Authentication' : - //Util.Debug("Security auth scheme: " + RFB.auth_scheme); - switch (RFB.auth_scheme) { + //Util.Debug("Security auth scheme: " + rfb_auth_scheme); + switch (rfb_auth_scheme) { case 0: // connection failed if (RQ.length < 4) { //Util.Debug(" waiting for auth reason bytes"); @@ -369,16 +751,15 @@ init_msg: function () { } strlen = RQ.shift32(); reason = RQ.shiftStr(strlen); - RFB.updateState('failed', + updateState('failed', "Disconnected: auth failure: " + reason); return; case 1: // no authentication - // RFB.send_array([RFB.shared]); // ClientInitialisation - RFB.updateState('SecurityResult'); + updateState('SecurityResult'); break; case 2: // VNC authentication - if (RFB.password.length === 0) { - RFB.updateState('password', "Password Required"); + if (rfb_password.length === 0) { + updateState('password', "Password Required"); return; } if (RQ.length < 16) { @@ -386,60 +767,60 @@ init_msg: function () { return; } challenge = RQ.shiftBytes(16); - //Util.Debug("Password: " + RFB.password); + //Util.Debug("Password: " + rfb_password); //Util.Debug("Challenge: " + challenge + // " (" + challenge.length + ")"); - response = RFB.DES(RFB.password, challenge); + response = genDES(rfb_password, challenge); //Util.Debug("Response: " + response + // " (" + response.length + ")"); //Util.Debug("Sending DES encrypted auth response"); - RFB.send_array(response); - RFB.updateState('SecurityResult'); + send_array(response); + updateState('SecurityResult'); break; default: - RFB.updateState('failed', + updateState('failed', "Disconnected: unsupported auth scheme: " + - RFB.auth_scheme); + rfb_auth_scheme); return; } break; case 'SecurityResult' : if (RQ.length < 4) { - RFB.updateState('failed', "Invalid VNC auth response"); + updateState('failed', "Invalid VNC auth response"); return; } switch (RQ.shift32()) { case 0: // OK - RFB.updateState('ServerInitialisation', "Authentication OK"); + updateState('ServerInitialisation', "Authentication OK"); break; case 1: // failed - if (RFB.version >= 3.8) { + if (rfb_version >= 3.8) { reason_len = RQ.shift32(); reason = RQ.shiftStr(reason_len); - RFB.updateState('failed', reason); + updateState('failed', reason); } else { - RFB.updateState('failed', "Authentication failed"); + updateState('failed', "Authentication failed"); } return; case 2: // too-many - RFB.updateState('failed', + updateState('failed', "Disconnected: too many auth attempts"); return; } - RFB.send_array([RFB.shared]); // ClientInitialisation + send_array([rfb_shared]); // ClientInitialisation break; case 'ServerInitialisation' : if (RQ.length < 24) { - RFB.updateState('failed', "Invalid server initialisation"); + updateState('failed', "Invalid server initialisation"); return; } /* Screen size */ - RFB.fb_width = RQ.shift16(); - RFB.fb_height = RQ.shift16(); + fb_width = RQ.shift16(); + fb_height = RQ.shift16(); /* PIXEL_FORMAT */ bpp = RQ.shift8(); @@ -447,7 +828,7 @@ init_msg: function () { big_endian = RQ.shift8(); true_color = RQ.shift8(); - Util.Info("Screen: " + RFB.fb_width + "x" + RFB.fb_height + + Util.Info("Screen: " + fb_width + "x" + fb_height + ", bpp: " + bpp + ", depth: " + depth + ", big_endian: " + big_endian + ", true_color: " + true_color); @@ -455,61 +836,61 @@ init_msg: function () { /* Connection name/title */ RQ.shiftStr(12); name_length = RQ.shift32(); - RFB.fb_name = RQ.shiftStr(name_length); + fb_name = RQ.shiftStr(name_length); - Canvas.resize(RFB.fb_width, RFB.fb_height, RFB.true_color); - Canvas.start(RFB.keyPress, RFB.mouseButton, RFB.mouseMove); + canvas.resize(fb_width, fb_height, conf.true_color); + canvas.start(keyPress, mouseButton, mouseMove); - if (RFB.true_color) { - RFB.fb_Bpp = 4; - RFB.fb_depth = 3; + if (conf.true_color) { + fb_Bpp = 4; + fb_depth = 3; } else { - RFB.fb_Bpp = 1; - RFB.fb_depth = 1; + fb_Bpp = 1; + fb_depth = 1; } - response = RFB.pixelFormat(); - response = response.concat(RFB.clientEncodings()); - response = response.concat(RFB.fbUpdateRequest(0)); - RFB.timing.fbu_rt_start = (new Date()).getTime(); - RFB.send_array(response); + response = pixelFormat(); + response = response.concat(clientEncodings()); + response = response.concat(fbUpdateRequest(0)); + timing.fbu_rt_start = (new Date()).getTime(); + send_array(response); /* Start pushing/polling */ - setTimeout(RFB.checkEvents, RFB.check_rate); - setTimeout(RFB.scan_tight_imgs, RFB.scan_imgs_rate); - RFB.timing.history_start = (new Date()).getTime(); - setTimeout(RFB.update_timings, 1000); + setTimeout(checkEvents, conf.check_rate); + setTimeout(scan_tight_imgs, scan_imgs_rate); + timing.history_start = (new Date()).getTime(); + setTimeout(update_timings, 1000); - if (RFB.encrypt) { - RFB.updateState('normal', "Connected (encrypted) to: " + RFB.fb_name); + if (conf.encrypt) { + updateState('normal', "Connected (encrypted) to: " + fb_name); } else { - RFB.updateState('normal', "Connected (unencrypted) to: " + RFB.fb_name); + updateState('normal', "Connected (unencrypted) to: " + fb_name); } break; } //Util.Debug("<< init_msg"); -}, +}; -/* Normal RFB/VNC server messages */ -normal_msg: function () { +/* Normal RFB/VNC server message handler */ +normal_msg = function() { //Util.Debug(">> normal_msg"); - var RQ = RFB.RQ, ret = true, msg_type, + var ret = true, msg_type, c, first_colour, num_colours, red, green, blue; //Util.Debug(">> msg RQ.slice(0,10): " + RQ.slice(0,20)); //Util.Debug(">> msg RQ.slice(-10,-1): " + RQ.slice(RQ.length-10,RQ.length)); - if (RFB.FBU.rects > 0) { + if (FBU.rects > 0) { msg_type = 0; - } else if (RFB.cuttext !== 'none') { + } else if (cuttext !== 'none') { msg_type = 3; } else { msg_type = RQ.shift8(); } switch (msg_type) { case 0: // FramebufferUpdate - ret = RFB.framebufferUpdate(); // false means need more data + ret = framebufferUpdate(); // false means need more data break; case 1: // SetColourMapEntries Util.Debug("SetColourMapEntries"); @@ -523,10 +904,10 @@ normal_msg: function () { //Util.Debug("red after: " + red); green = parseInt(RQ.shift16() / 256, 10); blue = parseInt(RQ.shift16() / 256, 10); - Canvas.colourMap[first_colour + c] = [red, green, blue]; + canvas.set_colourMap([red, green, blue], first_colour + c); } Util.Info("Registered " + num_colours + " colourMap entries"); - //Util.Debug("colourMap: " + Canvas.colourMap); + //Util.Debug("colourMap: " + canvas.get_colourMap()); break; case 2: // Bell Util.Warn("Bell (unsupported)"); @@ -534,39 +915,37 @@ normal_msg: function () { case 3: // ServerCutText Util.Debug("ServerCutText"); Util.Debug("RQ:" + RQ.slice(0,20)); - if (RFB.cuttext === 'none') { - RFB.cuttext = 'header'; + if (cuttext === 'none') { + cuttext = 'header'; } - if (RFB.cuttext === 'header') { + if (cuttext === 'header') { if (RQ.length < 7) { //Util.Debug("waiting for ServerCutText header"); return false; } RQ.shiftBytes(3); // Padding - RFB.ct_length = RQ.shift32(); + cuttext_length = RQ.shift32(); } - RFB.cuttext = 'bytes'; - if (RQ.length < RFB.ct_length) { + cuttext = 'bytes'; + if (RQ.length < cuttext_length) { //Util.Debug("waiting for ServerCutText bytes"); return false; } - RFB.clipboardCopyTo(RQ.shiftStr(RFB.ct_length)); - RFB.cuttext = 'none'; + conf.clipboardReceive(that, RQ.shiftStr(cuttext_length)); + cuttext = 'none'; break; default: - RFB.updateState('failed', + updateState('failed', "Disconnected: illegal server message type " + msg_type); Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30)); break; } //Util.Debug("<< normal_msg"); return ret; -}, +}; -framebufferUpdate: function() { - var RQ = RFB.RQ, FBU = RFB.FBU, timing = RFB.timing, - now, fbu_rt_diff, last_bytes, last_rects, - ret = true; +framebufferUpdate = function() { + var now, fbu_rt_diff, last_bytes, last_rects, ret = true; if (FBU.rects === 0) { //Util.Debug("New FBU: RQ.slice(0,20): " + RQ.slice(0,20)); @@ -588,7 +967,7 @@ framebufferUpdate: function() { } while (FBU.rects > 0) { - if (RFB.state !== "normal") { + if (rfb_state !== "normal") { return false; } if (RQ.length < FBU.bytes) { @@ -607,19 +986,19 @@ framebufferUpdate: function() { FBU.encoding = parseInt(RQ.shift32(), 10); timing.h_bytes += 12; - if (RFB.encNames[FBU.encoding]) { + if (encNames[FBU.encoding]) { // Debug: /* var msg = "FramebufferUpdate rects:" + FBU.rects; msg += " x: " + FBU.x + " y: " + FBU.y msg += " width: " + FBU.width + " height: " + FBU.height; msg += " encoding:" + FBU.encoding; - msg += "(" + RFB.encNames[FBU.encoding] + ")"; + msg += "(" + encNames[FBU.encoding] + ")"; msg += ", RQ.length: " + RQ.length; Util.Debug(msg); */ } else { - RFB.updateState('failed', + updateState('failed', "Disconnected: unsupported encoding " + FBU.encoding); return false; @@ -631,7 +1010,7 @@ framebufferUpdate: function() { last_rects = FBU.rects; // false ret means need more data - ret = RFB.encHandlers[FBU.encoding](); + ret = encHandlers[FBU.encoding](); now = (new Date()).getTime(); timing.cur_fbu += (now - timing.last_fbu); @@ -644,8 +1023,8 @@ framebufferUpdate: function() { } if (FBU.rects === 0) { - if (((FBU.width === RFB.fb_width) && - (FBU.height === RFB.fb_height)) || + if (((FBU.width === fb_width) && + (FBU.height === fb_height)) || (timing.fbu_rt_start > 0)) { timing.full_fbu_total += timing.cur_fbu; timing.full_fbu_cnt += 1; @@ -671,21 +1050,21 @@ framebufferUpdate: function() { } } return ret; -}, +}; -/* - * FramebufferUpdate encodings - */ +// +// FramebufferUpdate encodings +// -display_raw: function () { +encHandlers.RAW = function display_raw() { //Util.Debug(">> display_raw"); - var RQ = RFB.RQ, FBU = RFB.FBU, cur_y, cur_height; + var cur_y, cur_height; if (FBU.lines === 0) { FBU.lines = FBU.height; } - FBU.bytes = FBU.width * RFB.fb_Bpp; // At least a line + FBU.bytes = FBU.width * fb_Bpp; // At least a line if (RQ.length < FBU.bytes) { //Util.Debug(" waiting for " + // (FBU.bytes - RQ.length) + " RAW bytes"); @@ -693,24 +1072,24 @@ display_raw: function () { } cur_y = FBU.y + (FBU.height - FBU.lines); cur_height = Math.min(FBU.lines, - Math.floor(RQ.length/(FBU.width * RFB.fb_Bpp))); - Canvas.blitImage(FBU.x, cur_y, FBU.width, cur_height, RQ, 0); - RQ.shiftBytes(FBU.width * cur_height * RFB.fb_Bpp); + Math.floor(RQ.length/(FBU.width * fb_Bpp))); + canvas.blitImage(FBU.x, cur_y, FBU.width, cur_height, RQ, 0); + RQ.shiftBytes(FBU.width * cur_height * fb_Bpp); FBU.lines -= cur_height; if (FBU.lines > 0) { - FBU.bytes = FBU.width * RFB.fb_Bpp; // At least another line + FBU.bytes = FBU.width * fb_Bpp; // At least another line } else { FBU.rects -= 1; FBU.bytes = 0; } return true; -}, +}; -display_copy_rect: function () { +encHandlers.COPYRECT = function display_copy_rect() { //Util.Debug(">> display_copy_rect"); - var RQ = RFB.RQ, FBU = RFB.FBU, old_x, old_y; + var old_x, old_y; if (RQ.length < 4) { //Util.Debug(" waiting for " + @@ -719,52 +1098,52 @@ display_copy_rect: function () { } old_x = RQ.shift16(); old_y = RQ.shift16(); - Canvas.copyImage(old_x, old_y, FBU.x, FBU.y, FBU.width, FBU.height); + canvas.copyImage(old_x, old_y, FBU.x, FBU.y, FBU.width, FBU.height); FBU.rects -= 1; FBU.bytes = 0; return true; -}, +}; + +encHandlers.RRE = function display_rre() { + //Util.Debug(">> display_rre (" + RQ.length + " bytes)"); + var color, x, y, width, height, chunk; -display_rre: function () { - //Util.Debug(">> display_rre (" + RFB.RQ.length + " bytes)"); - var RQ = RFB.RQ, FBU = RFB.FBU, color, x, y, width, height, chunk; if (FBU.subrects === 0) { - if (RQ.length < 4 + RFB.fb_Bpp) { + if (RQ.length < 4 + fb_Bpp) { //Util.Debug(" waiting for " + - // (4 + RFB.fb_Bpp - RQ.length) + " RRE bytes"); + // (4 + fb_Bpp - RQ.length) + " RRE bytes"); return false; } FBU.subrects = RQ.shift32(); - color = RQ.shiftBytes(RFB.fb_Bpp); // Background - Canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); + color = RQ.shiftBytes(fb_Bpp); // Background + canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); } - while ((FBU.subrects > 0) && (RQ.length >= (RFB.fb_Bpp + 8))) { - color = RQ.shiftBytes(RFB.fb_Bpp); + while ((FBU.subrects > 0) && (RQ.length >= (fb_Bpp + 8))) { + color = RQ.shiftBytes(fb_Bpp); x = RQ.shift16(); y = RQ.shift16(); width = RQ.shift16(); height = RQ.shift16(); - Canvas.fillRect(FBU.x + x, FBU.y + y, width, height, color); + canvas.fillRect(FBU.x + x, FBU.y + y, width, height, color); FBU.subrects -= 1; } //Util.Debug(" display_rre: rects: " + FBU.rects + // ", FBU.subrects: " + FBU.subrects); if (FBU.subrects > 0) { - chunk = Math.min(RFB.rre_chunk, FBU.subrects); - FBU.bytes = (RFB.fb_Bpp + 8) * chunk; + chunk = Math.min(rre_chunk_sz, FBU.subrects); + FBU.bytes = (fb_Bpp + 8) * chunk; } else { FBU.rects -= 1; FBU.bytes = 0; } //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes); return true; -}, +}; -display_hextile: function() { +encHandlers.HEXTILE = function display_hextile() { //Util.Debug(">> display_hextile"); - var RQ = RFB.RQ, FBU = RFB.FBU, - subencoding, subrects, idx, tile, color, cur_tile, + var subencoding, subrects, idx, tile, color, cur_tile, tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh; if (FBU.tiles === 0) { @@ -783,7 +1162,7 @@ display_hextile: function() { } subencoding = RQ[0]; // Peek if (subencoding > 30) { // Raw - RFB.updateState('failed', + updateState('failed', "Disconnected: illegal hextile subencoding " + subencoding); //Util.Debug("RQ.slice(0,30):" + RQ.slice(0,30)); return false; @@ -800,13 +1179,13 @@ display_hextile: function() { /* Figure out how much we are expecting */ if (subencoding & 0x01) { // Raw //Util.Debug(" Raw subencoding"); - FBU.bytes += w * h * RFB.fb_Bpp; + FBU.bytes += w * h * fb_Bpp; } else { if (subencoding & 0x02) { // Background - FBU.bytes += RFB.fb_Bpp; + FBU.bytes += fb_Bpp; } if (subencoding & 0x04) { // Foreground - FBU.bytes += RFB.fb_Bpp; + FBU.bytes += fb_Bpp; } if (subencoding & 0x08) { // AnySubrects FBU.bytes += 1; // Since we aren't shifting it off @@ -817,7 +1196,7 @@ display_hextile: function() { } subrects = RQ[FBU.bytes-1]; // Peek if (subencoding & 0x10) { // SubrectsColoured - FBU.bytes += subrects * (RFB.fb_Bpp + 2); + FBU.bytes += subrects * (fb_Bpp + 2); } else { FBU.bytes += subrects * 2; } @@ -846,28 +1225,28 @@ display_hextile: function() { /* Weird: ignore blanks after RAW */ Util.Debug(" Ignoring blank after RAW"); } else { - Canvas.fillRect(x, y, w, h, FBU.background); + canvas.fillRect(x, y, w, h, FBU.background); } } else if (FBU.subencoding & 0x01) { // Raw - Canvas.blitImage(x, y, w, h, RQ, idx); + canvas.blitImage(x, y, w, h, RQ, idx); } else { if (FBU.subencoding & 0x02) { // Background - FBU.background = RQ.slice(idx, idx + RFB.fb_Bpp); - idx += RFB.fb_Bpp; + FBU.background = RQ.slice(idx, idx + fb_Bpp); + idx += fb_Bpp; } if (FBU.subencoding & 0x04) { // Foreground - FBU.foreground = RQ.slice(idx, idx + RFB.fb_Bpp); - idx += RFB.fb_Bpp; + FBU.foreground = RQ.slice(idx, idx + fb_Bpp); + idx += fb_Bpp; } - tile = Canvas.getTile(x, y, w, h, FBU.background); + tile = canvas.getTile(x, y, w, h, FBU.background); if (FBU.subencoding & 0x08) { // AnySubrects subrects = RQ[idx]; idx += 1; for (s = 0; s < subrects; s += 1) { if (FBU.subencoding & 0x10) { // SubrectsColoured - color = RQ.slice(idx, idx + RFB.fb_Bpp); - idx += RFB.fb_Bpp; + color = RQ.slice(idx, idx + fb_Bpp); + idx += fb_Bpp; } else { color = FBU.foreground; } @@ -881,10 +1260,10 @@ display_hextile: function() { sw = (wh >> 4) + 1; sh = (wh & 0x0f) + 1; - Canvas.setSubTile(tile, sx, sy, sw, sh, color); + canvas.setSubTile(tile, sx, sy, sw, sh, color); } } - Canvas.putTile(tile); + canvas.putTile(tile); } RQ.shiftBytes(FBU.bytes); FBU.lastsubencoding = FBU.subencoding; @@ -898,13 +1277,12 @@ display_hextile: function() { //Util.Debug("<< display_hextile"); return true; -}, +}; -display_tight_png: function() { +encHandlers.TIGHT_PNG = function display_tight_png() { //Util.Debug(">> display_tight_png"); - var RQ = RFB.RQ, FBU = RFB.FBU, - ctl, cmode, clength, getCLength, color, img; + var ctl, cmode, clength, getCLength, color, img; //Util.Debug(" FBU.rects: " + FBU.rects); //Util.Debug(" RQ.length: " + RQ.length); //Util.Debug(" RQ.slice(0,20): " + RQ.slice(0,20)); @@ -940,7 +1318,7 @@ display_tight_png: function() { } switch (cmode) { // fill uses fb_depth because TPIXELs drop the padding byte - case "fill": FBU.bytes += RFB.fb_depth; break; // TPIXEL + case "fill": FBU.bytes += fb_depth; break; // TPIXEL case "jpeg": FBU.bytes += 3; break; // max clength case "png": FBU.bytes += 3; break; // max clength } @@ -950,15 +1328,15 @@ display_tight_png: function() { return false; } - //Util.Debug(" RQ.slice(0,20): " + RFB.RQ.slice(0,20) + " (" + RFB.RQ.length + ")"); + //Util.Debug(" RQ.slice(0,20): " + RQ.slice(0,20) + " (" + RQ.length + ")"); //Util.Debug(" cmode: " + cmode); // Determine FBU.bytes switch (cmode) { case "fill": RQ.shift8(); // shift off ctl - color = RQ.shiftBytes(RFB.fb_depth); - Canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); + color = RQ.shiftBytes(fb_depth); + canvas.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); break; case "jpeg": case "png": @@ -973,10 +1351,10 @@ display_tight_png: function() { //Util.Debug(" png, RQ.length: " + RQ.length + ", clength[0]: " + clength[0] + ", clength[1]: " + clength[1]); RQ.shiftBytes(1 + clength[0]); // shift off ctl + compact length img = new Image(); - img.onload = RFB.scan_tight_imgs; + img.onload = scan_tight_imgs; FBU.imgs.push([img, FBU.x, FBU.y]); img.src = "data:image/" + cmode + - RFB.extract_data_uri(RQ.shiftBytes(clength[1])); + extract_data_uri(RQ.shiftBytes(clength[1])); img = null; break; } @@ -986,88 +1364,90 @@ display_tight_png: function() { //Util.Debug(" ending RQ.slice(0,20): " + RQ.slice(0,20)); //Util.Debug("<< display_tight_png"); return true; -}, +}; -extract_data_uri : function (arr) { +extract_data_uri = function(arr) { //var i, stra = []; //for (i=0; i< arr.length; i += 1) { // stra.push(String.fromCharCode(arr[i])); //} //return "," + escape(stra.join('')); return ";base64," + Base64.encode(arr); -}, +}; -scan_tight_imgs : function () { - var img, imgs; - if (RFB.state === 'normal') { - imgs = RFB.FBU.imgs; +scan_tight_imgs = function() { + var img, imgs, ctx; + ctx = canvas.getContext(); + if (rfb_state === 'normal') { + imgs = FBU.imgs; while ((imgs.length > 0) && (imgs[0][0].complete)) { img = imgs.shift(); - Canvas.ctx.drawImage(img[0], img[1], img[2]); + ctx.drawImage(img[0], img[1], img[2]); } - setTimeout(RFB.scan_tight_imgs, RFB.scan_imgs_rate); + setTimeout(scan_tight_imgs, scan_imgs_rate); } -}, +}; -set_desktopsize : function () { +encHandlers.DesktopSize = function set_desktopsize() { Util.Debug(">> set_desktopsize"); - RFB.fb_width = RFB.FBU.width; - RFB.fb_height = RFB.FBU.height; - Canvas.clear(); - Canvas.resize(RFB.fb_width, RFB.fb_height); - RFB.timing.fbu_rt_start = (new Date()).getTime(); + fb_width = FBU.width; + fb_height = FBU.height; + canvas.clear(); + canvas.resize(fb_width, fb_height); + timing.fbu_rt_start = (new Date()).getTime(); // Send a new non-incremental request - RFB.send_array(RFB.fbUpdateRequest(0)); + send_array(fbUpdateRequest(0)); - RFB.FBU.bytes = 0; - RFB.FBU.rects -= 1; + FBU.bytes = 0; + FBU.rects -= 1; Util.Debug("<< set_desktopsize"); return true; -}, +}; -set_cursor: function () { +encHandlers.Cursor = function set_cursor() { var x, y, w, h, pixelslength, masklength; //Util.Debug(">> set_cursor"); - x = RFB.FBU.x; // hotspot-x - y = RFB.FBU.y; // hotspot-y - w = RFB.FBU.width; - h = RFB.FBU.height; + x = FBU.x; // hotspot-x + y = FBU.y; // hotspot-y + w = FBU.width; + h = FBU.height; - pixelslength = w * h * RFB.fb_Bpp; + pixelslength = w * h * fb_Bpp; masklength = Math.floor((w + 7) / 8) * h; - if (RFB.RQ.length < (pixelslength + masklength)) { + if (RQ.length < (pixelslength + masklength)) { //Util.Debug("waiting for cursor encoding bytes"); - RFB.FBU.bytes = pixelslength + masklength; + FBU.bytes = pixelslength + masklength; return false; } //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); - Canvas.changeCursor(RFB.RQ.shiftBytes(pixelslength), - RFB.RQ.shiftBytes(masklength), - x, y, w, h); + canvas.changeCursor(RQ.shiftBytes(pixelslength), + RQ.shiftBytes(masklength), + x, y, w, h); - RFB.FBU.bytes = 0; - RFB.FBU.rects -= 1; + FBU.bytes = 0; + FBU.rects -= 1; //Util.Debug("<< set_cursor"); return true; -}, +}; -set_jpeg_quality : function () { - Util.Debug(">> set_jpeg_quality"); -}, -set_compress_level: function () { - Util.Debug(">> set_compress_level"); -}, +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 () { +pixelFormat = function() { //Util.Debug(">> pixelFormat"); var arr; arr = [0]; // msg-type @@ -1075,10 +1455,10 @@ pixelFormat: function () { arr.push8(0); // padding arr.push8(0); // padding - arr.push8(RFB.fb_Bpp * 8); // bits-per-pixel - arr.push8(RFB.fb_depth * 8); // depth + arr.push8(fb_Bpp * 8); // bits-per-pixel + arr.push8(fb_depth * 8); // depth arr.push8(0); // little-endian - arr.push8(RFB.true_color); // true-color + arr.push8(conf.true_color ? 1 : 0); // true-color arr.push16(255); // red-max arr.push16(255); // green-max @@ -1092,22 +1472,19 @@ pixelFormat: function () { arr.push8(0); // padding //Util.Debug("<< pixelFormat"); return arr; -}, +}; -fixColourMapEntries: function () { -}, - -clientEncodings: function () { +clientEncodings = function() { //Util.Debug(">> clientEncodings"); var arr, i, encList = []; - for (i=0; i> fbUpdateRequest"); if (!x) { x = 0; } if (!y) { y = 0; } - if (!xw) { xw = RFB.fb_width; } - if (!yw) { yw = RFB.fb_height; } + if (!xw) { xw = fb_width; } + if (!yw) { yw = fb_height; } var arr; arr = [3]; // msg-type arr.push8(incremental); @@ -1137,9 +1514,9 @@ fbUpdateRequest: function (incremental, x, y, xw, yw) { arr.push16(yw); //Util.Debug("<< fbUpdateRequest"); return arr; -}, +}; -keyEvent: function (keysym, down) { +keyEvent = function(keysym, down) { //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down); var arr; arr = [4]; // msg-type @@ -1148,21 +1525,21 @@ keyEvent: function (keysym, down) { arr.push32(keysym); //Util.Debug("<< keyEvent"); return arr; -}, +}; -pointerEvent: function (x, y) { +pointerEvent = function(x, y) { //Util.Debug(">> pointerEvent, x,y: " + x + "," + y + - // " , mask: " + RFB.mouse_buttonMask); + // " , mask: " + mouse_buttonMask); var arr; arr = [5]; // msg-type - arr.push8(RFB.mouse_buttonMask); + arr.push8(mouse_buttonMask); arr.push16(x); arr.push16(y); //Util.Debug("<< pointerEvent"); return arr; -}, +}; -clientCutText: function (text) { +clientCutText = function(text) { //Util.Debug(">> clientCutText"); var arr; arr = [6]; // msg-type @@ -1173,456 +1550,107 @@ clientCutText: function (text) { arr.pushStr(text); //Util.Debug("<< clientCutText:" + arr); return arr; -}, +}; -/* - * Utility routines - */ -encode_message: function(arr) { - if (RFB.b64encode) { - /* base64 encode */ - RFB.SQ = RFB.SQ + Base64.encode(arr); +// +// Public API interface functions +// + +that.init = function () { + + init_vars(); + + /* Check web-socket-js if no builtin WebSocket support */ + if (VNC_native_ws) { + Util.Info("Using native WebSockets"); + updateState('loaded', 'noVNC ready (using native WebSockets)'); } else { - /* UTF-8 encode. 0 -> 256 to avoid WebSockets framing */ - RFB.SQ = RFB.SQ + arr.map(function (num) { - if (num === 0) { - return String.fromCharCode(256); - } else { - return String.fromCharCode(num); - } - } ).join(''); - } -}, - -decode_message: function(data) { - var i, length, RQ = RFB.RQ; - //Util.Debug(">> decode_message: " + data); - if (RFB.b64encode) { - /* base64 decode */ - RFB.RQ = RFB.RQ.concat(Base64.decode(data, 0)); - } else { - /* UTF-8 decode. 256 -> 0 to WebSockets framing */ - length = data.length; - for (i=0; i < length; i += 1) { - RQ.push(data.charCodeAt(i) % 256); - } - } - //Util.Debug(">> decode_message, RQ: " + RFB.RQ); -}, - -recv_message: function(e) { - //Util.Debug(">> recv_message"); - - try { - RFB.decode_message(e.data); - if (RFB.RQ.length > 0) { - RFB.handle_message(); + Util.Warn("Using web-socket-js flash bridge"); + if ((! Util.Flash) || + (Util.Flash.version < 9)) { + updateState('fatal', "WebSockets or Adobe Flash is required"); + } else if (document.location.href.substr(0, 7) === "file://") { + updateState('fatal', + "'file://' URL is incompatible with Adobe Flash"); } else { - Util.Debug("Ignoring empty message"); - } - } catch (exc) { - if (typeof exc.stack !== 'undefined') { - Util.Warn("recv_message, caught exception: " + exc.stack); - } else if (typeof exc.description !== 'undefined') { - Util.Warn("recv_message, caught exception: " + exc.description); - } else { - Util.Warn("recv_message, caught exception:" + exc); - } - if (typeof exc.name !== 'undefined') { - RFB.updateState('failed', exc.name + ": " + exc.message); - } else { - RFB.updateState('failed', exc); + updateState('loaded', 'noVNC ready (using Flash WebSockets emulation)'); } } - //Util.Debug("<< recv_message"); -}, +}; -handle_message: function () { - //Util.Debug("RQ.slice(0,20): " + RFB.RQ.slice(0,20) + " (" + RFB.RQ.length + ")"); - switch (RFB.state) { - case 'disconnected': - Util.Error("Got data while disconnected"); - break; - case 'failed': - Util.Warn("Giving up!"); - RFB.disconnect(); - break; - case 'normal': - if ((RFB.state === 'normal') && (RFB.RQ.length > 0)) { - if (RFB.normal_msg()) { - // true means we can continue processing - Util.Debug("More data to process"); - // Give other events a chance to run - setTimeout(RFB.handle_message, 10); - } - } - break; - default: - RFB.init_msg(); - break; - } -}, +that.connect = function(host, port, password) { + //Util.Debug(">> connect"); -send_string: function (str) { - //Util.Debug(">> send_string: " + str); - RFB.send_array(str.split('').map( - function (chr) { return chr.charCodeAt(0); } ) ); -}, - -send_array: function (arr) { - //Util.Debug(">> send_array: " + arr); - RFB.encode_message(arr); - if (RFB.ws.bufferedAmount === 0) { - //Util.Debug("arr: " + arr); - //Util.Debug("RFB.SQ: " + RFB.SQ); - RFB.ws.send(RFB.SQ); - RFB.SQ = ""; - } else { - Util.Debug("Delaying send"); - } -}, - -DES: function (password, challenge) { - var i, passwd, response; - passwd = []; - response = challenge.slice(); - for (i=0; i < password.length; i += 1) { - passwd.push(password.charCodeAt(i)); + // Make sure we have done init checks + if ((rfb_state !== 'loaded') && (rfb_state !== 'fatal')) { + that.init(); } - DES.setKeys(passwd); - DES.encrypt(response, 0, response, 0); - DES.encrypt(response, 8, response, 8); - return response; -}, + rfb_host = host; + rfb_port = port; + rfb_password = (password !== undefined) ? password : ""; -flushClient: function () { - if (RFB.mouse_arr.length > 0) { - //RFB.send_array(RFB.mouse_arr.concat(RFB.fbUpdateRequest(1))); - RFB.send_array(RFB.mouse_arr); - setTimeout(function() { - RFB.send_array(RFB.fbUpdateRequest(1)); - }, 50); - - RFB.mouse_arr = []; - return true; - } else { - return false; - } -}, - -checkEvents: function () { - var now; - if (RFB.state === 'normal') { - if (! RFB.flushClient()) { - now = new Date().getTime(); - if (now > RFB.last_req + RFB.req_rate) { - RFB.last_req = now; - RFB.send_array(RFB.fbUpdateRequest(1)); - } - } - } - setTimeout(RFB.checkEvents, RFB.check_rate); -}, - -keyPress: function (keysym, down) { - var arr; - arr = RFB.keyEvent(keysym, down); - arr = arr.concat(RFB.fbUpdateRequest(1)); - RFB.send_array(arr); -}, - -mouseButton: function(x, y, down, bmask) { - if (down) { - RFB.mouse_buttonMask |= bmask; - } else { - RFB.mouse_buttonMask ^= bmask; - } - RFB.mouse_arr = RFB.mouse_arr.concat( RFB.pointerEvent(x, y) ); - RFB.flushClient(); -}, - -mouseMove: function(x, y) { - //Util.Debug('>> mouseMove ' + x + "," + y); - RFB.mouse_arr = RFB.mouse_arr.concat( RFB.pointerEvent(x, y) ); -}, - -clipboardCopyTo: function (text) { - Util.Debug(">> clipboardCopyTo stub"); - // Stub -}, - -externalUpdateState: function(state, msg) { - Util.Debug(">> externalUpdateState stub"); - // Stub -}, - -/* - * Running states: - * disconnected - idle state - * normal - connected - * - * Page states: - * loaded - page load, equivalent to disconnected - * connect - starting initialization - * password - waiting for password - * failed - abnormal transition to disconnected - * fatal - failed to load page, or fatal error - * - * VNC initialization states: - * ProtocolVersion - * Security - * Authentication - * SecurityResult - * ServerInitialization - */ -updateState: function(state, statusMsg) { - var func, cmsg, oldstate = RFB.state; - if (state === oldstate) { - /* Already here, ignore */ - Util.Debug("Already in state '" + state + "', ignoring."); + if ((!rfb_host) || (!rfb_port)) { + updateState('failed', "Must set host and port"); return; } - if (oldstate === 'fatal') { - Util.Error("Fatal error, cannot continue"); - } + updateState('connect'); + //Util.Debug("<< connect"); - if ((state === 'failed') || (state === 'fatal')) { - func = Util.Error; - } else { - func = Util.Warn; - } +}; - cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; - func("New state '" + state + "', was '" + oldstate + "'." + cmsg); +that.disconnect = function() { + //Util.Debug(">> disconnect"); + updateState('disconnected', 'Disconnected'); + //Util.Debug("<< disconnect"); +}; - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Do disconnect action, but stay in failed state. - RFB.state = 'failed'; - } else { - RFB.state = state; - } +that.sendPassword = function(passwd) { + rfb_password = passwd; + rfb_state = "Authentication"; + setTimeout(init_msg, 1); +}; - switch (state) { - case 'loaded': - case 'disconnected': +that.sendCtrlAltDel = function() { + if (rfb_state !== "normal") { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + var arr = []; + arr = arr.concat(keyEvent(0xFFE3, 1)); // Control + arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt + arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete + arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete + arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt + arr = arr.concat(keyEvent(0xFFE3, 0)); // Control + arr = arr.concat(fbUpdateRequest(1)); + send_array(arr); +}; - if (RFB.sendID) { - clearInterval(RFB.sendID); - RFB.sendID = null; - } +that.clipboardPasteFrom = function(text) { + if (rfb_state !== "normal") { return; } + //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); + send_array(clientCutText(text)); + //Util.Debug("<< clipboardPasteFrom"); +}; - if (RFB.ws) { - if (RFB.ws.readyState === WebSocket.OPEN) { - RFB.ws.close(); - } - RFB.ws.onmessage = function (e) { return; }; - } +that.testMode = function(override_send_array) { + // Overridable internal functions for testing + test_mode = true; + send_array = override_send_array; + that.recv_message = recv_message; // Expose it - if (Canvas.ctx) { - Canvas.stop(); - if (! /__debug__$/i.test(document.location.href)) { - Canvas.clear(); - } - } - - RFB.show_timings(); - - break; + checkEvents = function () { /* Stub Out */ }; + that.connect = function(host, port, password) { + rfb_host = host; + rfb_port = port; + rfb_password = password; + updateState('ProtocolVersion', "Starting VNC handshake"); + }; +}; - case 'connect': - RFB.init_vars(); +return constructor(); // Return the public API interface - if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { - RFB.ws.close(); - } - RFB.init_ws(); // onopen transitions to 'ProtocolVersion' - - break; - - - case 'password': - // Ignore password state by default - break; - - - case 'normal': - if ((oldstate === 'disconnected') || (oldstate === 'failed')) { - Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); - } - - 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."); - } - - if ((RFB.ws) && (RFB.ws.readyState === WebSocket.OPEN)) { - RFB.ws.close(); - } - // Make sure we transition to disconnected - setTimeout(function() { RFB.updateState('disconnected'); }, 50); - - break; - - - default: - // Invalid state transition - - } - - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Leave the failed message - RFB.externalUpdateState(state); - } else { - RFB.externalUpdateState(state, statusMsg); - } -}, - -update_timings: function() { - var now, timing = RFB.timing, offset; - now = (new Date()).getTime(); - timing.history.push([now, - timing.h_fbus, - timing.h_rects, - timing.h_bytes, - timing.h_pixels]); - timing.h_fbus = 0; - timing.h_rects = 0; - timing.h_bytes = 0; - timing.h_pixels = 0; - if ((RFB.state !== 'disconnected') && (RFB.state !== 'failed')) { - // Try for every second - offset = (now - timing.history_start) % 1000; - if (offset < 500) { - setTimeout(RFB.update_timings, 1000 - offset); - } else { - setTimeout(RFB.update_timings, 2000 - offset); - } - } -}, - -show_timings: function() { - var i, timing = RFB.timing, history, msg, - delta, tot_time = 0, tot_fbus = 0, tot_rects = 0, - tot_bytes = 0, tot_pixels = 0; - if (timing.history_start === 0) { return; } - //Util.Debug(">> show_timings"); - RFB.update_timings(); // Final accumulate - msg = "\nTimings\n"; - msg += " time: fbus,rects,bytes,pixels\n"; - for (i=0; i < timing.history.length; i += 1) { - history = timing.history[i]; - delta = ((history[0]-timing.history_start)/1000); - tot_time = delta; - tot_fbus += history[1]; - tot_rects += history[2]; - tot_bytes += history[3]; - tot_pixels += history[4]; - - msg += " " + delta.toFixed(3); - msg += ": " + history.slice(1) + "\n"; - } - msg += "\nTotals:\n"; - msg += " time: fbus,rects,bytes,pixels\n"; - msg += " " + tot_time.toFixed(3); - msg += ": " + tot_fbus + "," + tot_rects; - msg += "," + tot_bytes + "," + tot_pixels; - Util.Info(msg); - //Util.Debug("<< show_timings"); -}, - -/* - * Setup routines - */ - -init_ws: function () { - //Util.Debug(">> init_ws"); - - var uri = "", vars = []; - if (RFB.encrypt) { - uri = "wss://"; - } else { - uri = "ws://"; - } - uri += RFB.host + ":" + RFB.port + "/"; - if (RFB.b64encode) { - vars.push("b64encode"); - } - if (vars.length > 0) { - uri += "?" + vars.join("&"); - } - Util.Info("connecting to " + uri); - RFB.ws = new WebSocket(uri); - - RFB.ws.onmessage = RFB.recv_message; - RFB.ws.onopen = function(e) { - Util.Debug(">> WebSocket.onopen"); - if (RFB.state === "connect") { - RFB.updateState('ProtocolVersion', "Starting VNC handshake"); - } else { - RFB.updateState('failed', "Got unexpected WebSockets connection"); - } - Util.Debug("<< WebSocket.onopen"); - }; - RFB.ws.onclose = function(e) { - Util.Debug(">> WebSocket.onclose"); - if (RFB.state === 'normal') { - RFB.updateState('failed', 'Server disconnected'); - } else if (RFB.state === 'ProtocolVersion') { - RFB.updateState('failed', 'Failed to connect to server'); - } else { - RFB.updateState('disconnected', 'VNC disconnected'); - } - Util.Debug("<< WebSocket.onclose"); - }; - RFB.ws.onerror = function(e) { - Util.Debug(">> WebSocket.onerror"); - RFB.updateState('failed', "WebSocket error"); - Util.Debug("<< WebSocket.onerror"); - }; - - setTimeout(function () { - if (RFB.ws.readyState === WebSocket.CONNECTING) { - RFB.updateState('failed', "Connect timeout"); - } - }, RFB.connectTimeout); - - //Util.Debug("<< init_ws"); -}, - -init_vars: function () { - /* Reset state */ - RFB.cuttext = 'none'; - RFB.ct_length = 0; - RFB.RQ = []; - RFB.SQ = ""; - RFB.FBU.rects = 0; - RFB.FBU.subrects = 0; // RRE and HEXTILE - RFB.FBU.lines = 0; // RAW - RFB.FBU.tiles = 0; // HEXTILE - RFB.FBU.imgs = []; // TIGHT_PNG image queue - RFB.mouse_buttonmask = 0; - RFB.mouse_arr = []; - - RFB.timing.history_start = 0; - RFB.timing.history = []; - RFB.timing.h_fbus = 0; - RFB.timing.h_rects = 0; - RFB.timing.h_bytes = 0; - RFB.timing.h_pixels = 0; -} - -}; /* End of RFB */ +} // End of RFB() diff --git a/include/util.js b/include/util.js index 938506a..a355f7b 100644 --- a/include/util.js +++ b/include/util.js @@ -14,41 +14,6 @@ var Util = {}, $; -/* - * Logging/debug routines - */ - -Util.init_logging = function (level) { - if (typeof window.console === "undefined") { - if (typeof window.opera !== "undefined") { - window.console = { - 'log' : window.opera.postError, - 'warn' : window.opera.postError, - 'error': window.opera.postError }; - } else { - window.console = { - 'log' : function(m) {}, - 'warn' : function(m) {}, - 'error': function(m) {}}; - } - } - - Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; - switch (level) { - case 'debug': Util.Debug = function (msg) { console.log(msg); }; - case 'info': Util.Info = function (msg) { console.log(msg); }; - case 'warn': Util.Warn = function (msg) { console.warn(msg); }; - case 'error': Util.Error = function (msg) { console.error(msg); }; - break; - default: - throw("invalid logging type '" + level + "'"); - } -}; -// Initialize logging level -Util.init_logging( (document.location.href.match( - /logging=([A-Za-z0-9\._\-]*)/) || - ['', 'warn'])[1] ); - /* * Simple DOM selector by ID */ @@ -138,6 +103,42 @@ Array.prototype.shiftBytes = function (len) { * ------------------------------------------------------ */ +/* + * Logging/debug routines + */ + +Util.init_logging = function (level) { + if (typeof window.console === "undefined") { + if (typeof window.opera !== "undefined") { + window.console = { + 'log' : window.opera.postError, + 'warn' : window.opera.postError, + 'error': window.opera.postError }; + } else { + window.console = { + 'log' : function(m) {}, + 'warn' : function(m) {}, + 'error': function(m) {}}; + } + } + + Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + switch (level) { + case 'debug': Util.Debug = function (msg) { console.log(msg); }; + case 'info': Util.Info = function (msg) { console.log(msg); }; + case 'warn': Util.Warn = function (msg) { console.warn(msg); }; + case 'error': Util.Error = function (msg) { console.error(msg); }; + case 'none': + break; + default: + throw("invalid logging type '" + level + "'"); + } +}; +// Initialize logging level +Util.init_logging( (document.location.href.match( + /logging=([A-Za-z0-9\._\-]*)/) || + ['', 'warn'])[1] ); + Util.dirObj = function (obj, depth, parent) { var i, msg = "", val = ""; if (! depth) { depth=2; } @@ -168,7 +169,7 @@ Util.getQueryVar = function(name, defVal) { }; // Set defaults for Crockford style function namespaces -Util.conf_default = function(cfg, api, v, val) { +Util.conf_default = function(cfg, api, v, val, force_bool) { if (typeof cfg[v] === 'undefined') { cfg[v] = val; } @@ -181,6 +182,13 @@ Util.conf_default = function(cfg, api, v, val) { // Default setter if (typeof api['set_' + v] === 'undefined') { api['set_' + v] = function (val) { + if (force_bool) { + if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { + val = false; + } else { + val = true; + } + } cfg[v] = val; }; } @@ -311,10 +319,9 @@ Util.createCookie = function(name,value,days) { }; Util.readCookie = function(name, defaultValue) { - var nameEQ = name + "="; - var ca = document.cookie.split(';'); - for(var i=0;i < ca.length;i++) { - var c = ca[i]; + var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); + for(i=0; i < ca.length; i += 1) { + c = ca[i]; while (c.charAt(0) === ' ') { c = c.substring(1,c.length); } if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); } } @@ -330,7 +337,7 @@ Util.eraseCookie = function(name) { */ Util.getStylesheets = function() { var i, links, sheets = []; links = document.getElementsByTagName("link"); - for (i = 0; i < links.length; i++) { + for (i = 0; i < links.length; i += 1) { if (links[i].title && links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { sheets.push(links[i]); @@ -346,19 +353,15 @@ Util.selectStylesheet = function(sheet) { if (typeof sheet === 'undefined') { sheet = 'default'; } - for (i=0; i < sheets.length; i++) { + for (i=0; i < sheets.length; i += 1) { link = sheets[i]; if (link.title === sheet) { Util.Debug("Using stylesheet " + sheet); link.disabled = false; } else { - Util.Debug("Skipping stylesheet " + link.title); + //Util.Debug("Skipping stylesheet " + link.title); link.disabled = true; } } return sheet; }; - -// call once to disable alternates and get around webkit bug -Util.selectStylesheet(null); - diff --git a/tests/canvas.html b/tests/canvas.html index e762acd..87e7781 100644 --- a/tests/canvas.html +++ b/tests/canvas.html @@ -46,14 +46,16 @@ } function test_functions () { - var img, x, y; - Canvas.fillRect(0, 0, Canvas.c_wx, Canvas.c_wy, [240,240,240]); + var img, x, y, w, h, ctx = canvas.getContext(); + w = canvas.get_width(); + h = canvas.get_height(); + canvas.fillRect(0, 0, w, h, [240,240,240]); - Canvas.blitStringImage("data:image/png;base64," + face64, 150, 10); + canvas.blitStringImage("data:image/png;base64," + face64, 150, 10); var himg = new Image(); himg.onload = function () { - Canvas.ctx.drawImage(himg, 200, 40); }; + ctx.drawImage(himg, 200, 40); }; himg.src = "face.png"; /* Test array image data */ @@ -66,15 +68,14 @@ data[(y*50 + x)*4 + 3] = 255; } } - Canvas.blitImage(30, 10, 50, 50, data, 0); + canvas.blitImage(30, 10, 50, 50, data, 0); - //Canvas.prefer_js = false; - img = Canvas.getTile(5,5,16,16,[0,128,128]); - Canvas.putTile(img); + img = canvas.getTile(5,5,16,16,[0,128,128]); + canvas.putTile(img); - img = Canvas.getTile(90,15,16,16,[0,0,0]); - Canvas.setSubTile(img, 0,0,16,16,[128,128,0]); - Canvas.putTile(img); + img = canvas.getTile(90,15,16,16,[0,0,0]); + canvas.setSubTile(img, 0,0,16,16,[128,128,0]); + canvas.putTile(img); } function begin () { @@ -85,30 +86,35 @@ } function start_delayed () { + var ret; - message("Running test: prefer Javascript"); - Canvas.prefer_js = true; - var time1 = run_test(); - message("prefer Javascript: " + time1 + "ms total, " + - (time1 / iterations) + "ms per frame"); + ret = canvas.set_prefer_js(true); + if (ret) { + message("Running test: prefer Javascript ops"); + var time1 = run_test(); + message("prefer Javascript ops: " + time1 + "ms total, " + + (time1 / iterations) + "ms per frame"); + } else { + message("Could not run: prefer Javascript ops"); + } + canvas.set_prefer_js(false); message("Running test: prefer Canvas ops"); - Canvas.prefer_js = false; var time2 = run_test(); message("prefer Canvas ops: " + time2 + "ms total, " + (time2 / iterations) + "ms per frame"); - Canvas.resize(start_width, start_height, true); + canvas.resize(start_width, start_height, true); test_functions(); $('startButton').disabled = false; - $('startButton').value = "Start"; + $('startButton').value = "Do Performance Test"; } function run_test () { var width, height; width = $('width').value; height = $('height').value; - Canvas.resize(width, height); + canvas.resize(width, height); var color, start_time = (new Date()).getTime(), w, h; for (var i=0; i < iterations; i++) { color = [128, 128, (255 / iterations) * i, 0]; @@ -116,9 +122,9 @@ for (var y=0; y < height; y = y + 16) { w = Math.min(16, width - x); h = Math.min(16, height - y); - var tile = Canvas.getTile(x, y, w, h, color); - Canvas.setSubTile(tile, 0, 0, w, h, color); - Canvas.putTile(tile); + var tile = canvas.getTile(x, y, w, h, color); + canvas.setSubTile(tile, 0, 0, w, h, color); + canvas.putTile(tile); } } } @@ -129,8 +135,8 @@ window.onload = function() { message("in onload"); $('iterations').value = 10; - Canvas.init('canvas'); - Canvas.resize(start_width, start_height, true); + canvas = Canvas({'target' : 'canvas'}); + canvas.resize(start_width, start_height, true); message("Canvas initialized"); test_functions(); } diff --git a/tests/cursor.html b/tests/cursor.html index d244db1..81fba82 100644 --- a/tests/cursor.html +++ b/tests/cursor.html @@ -105,10 +105,10 @@ window.onload = function() { debug("onload"); - var cross, cursor, cursor64; + var canvas, cross, cursor, cursor64; - Canvas.init("testcanvas"); - debug("Canvas.init() indicates Data URI cursor support is: " + Canvas.isCursor()); + canvas = new Canvas({'target' : "testcanvas"}); + debug("canvas indicates Data URI cursor support is: " + canvas.get_cursor_uri()); $('button1').style.cursor="url(face.png), default"; diff --git a/tests/input.html b/tests/input.html index f89768e..43791ed 100644 --- a/tests/input.html +++ b/tests/input.html @@ -19,6 +19,7 @@ src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'> --> + diff --git a/vnc.html b/vnc.html index 9a4596a..9155964 100644 --- a/vnc.html +++ b/vnc.html @@ -16,12 +16,12 @@ noVNC example: simple example using default controls
    Loading
    - - + }; + + + diff --git a/vnc_auto.html b/vnc_auto.html index 1d37172..600b09f 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -7,8 +7,7 @@ Connect parameters are provided in query string: VNC Client - - +