From dc4b6301c810bd82f4d00c2032ac6775973a4edb Mon Sep 17 00:00:00 2001 From: Alexander Clouter Date: Sat, 22 Nov 2014 10:20:51 +0000 Subject: [PATCH 1/3] slip in PATH for 'npm test' as not everyone has karma So people can follow the 'unit tests' instructions, we need to make sure PATH includes the karma bin directory otherwise we see the following: ---- aclouter@stevemcqueen:/usr/src/aten-ikvm/noVNC$ npm test > noVNC@0.5.0 test /usr/src/aten-ikvm/noVNC > karma start karma.conf.js sh: 1: karma: not found npm ERR! Test failed. See above for more details. npm ERR! not ok code 0 ---- --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63c26662..81a2eddf 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "tests" }, "scripts": { - "test": "karma start karma.conf.js" + "test": "PATH=$PATH:node_modules/karma/bin karma start karma.conf.js" }, "repository": { "type": "git", From d2c58aec70d444004aed8ea896874fe40e5630cb Mon Sep 17 00:00:00 2001 From: Alexander Clouter Date: Sun, 12 Oct 2014 00:27:44 +0100 Subject: [PATCH 2/3] added client side pixel format conversion support --- README.md | 1 + include/display.js | 28 +--- include/rfb.js | 288 +++++++++++++++++++++++++++--------------- include/ui.js | 10 +- tests/test.display.js | 23 +--- tests/test.rfb.js | 178 ++++++++++++++++++-------- vnc.html | 4 +- vnc_auto.html | 4 +- 8 files changed, 326 insertions(+), 210 deletions(-) diff --git a/README.md b/README.md index b5679cdd..b1384ba6 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ use a WebSockets to TCP socket proxy. There is a python proxy included * UI and Icons : Chris Gordon * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) + * pixel format conversion : [Alexander Clouter](http://www.digriz.org.uk/) * Included libraries: * web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js) diff --git a/include/display.js b/include/display.js index a42b854a..45044f88 100644 --- a/include/display.js +++ b/include/display.js @@ -414,21 +414,12 @@ var Display; blitImage: function (x, y, width, height, arr, offset) { if (this._true_color) { - this._bgrxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + this._imageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); } else { this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); } }, - blitRgbImage: function (x, y , width, height, arr, offset) { - if (this._true_color) { - this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); - } else { - // probably wrong? - this._cmapImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); - } - }, - blitStringImage: function (str, x, y) { var img = new Image(); img.onload = function () { @@ -542,7 +533,7 @@ var Display; } }, - _rgbImageData: function (x, y, vx, vy, width, height, arr, offset) { + _imageData: function (x, y, vx, vy, width, height, arr, offset) { var img = this._drawCtx.createImageData(width, height); var data = img.data; for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { @@ -554,18 +545,6 @@ var Display; this._drawCtx.putImageData(img, x - vx, y - vy); }, - _bgrxImageData: function (x, y, vx, vy, width, height, arr, offset) { - var img = this._drawCtx.createImageData(width, height); - var data = img.data; - for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { - data[i] = arr[j + 2]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x - vx, y - vy); - }, - _cmapImageData: function (x, y, vx, vy, width, height, arr, offset) { var img = this._drawCtx.createImageData(width, height); var data = img.data; @@ -594,9 +573,6 @@ var Display; case 'blit': this.blitImage(a.x, a.y, a.width, a.height, a.data, 0); break; - case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); - break; case 'img': if (a.img.complete) { this.drawImage(a.img, a.x, a.y); diff --git a/include/rfb.js b/include/rfb.js index 59fd785d..66e62681 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -85,8 +85,7 @@ var RFB; zlib: [] // TIGHT zlib streams }; - this._fb_Bpp = 4; - this._fb_depth = 3; + this._pixelFormat = {}; this._fb_width = 0; this._fb_height = 0; this._fb_name = ""; @@ -117,7 +116,7 @@ var RFB; 'target': 'null', // VNC display rendering Canvas object 'focusContainer': document, // DOM element that captures keyboard input 'encrypt': false, // Use TLS/SSL/wss encryption - 'true_color': true, // Request true color pixel data + 'convertColor': false, // Client will not honor request for native color 'local_cursor': false, // Request locally rendered cursor 'shared': true, // Request shared mode 'view_only': false, // Disable client mouse/keyboard @@ -854,21 +853,21 @@ var RFB; if (this._sock.rQwait("server initialization", 24)) { return false; } /* Screen size */ - this._fb_width = this._sock.rQshift16(); - this._fb_height = this._sock.rQshift16(); + this._fb_width = this._sock.rQshift16(); + this._fb_height = this._sock.rQshift16(); /* PIXEL_FORMAT */ - var bpp = this._sock.rQshift8(); - var depth = this._sock.rQshift8(); - var big_endian = this._sock.rQshift8(); - var true_color = this._sock.rQshift8(); + this._pixelFormat.bpp = this._sock.rQshift8(); + this._pixelFormat.depth = this._sock.rQshift8(); + this._pixelFormat.big_endian = (this._sock.rQshift8() === 1) ? true : false; + this._pixelFormat.true_color = (this._sock.rQshift8() === 1) ? true : false; - var red_max = this._sock.rQshift16(); - var green_max = this._sock.rQshift16(); - var blue_max = this._sock.rQshift16(); - var red_shift = this._sock.rQshift8(); - var green_shift = this._sock.rQshift8(); - var blue_shift = this._sock.rQshift8(); + this._pixelFormat.red_max = this._sock.rQshift16(); + this._pixelFormat.green_max = this._sock.rQshift16(); + this._pixelFormat.blue_max = this._sock.rQshift16(); + this._pixelFormat.red_shift = this._sock.rQshift8(); + this._pixelFormat.green_shift = this._sock.rQshift8(); + this._pixelFormat.blue_shift = this._sock.rQshift8(); this._sock.rQskipBytes(3); // padding // NB(directxman12): we don't want to call any callbacks or print messages until @@ -907,56 +906,90 @@ var RFB; // NB(directxman12): these are down here so that we don't run them multiple times // if we backtrack Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); - - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } - - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } - - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } + ", bpp: " + this._pixelFormat.bpp + ", depth: " + this._pixelFormat.depth + + ", big_endian: " + this._pixelFormat.big_endian + + ", true_color: " + this._pixelFormat.true_color + + ", red_max: " + this._pixelFormat.red_max + + ", green_max: " + this._pixelFormat.green_max + + ", blue_max: " + this._pixelFormat.blue_max + + ", red_shift: " + this._pixelFormat.red_shift + + ", green_shift: " + this._pixelFormat.green_shift + + ", blue_shift: " + this._pixelFormat.blue_shift); // we're past the point where we could backtrack, so it's safe to call this this._onDesktopName(this, this._fb_name); - if (this._true_color && this._fb_name === "Intel(r) AMT KVM") { - Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color"); - this._true_color = false; + if (this._fb_name === "Intel(r) AMT KVM") { + Util.Warn("Intel AMT KVM only supports 8/16 bit depths, using server pixel format"); + this._convertColor = true; } - this._display.set_true_color(this._true_color); + if (this._convertColor) + this._display.set_true_color(this._pixelFormat.true_color); this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); this._keyboard.grab(); this._mouse.grab(); - if (this._true_color) { - this._fb_Bpp = 4; - this._fb_depth = 3; - } else { - this._fb_Bpp = 1; - this._fb_depth = 1; + var response = []; + + // only send if not native, and we think the server will honor the conversion + if (!this._convertColor) { + if (this._pixelFormat.big_endian !== false + || this._pixelFormat.red_max !== 255 + || this._pixelFormat.green_max !== 255 + || this._pixelFormat.blue_max !== 255 + || this._pixelFormat.red_shift !== 16 + || this._pixelFormat.green_shift !== 8 + || this._pixelFormat.blue_shift !== 0 + || !( this._pixelFormat.bpp === 32 + && this._pixelFormat.depth === 24 + && this._pixelFormat.true_color === true ) + || !( this._pixelFormat.bpp === 8 + && this._pixelFormat.depth === 8 + && this._pixelFormat.true_color === false )) { + this._pixelFormat.big_endian = false; + this._pixelFormat.red_max = 255; + this._pixelFormat.green_max = 255; + this._pixelFormat.blue_max = 255; + this._pixelFormat.red_shift = 16; + this._pixelFormat.green_shift = 8; + this._pixelFormat.blue_shift = 0; + if (this._pixelFormat.true_color) { + this._pixelFormat.bpp = 32; + this._pixelFormat.depth = 24; + } else { + this._pixelFormat.bpp = 8; + this._pixelFormat.depth = 8; + } + response = response.concat(RFB.messages.pixelFormat(this._pixelFormat)); + } else { + Util.Warn("Server pixel format matches our preferred native, disabling color conversion"); + this._convertColor = false; + } } - var response = RFB.messages.pixelFormat(this._fb_Bpp, this._fb_depth, this._true_color); - response = response.concat( - RFB.messages.clientEncodings(this._encodings, this._local_cursor, this._true_color)); - response = response.concat( - RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), - this._fb_width, this._fb_height)); + this._pixelFormat.Bpp = this._pixelFormat.bpp / 8; + this._pixelFormat.Bdepth = Math.ceil(this._pixelFormat.depth / 8); + + if (this._pixelFormat.bpp < this._pixelFormat.depth) { + return this._fail('server claims greater depth than bpp'); + } + + if (this._pixelFormat.true_color + && this._pixelFormat.depth > + Math.ceil(Math.log(this._pixelFormat.red_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.green_max)/Math.LN2) + + Math.ceil(Math.log(this._pixelFormat.blue_max)/Math.LN2)) { + return this._fail('server claims greater depth than sum of RGB maximums'); + } + + response = response.concat(RFB.messages.clientEncodings( + this._encodings, this._local_cursor, + this._pixelFormat.true_color)); + response = response.concat(RFB.messages.fbUpdateRequests( + this._display.getCleanDirtyReset(), + this._fb_width, this._fb_height)); this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; @@ -1184,13 +1217,58 @@ var RFB; return true; // We finished this FBU }, + + // input: byte stream in pixel format + // output: RGB + _convert_color: function (arr, Bpp) { + if (Bpp === undefined) + Bpp = this._pixelFormat.Bpp; + + if (!this._convertColor + // HACK? Xtightvnc needs this and I have no idea why + || (this._FBU.encoding === 0x07 && this._pixelFormat.depth === 24)) { + switch (Bpp) { + case 4: + for (var i = arr.length; i > 0; i -= 4) { + arr.splice(i - 1, 1); // convert from RGBX to RGB + } + case 3: + return arr; + default: + Util.Error('convert color disabled, but Bpp is not 3 or 4!'); + } + } + + var data = []; + + for (var i = 0; i < arr.length; i += Bpp) { + var pix = 0; + + for (var j = 0; j < Bpp; j++) { + if (this._pixelFormat.big_endian) { + pix = (pix << 8) | arr[i + j]; + } else { + pix = (arr[i + j] << (j*8)) | pix; + } + } + + data.push(((pix >>> this._pixelFormat.red_shift) & this._pixelFormat.red_max) + * (256/(this._pixelFormat.red_max + 1))); + data.push(((pix >>> this._pixelFormat.green_shift) & this._pixelFormat.green_max) + * (256/(this._pixelFormat.green_max + 1))); + data.push(((pix >>> this._pixelFormat.blue_shift) & this._pixelFormat.blue_max) + * (256/(this._pixelFormat.blue_max + 1))); + } + + return data; + }, }; Util.make_properties(RFB, [ ['target', 'wo', 'dom'], // VNC display rendering Canvas object ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption - ['true_color', 'rw', 'bool'], // Request true color pixel data + ['convertColor', 'rw', 'bool'], // Client will not honor request for native color ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor ['shared', 'rw', 'bool'], // Request shared mode ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard @@ -1261,23 +1339,23 @@ var RFB; return arr; }, - pixelFormat: function (bpp, depth, true_color) { + pixelFormat: function (pf) { var arr = [0]; // msg-type arr.push8(0); // padding arr.push8(0); // padding arr.push8(0); // padding - arr.push8(bpp * 8); // bits-per-pixel - arr.push8(depth * 8); // depth - arr.push8(0); // little-endian - arr.push8(true_color ? 1 : 0); // true-color + arr.push8(pf.bpp); + arr.push8(pf.depth); + arr.push8(pf.big_endian ? 1 : 0); + arr.push8(pf.true_color ? 1 : 0); - arr.push16(255); // red-max - arr.push16(255); // green-max - arr.push16(255); // blue-max - arr.push8(16); // red-shift - arr.push8(8); // green-shift - arr.push8(0); // blue-shift + arr.push16(pf.red_max); + arr.push16(pf.green_max); + arr.push16(pf.blue_max); + arr.push8(pf.red_shift); + arr.push8(pf.green_shift); + arr.push8(pf.blue_shift); arr.push8(0); // padding arr.push8(0); // padding @@ -1366,19 +1444,25 @@ var RFB; this._FBU.lines = this._FBU.height; } - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // at least a line if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); var curr_height = Math.min(this._FBU.lines, - Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp))); - this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, - curr_height, this._sock.get_rQ(), - this._sock.get_rQi()); - this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp); + Math.floor(this._sock.rQlen() / (this._FBU.width * this._pixelFormat.Bpp))); + + this._display.renderQ_push({ + 'type': 'blit', + 'data': this._convert_color(this._sock.rQshiftBytes(curr_height * this._FBU.width * this._pixelFormat.Bpp)), + 'x': this._FBU.x, + 'y': cur_y, + 'width': this._FBU.width, + 'height': curr_height + }); + this._FBU.lines -= curr_height; if (this._FBU.lines > 0) { - this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + this._FBU.bytes = this._FBU.width * this._pixelFormat.Bpp; // At least another line } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1407,15 +1491,15 @@ var RFB; RRE: function () { var color; if (this._FBU.subrects === 0) { - this._FBU.bytes = 4 + this._fb_Bpp; - if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.bytes = 4 + this._pixelFormat.Bpp; + if (this._sock.rQwait("RRE", 4 + this._pixelFormat.Bpp)) { return false; } this._FBU.subrects = this._sock.rQshift32(); - color = this._sock.rQshiftBytes(this._fb_Bpp); // Background + color = this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)).reverse(); // Background this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); } - while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) { - color = this._sock.rQshiftBytes(this._fb_Bpp); + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._pixelFormat.Bpp + 8)) { + color = this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bpp)).reverse(); var x = this._sock.rQshift16(); var y = this._sock.rQshift16(); var width = this._sock.rQshift16(); @@ -1426,7 +1510,7 @@ var RFB; if (this._FBU.subrects > 0) { var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); - this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + this._FBU.bytes = (this._pixelFormat.Bpp + 8) * chunk; } else { this._FBU.rects--; this._FBU.bytes = 0; @@ -1466,20 +1550,20 @@ var RFB; // Figure out how much we are expecting if (subencoding & 0x01) { // Raw - this._FBU.bytes += w * h * this._fb_Bpp; + this._FBU.bytes += w * h * this._pixelFormat.Bpp; } else { if (subencoding & 0x02) { // Background - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x04) { // Foreground - this._FBU.bytes += this._fb_Bpp; + this._FBU.bytes += this._pixelFormat.Bpp; } if (subencoding & 0x08) { // AnySubrects this._FBU.bytes++; // Since we aren't shifting it off if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek if (subencoding & 0x10) { // SubrectsColoured - this._FBU.bytes += subrects * (this._fb_Bpp + 2); + this._FBU.bytes += subrects * (this._pixelFormat.Bpp + 2); } else { this._FBU.bytes += subrects * 2; } @@ -1499,16 +1583,23 @@ var RFB; this._display.fillRect(x, y, w, h, this._FBU.background); } } else if (this._FBU.subencoding & 0x01) { // Raw - this._display.blitImage(x, y, w, h, rQ, rQi); + this._display.renderQ_push({ + 'type': 'blit', + 'data': this._convert_color(rQ.slice(rQi, rQi + this._FBU.bytes - 1)), + 'x': x, + 'y': y, + 'width': w, + 'height': h + }); rQi += this._FBU.bytes - 1; } else { if (this._FBU.subencoding & 0x02) { // Background - this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp); - rQi += this._fb_Bpp; + this._FBU.background = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse(); + rQi += this._pixelFormat.Bpp; } if (this._FBU.subencoding & 0x04) { // Foreground - this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); - rQi += this._fb_Bpp; + this._FBU.foreground = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse(); + rQi += this._pixelFormat.Bpp; } this._display.startTile(x, y, w, h, this._FBU.background); @@ -1519,8 +1610,8 @@ var RFB; for (var s = 0; s < subrects; s++) { var color; if (this._FBU.subencoding & 0x10) { // SubrectsColoured - color = rQ.slice(rQi, rQi + this._fb_Bpp); - rQi += this._fb_Bpp; + color = this._convert_color(rQ.slice(rQi, rQi + this._pixelFormat.Bpp)).reverse(); + rQi += this._pixelFormat.Bpp; } else { color = this._FBU.foreground; } @@ -1567,7 +1658,7 @@ var RFB; }, display_tight: function (isTightPNG) { - if (this._fb_depth === 1) { + if (this._pixelFormat.Bdepth === 1) { this._fail("Tight protocol handler only implements true color mode"); } @@ -1651,7 +1742,7 @@ var RFB; var handlePalette = function () { var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * this._fb_depth; + var paletteSize = numColors * this._pixelFormat.Bdepth; this._FBU.bytes += paletteSize; if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } @@ -1671,7 +1762,7 @@ var RFB; // Shift ctl, filter id, num colors, palette entries, and clength off this._sock.rQskipBytes(3); - var palette = this._sock.rQshiftBytes(paletteSize); + var palette = this._convert_color(this._sock.rQshiftBytes(paletteSize), this._pixelFormat.Bdepth); this._sock.rQskipBytes(clength[0]); if (raw) { @@ -1684,7 +1775,7 @@ var RFB; var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height); this._display.renderQ_push({ - 'type': 'blitRgb', + 'type': 'blit', 'data': rgb, 'x': this._FBU.x, 'y': this._FBU.y, @@ -1697,7 +1788,7 @@ var RFB; var handleCopy = function () { var raw = false; - var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + var uncompressedSize = this._FBU.width * this._FBU.height * this._pixelFormat.Bdepth; if (uncompressedSize < 12) { raw = true; clength = [0, uncompressedSize]; @@ -1717,8 +1808,8 @@ var RFB; } this._display.renderQ_push({ - 'type': 'blitRgb', - 'data': data, + 'type': 'blit', + 'data': this._convert_color(data, this._pixelFormat.Bdepth), 'x': this._FBU.x, 'y': this._FBU.y, 'width': this._FBU.width, @@ -1751,7 +1842,7 @@ var RFB; switch (cmode) { // fill use fb_depth because TPIXELs drop the padding byte case "fill": // TPIXEL - this._FBU.bytes += this._fb_depth; + this._FBU.bytes += this._pixelFormat.Bdepth; break; case "jpeg": // max clength this._FBU.bytes += 3; @@ -1772,14 +1863,13 @@ var RFB; switch (cmode) { case "fill": this._sock.rQskip8(); // shift off ctl - var color = this._sock.rQshiftBytes(this._fb_depth); this._display.renderQ_push({ 'type': 'fill', 'x': this._FBU.x, 'y': this._FBU.y, 'width': this._FBU.width, 'height': this._FBU.height, - 'color': [color[2], color[1], color[0]] + 'color': this._convert_color(this._sock.rQshiftBytes(this._pixelFormat.Bdepth), this._pixelFormat.Bdepth).reverse(), }); break; case "png": @@ -1854,7 +1944,7 @@ var RFB; var w = this._FBU.width; var h = this._FBU.height; - var pixelslength = w * h * this._fb_Bpp; + var pixelslength = w * h * this._pixelFormat.Bpp; var masklength = Math.floor((w + 7) / 8) * h; this._FBU.bytes = pixelslength + masklength; diff --git a/include/ui.js b/include/ui.js index 4748ff00..c25d445c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -88,7 +88,7 @@ var UI; UI.initSetting('port', port); UI.initSetting('password', ''); UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); + UI.initSetting('convertColor', false); UI.initSetting('cursor', !UI.isTouchDevice); UI.initSetting('shared', true); UI.initSetting('view_only', false); @@ -418,7 +418,7 @@ var UI; UI.closeSettingsMenu(); } else { UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); + UI.updateSetting('convertColor'); if (UI.rfb.get_display().get_cursor_uri()) { UI.updateSetting('cursor'); } else { @@ -473,7 +473,7 @@ var UI; settingsApply: function() { //Util.Debug(">> settingsApply"); UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); + UI.saveSetting('convertColor'); if (UI.rfb.get_display().get_cursor_uri()) { UI.saveSetting('cursor'); } @@ -586,7 +586,7 @@ var UI; //Util.Debug(">> updateVisualState"); $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; + $D('noVNC_convertColor').disabled = connected; if (UI.rfb && UI.rfb.get_display() && UI.rfb.get_display().get_cursor_uri()) { $D('noVNC_cursor').disabled = connected; @@ -673,7 +673,7 @@ var UI; } UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_convertColor(UI.getSetting('convertColor')); UI.rfb.set_local_cursor(UI.getSetting('cursor')); UI.rfb.set_shared(UI.getSetting('shared')); UI.rfb.set_view_only(UI.getSetting('view_only')); diff --git a/tests/test.display.js b/tests/test.display.js index 25adfbea..bbc556f1 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -208,26 +208,14 @@ describe('Display/Canvas Helper', function () { expect(display).to.have.displayed(checked_data); }); - it('should support drawing BGRX blit images with true color via #blitImage', function () { - var data = []; - for (var i = 0; i < 16; i++) { - data[i * 4] = checked_data[i * 4 + 2]; - data[i * 4 + 1] = checked_data[i * 4 + 1]; - data[i * 4 + 2] = checked_data[i * 4]; - data[i * 4 + 3] = checked_data[i * 4 + 3]; - } - display.blitImage(0, 0, 4, 4, data, 0); - expect(display).to.have.displayed(checked_data); - }); - - it('should support drawing RGB blit images with true color via #blitRgbImage', function () { + it('should support drawing RGB blit images with true color via #blitImage', function () { var data = []; for (var i = 0; i < 16; i++) { data[i * 3] = checked_data[i * 4]; data[i * 3 + 1] = checked_data[i * 4 + 1]; data[i * 3 + 2] = checked_data[i * 4 + 2]; } - display.blitRgbImage(0, 0, 4, 4, data, 0); + display.blitImage(0, 0, 4, 4, data, 0); expect(display).to.have.displayed(checked_data); }); @@ -322,13 +310,6 @@ describe('Display/Canvas Helper', function () { expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); }); - it('should draw a blit RGB image on type "blitRgb"', function () { - display.blitRgbImage = sinon.spy(); - display.renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); - expect(display.blitRgbImage).to.have.been.calledOnce; - expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); - }); - it('should copy a region on type "copy"', function () { display.copyImage = sinon.spy(); display.renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d80e3d52..a3a18bdf 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -907,8 +907,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._fb_height).to.equal(84); }); - // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them - it('should set the framebuffer name and call the callback', function () { client.set_onDesktopName(sinon.spy()); send_server_init({ name: 'some name' }, client); @@ -938,14 +936,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('normal'); }); - it('should set the true color mode on the display to the configuration variable', function () { - client.set_true_color(false); - sinon.spy(client._display, 'set_true_color'); - send_server_init({ true_color: 1 }, client); - expect(client._display.set_true_color).to.have.been.calledOnce; - expect(client._display.set_true_color).to.have.been.calledWith(false); - }); - it('should call the resize callback and resize the display', function () { client.set_onFBResize(sinon.spy()); sinon.spy(client._display, 'resize'); @@ -967,25 +957,30 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._mouse.grab).to.have.been.calledOnce; }); - it('should set the BPP and depth to 4 and 3 respectively if in true color mode', function () { - client.set_true_color(true); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(4); - expect(client._fb_depth).to.equal(3); + it('should set the BPP and depth to 4 and 3 respectively if server can send native (true color)', function () { + send_server_init({ true_color: 1, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(4); + expect(client._pixelFormat.Bdepth).to.equal(3); }); - it('should set the BPP and depth to 1 and 1 respectively if not in true color mode', function () { - client.set_true_color(false); - send_server_init({}, client); - expect(client._fb_Bpp).to.equal(1); - expect(client._fb_depth).to.equal(1); + it('should set the BPP and depth to 2 and 2 respectively if server cannot send native (true color)', function () { + client.set_convertColor(true); + send_server_init({ true_color: 1, bpp: 16, depth: 15 }, client); + expect(client._pixelFormat.Bpp).to.equal(2); + expect(client._pixelFormat.Bdepth).to.equal(2); + }); + + it('should set the BPP and depth to 1 and 1 respectively if server cannot send native (not true color)', function () { + client.set_convertColor(true); + send_server_init({ true_color: 0, bpp: 8, depth: 8 }, client); + expect(client._pixelFormat.Bpp).to.equal(1); + expect(client._pixelFormat.Bdepth).to.equal(1); }); // TODO(directxman12): test the various options in this configuration matrix it('should reply with the pixel format, client encodings, and initial update request', function () { - client.set_true_color(true); client.set_local_cursor(false); - var expected = RFB.messages.pixelFormat(4, 3, true); + var expected = RFB.messages.pixelFormat({ bpp: 32, depth: 24, big_endian: false, true_color: true, red_max: 255, green_max: 255, blue_max: 255, red_shift: 16, green_shift: 8, blue_shift: 0 }); expected = expected.concat(RFB.messages.clientEncodings(client._encodings, false, true)); var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 27, h: 32 } ] }; @@ -1036,10 +1031,10 @@ describe('Remote Frame Buffer Protocol Client', function() { }); var target_data_arr = [ - 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + 0xf8, 0x00, 0x00, 255, 0x00, 0xf8, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0x00, 0xf8, 0x00, 255, 0xf8, 0x00, 0x00, 255, 0x00, 0x00, 0xf8, 255, 0x00, 0x00, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, + 0xe8, 0x00, 0xf8, 255, 0x00, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255, 0xa8, 0xe8, 0xf8, 255 ]; var target_data; @@ -1188,22 +1183,95 @@ describe('Remote Frame Buffer Protocol Client', function() { client._display._fb_height = 4; client._display._viewportLoc.w = 4; client._display._viewportLoc.h = 4; - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; }); - it('should handle the RAW encoding', function () { - var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, - { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; - // data is in bgrx - var rects = [ - [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], - [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; - send_fbu_msg(info, rects, client); - expect(client._display).to.have.displayed(target_data); + // warning: the fbupdates *overlap* so you have to send all rects for the numbers + // to even make sense; this means (ironically) no iterative building of your tests + describe('should handle the RAW encoding', function () { + it('24bit depth (RGBX888) @ 32bpp [native]', function () { + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0xf8, 0x00, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0, 0x00, 0x00, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0], + [0xe8, 0x00, 0xf8, 0, 0x00, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0, 0xa8, 0xe8, 0xf8, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('24bit depth (BGRX888) @ 32bpp', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.red_shift = 16; + client._pixelFormat.red_max = 255; + client._pixelFormat.green_shift = 8; + client._pixelFormat.green_max = 255; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 255; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x00, 0x00, 0xf8, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0xf8, 0x00, 0, 0x00, 0x00, 0xf8, 0], + [0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0, 0xf8, 0x00, 0x00, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0], + [0xf8, 0x00, 0xe8, 0, 0xf8, 0xe8, 0x00, 0, 0xf8, 0xe8, 0xa8, 0, 0xf8, 0xe8, 0xa8, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + // for wisdom: perl -e '($w, $r, $g, $b) = @ARGV; $W=2**$w; $nb = $b*($W/256); $ng = $g*($W/256); $nr = $r*($W/256); printf "%f:%f:%f %04x\n", $nr, $ng, $nb, unpack("S", pack("n", ($nr << (2*$w)) | ($ng << (1*$w)) | ($nb << (0*$w))))' 5 0 248 0 + it('15bit depth (BGR555) @ 16bpp', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x00, 0x7c, 0xe0, 0x03, 0xe0, 0x03, 0x00, 0x7c], + [0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57], + [0x1f, 0x74, 0xbf, 0x03, 0xbf, 0x57, 0xbf, 0x57]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('15bit depth (BGR555) @ 16bpp big-endian', function () { + client._convertColor = true; + client._pixelFormat.big_endian = false; + client._pixelFormat.Bpp = 2; + client._pixelFormat.big_endian = true; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + var info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, + { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, + { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; + var rects = [ + [0x7c, 0x00, 0x03, 0xe0, 0x03, 0xe0, 0x7c, 0x00], + [0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x1f], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf], + [0x74, 0x1f, 0x03, 0xbf, 0x57, 0xbf, 0x57, 0xbf]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); }); it('should handle the COPYRECT encoding', function () { @@ -1229,18 +1297,18 @@ describe('Remote Frame Buffer Protocol Client', function() { var rect = []; rect.push32(2); // 2 subrects rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push16(0); // x: 0 rect.push16(0); // y: 0 rect.push16(2); // width: 2 rect.push16(2); // height: 2 - rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push16(2); // x: 2 rect.push16(2); // y: 2 rect.push16(2); // width: 2 @@ -1265,7 +1333,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._display._fb_height = 4; client._display._viewportLoc.w = 4; client._display._viewportLoc.h = 4; - client._fb_Bpp = 4; + client._pixelFormat.Bpp = 4; }); it('should handle a tile with fg, bg specified, normal subrects', function () { @@ -1273,10 +1341,10 @@ describe('Remote Frame Buffer Protocol Client', function() { var rect = []; rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF fg color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push(2); // 2 subrects rect.push(0); // x: 0, y: 0 rect.push(1 | (1 << 4)); // width: 2, height: 2 @@ -1291,9 +1359,9 @@ describe('Remote Frame Buffer Protocol Client', function() { var rect = []; rect.push(0x01); // raw for (var i = 0; i < target_data.length; i += 4) { - rect.push(target_data[i + 2]); - rect.push(target_data[i + 1]); rect.push(target_data[i]); + rect.push(target_data[i + 1]); + rect.push(target_data[i + 2]); rect.push(target_data[i + 3]); } send_fbu_msg(info, [rect], client); @@ -1344,16 +1412,16 @@ describe('Remote Frame Buffer Protocol Client', function() { rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color rect.push(2); // 2 subrects - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF fg color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push(0); // x: 0, y: 0 rect.push(1 | (1 << 4)); // width: 2, height: 2 - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF fg color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push(2 | (2 << 4)); // x: 2, y: 2 rect.push(1 | (1 << 4)); // width: 2, height: 2 send_fbu_msg(info, [rect], client); @@ -1369,10 +1437,10 @@ describe('Remote Frame Buffer Protocol Client', function() { var rect = []; rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); + rect.push(0x00); // becomes 0000ffff --> #0000FF fg color rect.push(0x00); rect.push(0xff); + rect.push(0xff); rect.push(8); // 8 subrects var i; for (i = 0; i < 4; i++) { diff --git a/vnc.html b/vnc.html index adb01576..b6450cda 100644 --- a/vnc.html +++ b/vnc.html @@ -10,7 +10,7 @@ This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: - http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/?host=HOST&port=PORT&encrypt=1&convertColor=1 --> noVNC @@ -154,7 +154,7 @@
  • Encrypt
  • -
  • True Color
  • +
  • Convert Color
  • Local Cursor
  • Clip to Window
  • Shared Mode
  • diff --git a/vnc_auto.html b/vnc_auto.html index b05024e3..f967b16a 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -10,7 +10,7 @@ This file is licensed under the 2-Clause BSD license (see LICENSE.txt). Connect parameters are provided in query string: - http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + http://example.com/?host=HOST&port=PORT&encrypt=1&convertColor=1 --> noVNC @@ -194,7 +194,7 @@ 'encrypt': WebUtil.getQueryVar('encrypt', (window.location.protocol === "https:")), 'repeaterID': WebUtil.getQueryVar('repeaterID', ''), - 'true_color': WebUtil.getQueryVar('true_color', true), + 'convertColor': WebUtil.getQueryVar('convertColor', true), 'local_cursor': WebUtil.getQueryVar('cursor', true), 'shared': WebUtil.getQueryVar('shared', true), 'view_only': WebUtil.getQueryVar('view_only', false), From 24439f5bf2cdf6005927999b601de15f6630ef21 Mon Sep 17 00:00:00 2001 From: Alexander Clouter Date: Sun, 12 Oct 2014 00:27:44 +0100 Subject: [PATCH 3/3] added aten-ikvm support --- README.md | 2 +- include/keysym.js | 77 ++++++++++++++ include/rfb.js | 259 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test.rfb.js | 152 +++++++++++++++++++++++++++ 4 files changed, 489 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1384ba6..92d0f3c2 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ use a WebSockets to TCP socket proxy. There is a python proxy included * UI and Icons : Chris Gordon * Original Logo : Michael Sersen * tight encoding : Michael Tinglof (Mercuri.ca) - * pixel format conversion : [Alexander Clouter](http://www.digriz.org.uk/) + * pixel format conversion and ATEN iKVM support : [Alexander Clouter](http://www.digriz.org.uk/) * Included libraries: * web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js) diff --git a/include/keysym.js b/include/keysym.js index 2b971984..7b49a6d8 100644 --- a/include/keysym.js +++ b/include/keysym.js @@ -376,3 +376,80 @@ XK_udiaeresis = 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIA XK_yacute = 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ XK_thorn = 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ XK_ydiaeresis = 0x00ff; /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ + +var XK2HID = {}; + +// F{1..12} +for (var i = XK_F1; i <= XK_F12; i++) { + XK2HID[i] = 0x3a + (i - XK_F1); +} +// A-Za-z +for (var i = XK_A; i <= XK_Z; i++) { + XK2HID[i] = 0x04 + (i - XK_A); + XK2HID[i + (XK_a - XK_A)] = 0x04 + (i - XK_A); +} +// 1-9 +for (var i = XK_1; i <= XK_9; i++) { + XK2HID[i] = 0x1e + (i - XK_1); +} + +XK2HID[XK_0] = 0x27; +XK2HID[XK_Return] = 0x28; +XK2HID[XK_Escape] = 0x29; +XK2HID[XK_BackSpace] = 0x2a; +XK2HID[XK_Tab] = 0x2b; +XK2HID[XK_space] = 0x2c; +XK2HID[XK_minus] = 0x2d; +XK2HID[XK_equal] = 0x2e; +XK2HID[XK_bracketleft] = 0x2f; +XK2HID[XK_bracketright] = 0x30; +XK2HID[XK_backslash] = 0x31; +XK2HID[XK_semicolon] = 0x33; +XK2HID[XK_apostrophe] = 0x34; +XK2HID[XK_grave] = 0x35; +XK2HID[XK_comma] = 0x36; +XK2HID[XK_period] = 0x37; +XK2HID[XK_slash] = 0x38; + +XK2HID[XK_Print] = 0x46; +XK2HID[XK_Scroll_Lock] = 0x47; +XK2HID[XK_Pause] = 0x48; +XK2HID[XK_Insert] = 0x49; +XK2HID[XK_Home] = 0x4a; +XK2HID[XK_Page_Up] = 0x4b; +XK2HID[XK_Delete] = 0x4c; +XK2HID[XK_End] = 0x4d; +XK2HID[XK_Page_Down] = 0x4e; +XK2HID[XK_Right] = 0x4f; +XK2HID[XK_Left] = 0x50; +XK2HID[XK_Down] = 0x51; +XK2HID[XK_Up] = 0x52; + +XK2HID[XK_Control_L] = 0xe0; +XK2HID[XK_Control_R] = XK2HID[XK_Control_L]; +XK2HID[XK_Shift_L] = 0xe1; +XK2HID[XK_Shift_R] = XK2HID[XK_Shift_L]; +XK2HID[XK_Alt_L] = 0xe2; +XK2HID[XK_Alt_R] = XK2HID[XK_Alt_L]; +XK2HID[XK_Super_L] = 0xe3; +XK2HID[XK_Super_R] = XK2HID[XK_Super_L]; + +XK2HID[XK_Caps_Lock] = 0x39; + +// locale hardcoded hack +XK2HID[XK_less] = XK2HID[XK_comma]; +XK2HID[XK_greater] = XK2HID[XK_period]; +XK2HID[XK_exclam] = XK2HID[XK_1]; +XK2HID[XK_at] = XK2HID[XK_2]; +XK2HID[XK_numbersign] = XK2HID[XK_3]; +XK2HID[XK_dollar] = XK2HID[XK_4]; +XK2HID[XK_percent] = XK2HID[XK_5]; +XK2HID[XK_asciicircum] = XK2HID[XK_6]; +XK2HID[XK_ampersand] = XK2HID[XK_7]; +XK2HID[XK_asterisk] = XK2HID[XK_8]; +XK2HID[XK_parenleft] = XK2HID[XK_9]; +XK2HID[XK_parenright] = XK2HID[XK_0]; +XK2HID[XK_underscore] = XK2HID[XK_minus]; +XK2HID[XK_bar] = XK2HID[XK_backslash]; +XK2HID[XK_quotedbl] = XK2HID[XK_apostrophe]; +XK2HID[XK_asciitilde] = XK2HID[XK_grave]; diff --git a/include/rfb.js b/include/rfb.js index 66e62681..252345d4 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -33,6 +33,7 @@ var RFB; this._rfb_auth_scheme = ''; this._rfb_tightvnc = false; + this._rfb_atenikvm = false; this._rfb_xvp_ver = 0; // In preference order @@ -43,6 +44,7 @@ var RFB; ['HEXTILE', 0x05 ], ['RRE', 0x02 ], ['RAW', 0x00 ], + ['ATEN', 0x59 ], ['DesktopSize', -223 ], ['Cursor', -239 ], @@ -74,6 +76,8 @@ var RFB; subrects: 0, // RRE lines: 0, // RAW tiles: 0, // HEXTILE + aten_len: -1, // ATEN + aten_type: -1, // ATEN bytes: 0, x: 0, y: 0, @@ -120,6 +124,7 @@ var RFB; 'local_cursor': false, // Request locally rendered cursor 'shared': true, // Request shared mode 'view_only': false, // Disable client mouse/keyboard + 'aten_password_sep': ':', // Separator for ATEN iKVM password fields 'xvp_password_sep': '@', // Separator for XVP password fields 'disconnectTimeout': 3, // Time (s) to wait for disconnection 'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection @@ -329,9 +334,12 @@ var RFB; this._FBU.lines = 0; // RAW this._FBU.tiles = 0; // HEXTILE this._FBU.zlibs = []; // TIGHT zlib encoders + this._FBU.aten_len = -1 // ATEN + this._FBU.aten_type = -1; // ATEN this._mouse_buttonMask = 0; this._mouse_arr = []; this._rfb_tightvnc = false; + this._rfb_atenikvm = false; // Clear the per connection encoding stats var i; @@ -683,6 +691,36 @@ var RFB; }, // authentication + _negotiate_aten_auth: function () { + var aten_sep = this._aten_password_sep; + var aten_auth = this._rfb_password.split(aten_sep); + if (aten_auth.length < 2) { + this._updateState('password', 'ATEN iKVM credentials required (user' + aten_sep + + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } + + this._rfb_atenikvm = true; + + if (this._rfb_tightvnc) { + this._rfb_tightvnc = false; + } else { + this._sock.rQskipBytes(4); + } + + this._sock.rQskipBytes(16); + + var username = aten_auth[0]; + username += Array(24-username.length+1).join("\x00"); + var password = aten_auth.slice(1).join(aten_sep); + password += Array(24-password.length+1).join("\x00"); + + this._sock.send_string(username+password); + this._updateState("SecurityResult"); + return true; + }, + _negotiate_xvp_auth: function () { var xvp_sep = this._xvp_password_sep; var xvp_auth = this._rfb_password.split(xvp_sep); @@ -750,6 +788,7 @@ var RFB; if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation if (this._sock.rQwait("num tunnels", 4)) { return false; } var numTunnels = this._sock.rQshift32(); + if (this._rfb_version === 3.8 && (numTunnels & 0xffff0ff0) >>> 0 === 0xaff90fb0) { return this._negotiate_aten_auth(); } if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } this._rfb_tightvnc = true; @@ -765,6 +804,12 @@ var RFB; var subAuthCount = this._sock.rQshift32(); if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } + // Newer X10 Supermicro motherboards get here + if (this._rfb_version === 3.8 && numTunnels === 0 && subAuthCount === 0) { + Util.Warn("Newer ATEN iKVM detected, you may get an 'unsupported encoding 87'"); + return this._negotiate_aten_auth(); + } + var clientSupportedTypes = { 'STDVNOAUTH__': 1, 'STDVVNCAUTH_': 2 @@ -851,6 +896,7 @@ var RFB; _negotiate_server_init: function () { if (this._sock.rQwait("server initialization", 24)) { return false; } + if (this._rfb_atenikvm && this._sock.rQwait("ATEN server initialization", 36)) { return false; } /* Screen size */ this._fb_width = this._sock.rQshift16(); @@ -878,6 +924,14 @@ var RFB; if (this._sock.rQwait('server init name', name_length, 24)) { return false; } this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + if (this._rfb_atenikvm) { + this._sock.rQskipBytes(8); // unknown + this._sock.rQskip8(); // IKVMVideoEnable + this._sock.rQskip8(); // IKVMKMEnable + this._sock.rQskip8(); // IKVMKickEnable + this._sock.rQskip8(); // VUSBEnable + } + if (this._rfb_tightvnc) { if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } // In TightVNC mode, ServerInit message is extended @@ -924,6 +978,83 @@ var RFB; this._convertColor = true; } + // ATEN 'wisdom' from chicken-aten-ikvm:lens/lens.rb + // tested against the following Supermicro motherboards + // (use 'dmidecode -s baseboard-product-name' for model): + // - X7SPA-F + // - X8DTL + // - X8SIE-F + // - X9SCL/X9SCM + // - X9SCM-F + // - X9DRD-iF + // - X9SRE/X9SRE-3F/X9SRi/X9SRi-3F + // - X9DRL-3F/X9DRL-6F + // - X10SLD + // + // Not supported (uses encoding 87): + // - X10SL7-F + // - X10SLD-F + // - X10SLM-F + // - X10SLE + // + // Simply does not work: + // Hermon (WPMC450) [hangs at login]: + // - X7SB3-F + // - X8DTU-F + // - X8STi-3F + // Peppercon (Raritan/Kira100) [connection refused]: + // - X7SBi + // + // Thanks to Brian Rak and Erik Smit for testing + if (this._rfb_atenikvm) { + // we do not know the resolution till the first fbupdate so go large + // although, not necessary, saves a pointless full screen refresh + this._fb_width = 10000; + this._fb_height = 10000; + + // lies about what it supports + Util.Warn("ATEN iKVM lies and only does 15 bit depth with RGB555"); + this._convertColor = true; + this._pixelFormat.bpp = 16; + this._pixelFormat.depth = 15; + this._pixelFormat.red_max = (1 << 5) - 1; + this._pixelFormat.green_max = (1 << 5) - 1; + this._pixelFormat.blue_max = (1 << 5) - 1; + this._pixelFormat.red_shift = 10; + this._pixelFormat.green_shift = 5; + this._pixelFormat.blue_shift = 0; + + // changes to protocol format + RFB.messages.keyEvent = function (keysym, down) { + if (XK2HID[keysym] === undefined) { + Util.Warn("XK2HID["+keysym+"] returns undefined"); + return; + } + + var arr = [4]; + arr.push8(0); + arr.push8(down); + arr.push16(0); + arr.push32(XK2HID[keysym]); + arr.push32(0); + arr.push32(0); + arr.push8(0); + return arr; + }; + RFB.messages.pointerEvent = function (x, y, mask) { + var arr = [5]; + arr.push8(0); + arr.push8(mask); + arr.push16(x); + arr.push16(y); + arr.push32(0); + arr.push32(0); + arr.push16(0); + arr.push8(0); + return arr; + }; + } + if (this._convertColor) this._display.set_true_color(this._pixelFormat.true_color); this._onFBResize(this, this._fb_width, this._fb_height); @@ -1093,6 +1224,42 @@ var RFB; msg_type = this._sock.rQshift8(); } + if (this._rfb_atenikvm) { + switch (msg_type) { + case 4: // Front Ground Event + Util.Debug("ATEN iKVM Front Ground Event"); + this._sock.rQskipBytes(20); + return true; + + case 22: // Keep Alive Event + Util.Debug("ATEN iKVM Keep Alive Event"); + this._sock.rQskipBytes(1); + return true; + + case 51: // Video Get Info + Util.Debug("ATEN iKVM Video Get Info"); + this._sock.rQskipBytes(4); + return true; + + case 55: // Mouse Get Info + Util.Debug("ATEN iKVM Mouse Get Info"); + this._sock.rQskipBytes(2); + return true; + + case 57: // Session Message + Util.Debug("ATEN iKVM Session Message"); + this._sock.rQskipBytes(4); // u32 + this._sock.rQskipBytes(4); // u32 + this._sock.rQskipBytes(256); + return true; + + case 60: // Get Viewer Lang + Util.Debug("ATEN iKVM Get Viewer Lang"); + this._sock.rQskipBytes(8); + return true; + } + } + switch (msg_type) { case 0: // FramebufferUpdate var ret = this._framebufferUpdate(); @@ -1166,6 +1333,11 @@ var RFB; this._FBU.encoding); return false; } + + // ATEN uses 0x00 even when it is meant to be 0x59 + if (this._rfb_atenikvm && this._FBU.encoding === 0x00) { + this._FBU.encoding = 0x59; + } } this._timing.last_fbu = (new Date()).getTime(); @@ -1272,6 +1444,7 @@ var RFB; ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor ['shared', 'rw', 'bool'], // Request shared mode ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['aten_password_sep', 'rw', 'str'], // Separator for ATEN iKVM password fields ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection @@ -1967,6 +2140,92 @@ var RFB; compress_lo: function () { Util.Error("Server sent compress level pseudo-encoding"); + }, + + ATEN: function () { + if (this._FBU.aten_len === - 1) { + this._FBU.bytes = 8; + if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; } + this._FBU.bytes = 0; + this._sock.rQskipBytes(4); + this._FBU.aten_len = this._sock.rQshift32(); + + if (this._FBU.width === 64896 && this._FBU.height === 65056) { + Util.Info("ATEN iKVM screen is probably off"); + if (this._FBU.aten_len !== 10) { + Util.Debug(">> ATEN iKVM screen off (aten_len="+this._FBU.aten_len+")"); + this._fail('expected aten_len to be 10 when screen is off'); + } + this._FBU.aten_len -= 10; + return true; + } + if (this._fb_width !== this._FBU.width && this._fb_height !== this._FBU.height) { + Util.Debug(">> ATEN resize desktop"); + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + Util.Debug("<< ATEN resize desktop"); + } + } + + if (this._FBU.aten_type === -1) { + this._FBU.bytes = 10; + if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; } + this._FBU.bytes = 0; + this._FBU.aten_type = this._sock.rQshift8(); + this._sock.rQskip8(); + + this._sock.rQshift32(); // number of subrects + if (this._FBU.aten_len !== this._sock.rQshift32()) { + return this._fail('ATEN RAW len mis-match'); + } + this._FBU.aten_len -= 10; + } + + while (this._FBU.aten_len > 0) { + switch (this._FBU.aten_type) { + case 0: // Subrects + this._FBU.bytes = 6 + (16 * 16 * this._pixelFormat.Bpp); // at least a subrect + if (this._sock.rQwait("ATEN", this._FBU.bytes)) { return false; } + var a = this._sock.rQshift16(); + var b = this._sock.rQshift16(); + var y = this._sock.rQshift8(); + var x = this._sock.rQshift8(); + this._display.renderQ_push({ + 'type': 'blit', + 'data': this._convert_color(this._sock.rQshiftBytes(this._FBU.bytes - 6)), + 'x': x * 16, + 'y': y * 16, + 'width': 16, + 'height': 16 + }); + this._FBU.aten_len -= this._FBU.bytes; + this._FBU.bytes = 0; + break; + case 1: // RAW + var olines = (this._FBU.lines === 0) ? this._FBU.height : this._FBU.lines; + this._encHandlers.RAW(); + this._FBU.aten_len -= (olines - this._FBU.lines) * this._FBU.width * this._pixelFormat.Bpp; + if (this._FBU.bytes > 0) return false; + break; + default: + return this._fail('unknown ATEN type: '+this._FBU.aten_type); + } + } + + if (this._FBU.aten_len < 0) { + this._fail('aten_len dropped below zero'); + } + + if (this._FBU.aten_type === 0) { + this._FBU.rects--; + } + + this._FBU.aten_len = -1; + this._FBU.aten_type = -1; + + return true; } }; })(); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index a3a18bdf..c3fea0a2 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -789,6 +789,51 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('failed'); }); }); + + describe('ATEN iKVM Authentication Handler', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + client._rfb_version = 3.8; + send_security(16, client); + client._sock._websocket._get_sent_data(); // skip the security reply + client._rfb_password = 'test1:test2'; + }); + + var auth = [ + 116, 101, 115, 116, 49, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 116, 101, 115, 116, 50, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0]; + + it('via old style method', function () { + client._sock._websocket._receive_data(new Uint8Array([ + 0xaf, 0xf9, 0x0f, 0xb0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.false; + expect(client._rfb_atenikvm).to.be.true; + expect(client._sock).to.have.sent(auth); + }); + + it('via new style method', function () { + client._sock._websocket._receive_data(new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.false; + expect(client._rfb_atenikvm).to.be.true; + expect(client._sock).to.have.sent(auth); + }); + }); }); describe('SecurityResult', function () { @@ -936,6 +981,23 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('normal'); }); + it('should handle an ATEN iKVM server initialization', function () { + client._rfb_atenikvm = true; + send_server_init({ true_color: 1, bpp: 32 }, client); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); + expect(client._pixelFormat.bpp).to.equal(16); + expect(client._pixelFormat.depth).to.equal(15); + expect(client._pixelFormat.red_max).to.equal(31); + expect(client._pixelFormat.green_max).to.equal(31); + expect(client._pixelFormat.blue_max).to.equal(31); + expect(client._pixelFormat.red_shift).to.equal(10); + expect(client._pixelFormat.green_shift).to.equal(5); + expect(client._pixelFormat.blue_shift).to.equal(0); + expect(client._pixelFormat.Bpp).to.equal(2); + expect(client._pixelFormat.Bdepth).to.equal(2); + expect(client._rfb_state).to.equal('normal'); + }); + it('should call the resize callback and resize the display', function () { client.set_onFBResize(sinon.spy()); sinon.spy(client._display, 'resize'); @@ -1477,6 +1539,96 @@ describe('Remote Frame Buffer Protocol Client', function() { // TODO(directxman12): test this }); + describe('the ATEN encoding handler', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._rfb_atenikvm = true; + // start large, then go small + client._fb_width = 10000; + client._fb_height = 10000; + client._display._fb_width = 10000; + client._display._fb_height = 10000; + client._display._viewportLoc.w = 10000; + client._display._viewportLoc.h = 10000; + client._convertColor = true; + client._pixelFormat.Bpp = 2; + client._pixelFormat.big_endian = false; + client._pixelFormat.red_shift = 10; + client._pixelFormat.red_max = 31; + client._pixelFormat.green_shift = 5; + client._pixelFormat.green_max = 31; + client._pixelFormat.blue_shift = 0; + client._pixelFormat.blue_max = 31; + }); + + var aten_target_data_arr = [0xa8, 0xe8, 0xf8, 0xff]; + var aten_target_data; + + before(function () { + for (var i = 0; i < 10; i++) { + aten_target_data_arr = aten_target_data_arr.concat(aten_target_data_arr); + } + aten_target_data = new Uint8Array(aten_target_data_arr); + }); + + it('should handle subtype subrect encoding', function () { + var info = [{ x: 0, y: 0, width: 32, height: 32, encoding: 0x59 }]; + var rect = []; + + rect.push32(0); // padding + rect.push32(2082); // 10 + 32x32x2Bpp + 6*(num of subrects) + + rect.push8(0); // type + rect.push8(0); // padding + + rect.push32(4); // num of subrects (32/16)*(32/16) + rect.push32(2082); // length (again) + + for (var y = 0; y < 2; y++) { + for (var x = 0; x < 2; x++) { + rect.push16(0); // a + rect.push16(0); // b + rect.push8(y); + rect.push8(x); + + for (var i = 0; i < 16*16; i++) { + rect.push16(0xbf57); + } + } + } + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(aten_target_data); + }); + + it('should handle subtype RAW encoding', function () { + // do not use encoding=0x59 here, as rfb.js should override it + var info = [{ x: 0, y: 0, width: 32, height: 32, encoding: 0x00 }]; + var rect = []; + + rect.push32(0); // padding + rect.push32(2058); // 10 + 32x32x2Bpp + + rect.push8(1); // type + rect.push8(0); // padding + + rect.push32(0); // padding + rect.push32(2058); // length (again) + + for (var i = 0; i < 32*32; i++) { + rect.push16(0xbf57); + } + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(aten_target_data); + }); + }); + it('should handle the DesktopSize pseduo-encoding', function () { client.set_onFBResize(sinon.spy()); sinon.spy(client._display, 'resize');