diff --git a/README.md b/README.md index b5679cdd9..92d0f3c22 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 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/display.js b/include/display.js index a42b854a0..45044f880 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/keysym.js b/include/keysym.js index 2b971984a..7b49a6d87 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 59fd785d4..252345d46 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, @@ -85,8 +89,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,10 +120,11 @@ 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 + '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 @@ -330,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; @@ -684,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); @@ -751,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; @@ -766,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 @@ -852,23 +896,24 @@ 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(); - 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(); - - 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.bpp = this._sock.rQshift8(); + this._pixelFormat.depth = this._sock.rQshift8(); + this._pixelFormat.big_endian = (this._sock.rQshift8() === 1) ? true : false; + this._pixelFormat.true_color = (this._sock.rQshift8() === 1) ? true : false; + + this._pixelFormat.red_max = this._sock.rQshift16(); + this._pixelFormat.green_max = this._sock.rQshift16(); + this._pixelFormat.blue_max = this._sock.rQshift16(); + this._pixelFormat.red_shift = this._sock.rQshift8(); + this._pixelFormat.green_shift = this._sock.rQshift8(); + this._pixelFormat.blue_shift = this._sock.rQshift8(); this._sock.rQskipBytes(3); // padding // NB(directxman12): we don't want to call any callbacks or print messages until @@ -879,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 @@ -907,56 +960,167 @@ 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; + } + + // 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; + } - this._display.set_true_color(this._true_color); + var arr = [4]; + arr.push8(0); + arr.push8(down); + arr.push16(0); + arr.push32(XK2HID[keysym]); + arr.push32(0); + arr.push32(0); + arr.push8(0); + return arr; + }; + RFB.messages.pointerEvent = function (x, y, mask) { + var arr = [5]; + arr.push8(0); + arr.push8(mask); + arr.push16(x); + arr.push16(y); + arr.push32(0); + arr.push32(0); + arr.push16(0); + arr.push8(0); + return arr; + }; + } + + if (this._convertColor) + this._display.set_true_color(this._pixelFormat.true_color); this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); this._keyboard.grab(); this._mouse.grab(); - 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; + } + } + + 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'); } - 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)); + 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; @@ -1060,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(); @@ -1133,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(); @@ -1184,16 +1389,62 @@ 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 + ['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 @@ -1261,23 +1512,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 +1617,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 +1664,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 +1683,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 +1723,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 +1756,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 +1783,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 +1831,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 +1915,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 +1935,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 +1948,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 +1961,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 +1981,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 +2015,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 +2036,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 +2117,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; @@ -1877,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/include/ui.js b/include/ui.js index 4748ff00d..c25d445c0 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/package.json b/package.json index 63c266621..81a2eddf3 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", diff --git a/tests/test.display.js b/tests/test.display.js index 25adfbeac..bbc556f1f 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 d80e3d523..c3fea0a2f 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 () { @@ -907,8 +952,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,12 +981,21 @@ 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 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 () { @@ -967,25 +1019,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 +1093,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 +1245,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 +1359,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 +1395,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 +1403,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 +1421,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 +1474,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 +1499,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++) { @@ -1409,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'); diff --git a/vnc.html b/vnc.html index adb01576f..b6450cda8 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 -->