From 960752ea5308ea5aacd5a350b2ad45dcf999a608 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 19 Aug 2014 13:49:55 +0200 Subject: [PATCH 001/527] fixes an error that was made in the merge with the last sync with websockify. --- include/websock.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/include/websock.js b/include/websock.js index 01a24c3f..0e4718ab 100644 --- a/include/websock.js +++ b/include/websock.js @@ -262,7 +262,7 @@ function on(evt, handler) { eventHandlers[evt] = handler; } -function init(protocols) { +function init(protocols, ws_schema) { rQ = []; rQi = 0; sQ = []; @@ -277,12 +277,13 @@ function init(protocols) { ('set' in Uint8Array.prototype)) { bt = true; } - - // Check for full binary type support in WebSockets - // TODO: this sucks, the property should exist on the prototype - // but it does not. + // Check for full binary type support in WebSocket + // Inspired by: + // https://github.com/Modernizr/Modernizr/issues/370 + // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js try { - if (bt && ('binaryType' in (new WebSocket("ws://localhost:17523")))) { + if (bt && ('binaryType' in WebSocket.prototype || + !!(new WebSocket(ws_schema + '://.').binaryType))) { Util.Info("Detected binaryType support in WebSockets"); wsbt = true; } @@ -325,7 +326,8 @@ function init(protocols) { } function open(uri, protocols) { - protocols = init(protocols); + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + protocols = init(protocols, ws_schema); if (test_mode) { websocket = {}; From 91127741bea2238277a426a382fec3740103d606 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 15 Sep 2014 16:44:36 -0400 Subject: [PATCH 002/527] Support running all tests from the root directory Previously, if you did not specify a tests file, you had to be in the 'tests' directory for the "run all tests" functionality to work. Now it will work in any directory. --- tests/run_from_console.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run_from_console.js b/tests/run_from_console.js index 84ebd10a..0d4cc8f5 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -23,6 +23,7 @@ program if (program.tests.length === 0) { program.tests = fs.readdirSync(__dirname).filter(function(f) { return (/^test\.(\w|\.|-)+\.js$/).test(f); }); + program.tests = program.tests.map(function (f) { return path.resolve(__dirname, f); }); // add full paths in console.log('using files %s', program.tests); } From 95eb681bbb609c1fe6a7dace694afa6ed00cf4d3 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 20 May 2014 17:03:40 -0400 Subject: [PATCH 003/527] Support the "NOTUNNEL" tunnel type for TightVNC Previously, tight auth was supported without any support for tunnels, even the no-op tunnel. No, the no-op tunnel type is supported. --- include/rfb.js | 47 ++++++++++++++++++++++++++++++++++++++--------- include/util.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 3b748ba1..75034ca5 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -322,6 +322,7 @@ init_vars = function() { FBU.zlibs = []; // TIGHT zlib encoders mouse_buttonMask = 0; mouse_arr = []; + rfb_tightvnc = false; // Clear the per connection encoding stats for (i=0; i < encodings.length; i+=1) { @@ -775,23 +776,51 @@ init_msg = function() { updateState('SecurityResult'); return; case 16: // TightVNC Security Type - if (ws.rQwait("num tunnels", 4)) { return false; } - var numTunnels = ws.rQshift32(); - //console.log("Number of tunnels: "+numTunnels); + if (!rfb_tightvnc) { + // we haven't been through this before, so assume + // we need to check for tunnel support + if (ws.rQwait("num tunnels", 4)) { return false; } + var numTunnels = ws.rQshift32(); + //console.log("Number of tunnels: "+numTunnels); - rfb_tightvnc = true; + rfb_tightvnc = true; - if (numTunnels != 0) - { - fail("Protocol requested tunnels, not currently supported. numTunnels: " + numTunnels); - return; - } + var clientSupportedTunnelTypes = { + 0: { vender: 'TGHT', signature: 'NOTUNNEL' } + }; + + if (numTunnels > 0) { + var serverSupportedTunnelTypes = {} + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + if (ws.rQwait("tunnel " + i.toString() + " capability", 16)) { return false; } + + var cap_code = ws.rQshift32(); + var cap_vendor = ws.rQshiftStr(4); + var cap_signature = ws.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose an auth type + for (var cap_code in clientSupportedTunnelTypes) { + if (serverSupportedTunnelTypes[cap_code] != undefined) { + // TODO(directxman12): convert capcode back to U32 + ws.send([0,0,0,cap_code]); + return; + } + } + + fail("No supported tunnel types"); + return; + } + } // otherwise, we've dealt with tunnels, so jump right into auth var clientSupportedTypes = { 'STDVNOAUTH__': 1, 'STDVVNCAUTH_': 2 }; + var serverSupportedTypes = []; if (ws.rQwait("sub auth count", 4)) { return false; } diff --git a/include/util.js b/include/util.js index 05c1ac32..ea14b5b6 100644 --- a/include/util.js +++ b/include/util.js @@ -84,6 +84,47 @@ if (!Array.prototype.indexOf) }; } +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys +if (!Object.keys) { + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} // // requestAnimationFrame shim with setTimeout fallback From f8e9b9f1bf45c84deb0d9d939f9a2ac809afa187 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 6 May 2014 15:11:31 -0400 Subject: [PATCH 004/527] Cleanup and Test: base64.js This is the first commit in a series of commits which improve the readability of some of the code and add tests. File: base64.js Tests Added: True Changes: - Improved indentation - Fixed JSHint errors - Moved loop variables to be declared in the loop for better readability (N.B. Javascript does not have block scoping, so the variables are still technically available outside the loop -- it just makes the code clearer to place them inside the loop, since they are only used there) --- include/base64.js | 194 +++++++++++++++++++++---------------------- tests/test.base64.js | 33 ++++++++ 2 files changed, 129 insertions(+), 98 deletions(-) create mode 100644 tests/test.base64.js diff --git a/include/base64.js b/include/base64.js index 5a6890ad..651fbadc 100644 --- a/include/base64.js +++ b/include/base64.js @@ -4,112 +4,110 @@ // From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js -/*jslint white: false, bitwise: false, plusplus: false */ +/*jslint white: false */ /*global console */ var Base64 = { + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad : '=', -/* Convert data (an array of integers) to a Base64 string. */ -toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), -base64Pad : '=', + encode: function (data) { + "use strict"; + var result = ''; + var toBase64Table = Base64.toBase64Table; + var length = data.length; + var lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. -encode: function (data) { - "use strict"; - var result = ''; - var toBase64Table = Base64.toBase64Table; - var length = data.length - var lengthpad = (length%3); - var i = 0, j = 0; - // Convert every three bytes to 4 ascii characters. - /* BEGIN LOOP */ - for (i = 0; i < (length - 2); i += 3) { - result += toBase64Table[data[i] >> 2]; - result += toBase64Table[((data[i] & 0x03) << 4) + (data[i+1] >> 4)]; - result += toBase64Table[((data[i+1] & 0x0f) << 2) + (data[i+2] >> 6)]; - result += toBase64Table[data[i+2] & 0x3f]; - } - /* END LOOP */ - - // Convert the remaining 1 or 2 bytes, pad out to 4 characters. - if (lengthpad === 2) { - j = length - lengthpad; - result += toBase64Table[data[j] >> 2]; - result += toBase64Table[((data[j] & 0x03) << 4) + (data[j+1] >> 4)]; - result += toBase64Table[(data[j+1] & 0x0f) << 2]; - result += toBase64Table[64]; - } else if (lengthpad === 1) { - j = length - lengthpad; - result += toBase64Table[data[j] >> 2]; - result += toBase64Table[(data[j] & 0x03) << 4]; - result += toBase64Table[64]; - result += toBase64Table[64]; - } - - return result; -}, - -/* Convert Base64 data to a string */ -toBinaryTable : [ - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, - 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, - 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, - -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, - 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 -], - -decode: function (data, offset) { - "use strict"; - offset = typeof(offset) !== 'undefined' ? offset : 0; - var toBinaryTable = Base64.toBinaryTable; - var base64Pad = Base64.base64Pad; - var result, result_length, idx, i, c, padding; - var leftbits = 0; // number of bits decoded, but yet to be appended - var leftdata = 0; // bits decoded, but yet to be appended - var data_length = data.indexOf('=') - offset; - - if (data_length < 0) { data_length = data.length - offset; } - - /* Every four characters is 3 resulting numbers */ - result_length = (data_length >> 2) * 3 + Math.floor((data_length%4)/1.5); - result = new Array(result_length); - - // Convert one by one. - /* BEGIN LOOP */ - for (idx = 0, i = offset; i < data.length; i++) { - c = toBinaryTable[data.charCodeAt(i) & 0x7f]; - padding = (data.charAt(i) === base64Pad); - // Skip illegal characters and whitespace - if (c === -1) { - console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); - continue; + for (var i = 0; i < (length - 2); i += 3) { + result += toBase64Table[data[i] >> 2]; + result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += toBase64Table[data[i + 2] & 0x3f]; } - - // Collect data into leftdata, update bitcount - leftdata = (leftdata << 6) | c; - leftbits += 6; - // If we have 8 or more bits, append 8 bits to the result - if (leftbits >= 8) { - leftbits -= 8; - // Append if not padding. - if (!padding) { - result[idx++] = (leftdata >> leftbits) & 0xff; + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + var j = 0; + if (lengthpad === 2) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += toBase64Table[64]; + } else if (lengthpad === 1) { + j = length - lengthpad; + result += toBase64Table[data[j] >> 2]; + result += toBase64Table[(data[j] & 0x03) << 4]; + result += toBase64Table[64]; + result += toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + /* jshint -W013 */ + toBinaryTable : [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 + ], + /* jshint +W013 */ + + decode: function (data, offset) { + "use strict"; + offset = typeof(offset) !== 'undefined' ? offset : 0; + var toBinaryTable = Base64.toBinaryTable; + var base64Pad = Base64.base64Pad; + var result, result_length; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + var data_length = data.indexOf('=') - offset; + + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + result = new Array(result_length); + + // Convert one by one. + for (var idx = 0, i = offset; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data.charAt(i) === base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; } - leftdata &= (1 << leftbits) - 1; } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; } - /* END LOOP */ - - // If there are any bits left, the base64 string was corrupted - if (leftbits) { - throw {name: 'Base64-Error', - message: 'Corrupted base64 string'}; - } - - return result; -} - }; /* End of Base64 namespace */ diff --git a/tests/test.base64.js b/tests/test.base64.js new file mode 100644 index 00000000..b2646a0f --- /dev/null +++ b/tests/test.base64.js @@ -0,0 +1,33 @@ +// requires local modules: base64 +var assert = chai.assert; +var expect = chai.expect; + +describe('Base64 Tools', function() { + "use strict"; + + var BIN_ARR = new Array(256); + for (var i = 0; i < 256; i++) { + BIN_ARR[i] = i; + } + + var B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="; + + + describe('encode', function() { + it('should encode a binary string into Base64', function() { + var encoded = Base64.encode(BIN_ARR); + expect(encoded).to.equal(B64_STR); + }); + }); + + describe('decode', function() { + it('should decode a Base64 string into a normal string', function() { + var decoded = Base64.decode(B64_STR); + expect(decoded).to.deep.equal(BIN_ARR); + }); + + it('should throw an error if we have extra characters at the end of the string', function() { + expect(function () { Base64.decode(B64_STR+'abcdef'); }).to.throw(Error); + }); + }); +}); From fb64ed213584423475a262631b6be2e2a157c628 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 6 May 2014 15:25:05 -0400 Subject: [PATCH 005/527] Cleanup: DES code File: des.js Tests Added: False Changes: - Fixed JSHint Errors - Improved whitespace around operators --- include/des.js | 349 +++++++++++++++++++++++++------------------------ 1 file changed, 176 insertions(+), 173 deletions(-) diff --git a/include/des.js b/include/des.js index 1f952851..ecbc819e 100644 --- a/include/des.js +++ b/include/des.js @@ -75,199 +75,202 @@ * fine Java utilities: http://www.acme.com/java/ */ -"use strict"; -/*jslint white: false, bitwise: false, plusplus: false */ +/* jslint white: false */ function DES(passwd) { + "use strict"; -// Tables, permutations, S-boxes, etc. -var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, - 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, - 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], - z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, - keys = []; + // Tables, permutations, S-boxes, etc. + // jshint -W013 + var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], + totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], + z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, + keys = []; -a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; -SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, - z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, - a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, - c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; -a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; -SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, - a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, - z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, - z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; -a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; -SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, - b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, - c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, - b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; -a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; -SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, - z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, - b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, - c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; -a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; -SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, - a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, - z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, - c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; -a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; -SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, - z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, - b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, - a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; -a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; -SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, - b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, - b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, - z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; -a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; -SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, - c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, - a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, - z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint -W015 + a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; + SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, + z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, + a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, + c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; + a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; + SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, + a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, + z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, + z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; + a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; + SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, + b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, + c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, + b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; + a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; + SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, + z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, + b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, + c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; + a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; + SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, + a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, + z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, + c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; + a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; + SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, + z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, + b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, + a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; + a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; + SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, + b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, + b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, + z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; + a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; + SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, + c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, + a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, + z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + // jshint +W013,+W015 -// Set the key. -function setKeys(keyBlock) { - var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], - raw0, raw1, rawi, KnLi; + // Set the key. + function setKeys(keyBlock) { + var i, j, l, m, n, o, pc1m = [], pcr = [], kn = [], + raw0, raw1, rawi, KnLi; - for (j = 0, l = 56; j < 56; ++j, l-=8) { - l += l<-5 ? 65 : l<-3 ? 31 : l<-1 ? 63 : l===27 ? 35 : 0; // PC1 - m = l & 0x7; - pc1m[j] = ((keyBlock[l >>> 3] & (1<>> 3] & (1<>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; } } - // cookey - for (i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { - raw0 = kn[rawi++]; - raw1 = kn[rawi++]; - keys[KnLi] = (raw0 & 0x00fc0000) << 6; - keys[KnLi] |= (raw0 & 0x00000fc0) << 10; - keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; - keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; - ++KnLi; - keys[KnLi] = (raw0 & 0x0003f000) << 12; - keys[KnLi] |= (raw0 & 0x0000003f) << 16; - keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; - keys[KnLi] |= (raw1 & 0x0000003f); - ++KnLi; - } -} + // Encrypt 8 bytes of text + function enc8(text) { + var i = 0, b = text.slice(), fval, keysi = 0, + l, r, x; // left, right, accumulator -// Encrypt 8 bytes of text -function enc8(text) { - var i = 0, b = text.slice(), fval, keysi = 0, - l, r, x; // left, right, accumulator + // Squash 8 bytes to 2 ints + l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - // Squash 8 bytes to 2 ints - l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); - x = ((l >>> 4) ^ r) & 0x0f0f0f0f; - r ^= x; - l ^= (x << 4); - x = ((l >>> 16) ^ r) & 0x0000ffff; - r ^= x; - l ^= (x << 16); - x = ((r >>> 2) ^ l) & 0x33333333; - l ^= x; - r ^= (x << 2); - x = ((r >>> 8) ^ l) & 0x00ff00ff; - l ^= x; - r ^= (x << 8); - r = (r << 1) | ((r >>> 31) & 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 1) | ((l >>> 31) & 1); + for (i = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; + } - for (i = 0; i < 8; ++i) { - x = (r << 28) | (r >>> 4); - x ^= keys[keysi++]; - fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = r ^ keys[keysi++]; - fval |= SP8[x & 0x3f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - l ^= fval; - x = (l << 28) | (l >>> 4); - x ^= keys[keysi++]; - fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = l ^ keys[keysi++]; - fval |= SP8[x & 0x0000003f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - r ^= fval; + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; } - r = (r << 31) | (r >>> 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 31) | (l >>> 1); - x = ((l >>> 8) ^ r) & 0x00ff00ff; - r ^= x; - l ^= (x << 8); - x = ((l >>> 2) ^ r) & 0x33333333; - r ^= x; - l ^= (x << 2); - x = ((r >>> 16) ^ l) & 0x0000ffff; - l ^= x; - r ^= (x << 16); - x = ((r >>> 4) ^ l) & 0x0f0f0f0f; - l ^= x; - r ^= (x << 4); - - // Spread ints to bytes - x = [r, l]; - for (i = 0; i < 8; i++) { - b[i] = (x[i>>>2] >>> (8*(3 - (i%4)))) % 256; - if (b[i] < 0) { b[i] += 256; } // unsigned + // Encrypt 16 bytes of text using passwd as key + function encrypt(t) { + return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); } - return b; -} -// Encrypt 16 bytes of text using passwd as key -function encrypt(t) { - return enc8(t.slice(0,8)).concat(enc8(t.slice(8,16))); -} - -setKeys(passwd); // Setup keys -return {'encrypt': encrypt}; // Public interface + setKeys(passwd); // Setup keys + return {'encrypt': encrypt}; // Public interface } // function DES From d21cd6c16453eec8ec4b218dff6dffecf55aa518 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 21 May 2014 14:54:28 -0400 Subject: [PATCH 006/527] Cleanup: Util code File: util.js Tests Added: True (partial -- for logging and array push methods) Changes: - Fixed JSHint Errors (indentation, semicolons, global "use strict") - Made browser detection methods more readable - added some newline characters when appropriate for readability - throw Errors not strings! - Removed conf_defaults, and added make_properties and set_defaults instead (see below) The removal of conf_defaults and switch to make_properties and set_defaults is to facilitate the switch over to normal Javascript constructors instead of Crockford-style constructors. Now, methods are added to the objects prototype (and thus make properties is called outside the constructor). --- include/util.js | 609 +++++++++++++++++++++++++++------------------ tests/test.util.js | 105 ++++++++ 2 files changed, 478 insertions(+), 236 deletions(-) create mode 100644 tests/test.util.js diff --git a/include/util.js b/include/util.js index ea14b5b6..c145d5a6 100644 --- a/include/util.js +++ b/include/util.js @@ -6,9 +6,8 @@ * See README.md for usage and integration instructions. */ -"use strict"; -/*jslint bitwise: false, white: false */ -/*global window, console, document, navigator, ActiveXObject */ +/* jshint white: false, nonstandard: true */ +/*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */ // Globals defined here var Util = {}; @@ -19,129 +18,161 @@ var Util = {}; */ Array.prototype.push8 = function (num) { + "use strict"; this.push(num & 0xFF); }; Array.prototype.push16 = function (num) { + "use strict"; this.push((num >> 8) & 0xFF, - (num ) & 0xFF ); + num & 0xFF); }; Array.prototype.push32 = function (num) { + "use strict"; this.push((num >> 24) & 0xFF, (num >> 16) & 0xFF, (num >> 8) & 0xFF, - (num ) & 0xFF ); + num & 0xFF); }; // IE does not support map (even in IE9) //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Array.prototype.map) -{ - Array.prototype.map = function(fun /*, thisp*/) - { - var len = this.length; - if (typeof fun != "function") - throw new TypeError(); +if (!Array.prototype.map) { + Array.prototype.map = function (fun /*, thisp*/) { + "use strict"; + var len = this.length; + if (typeof fun != "function") { + throw new TypeError(); + } - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) - { - if (i in this) - res[i] = fun.call(thisp, this[i], i, this); - } + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); + } + } - return res; - }; + return res; + }; } // IE <9 does not support indexOf //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Array.prototype.indexOf) -{ - Array.prototype.indexOf = function(elt /*, from*/) - { - var len = this.length >>> 0; +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (elt /*, from*/) { + "use strict"; + var len = this.length >>> 0; - var from = Number(arguments[1]) || 0; - from = (from < 0) - ? Math.ceil(from) - : Math.floor(from); - if (from < 0) - from += len; + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } - for (; from < len; from++) - { - if (from in this && - this[from] === elt) - return from; - } - return -1; - }; + for (; from < len; from++) { + if (from in this && + this[from] === elt) { + return from; + } + } + return -1; + }; } // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { - Object.keys = (function () { - 'use strict'; - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; + Object.keys = (function () { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; - return function (obj) { - if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { - throw new TypeError('Object.keys called on non-object'); - } + return function (obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } - var result = [], prop, i; + var result = [], prop, i; - for (prop in obj) { - if (hasOwnProperty.call(obj, prop)) { - result.push(prop); - } - } + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - }()); + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); } -// +// PhantomJS 1.x doesn't support bind, +// so leave this in until PhantomJS 2.0 is released +//This prototype is provided by the Mozilla foundation and +//is distributed under the MIT license. +//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - " + + "what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} + +// // requestAnimationFrame shim with setTimeout fallback // -window.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback){ +window.requestAnimFrame = (function () { + "use strict"; + return window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.oRequestAnimationFrame || + window.msRequestAnimationFrame || + function (callback) { window.setTimeout(callback, 1000 / 60); }; })(); -/* +/* * ------------------------------------------------------ * Namespaced in Util * ------------------------------------------------------ @@ -153,6 +184,7 @@ window.requestAnimFrame = (function(){ Util._log_level = 'warn'; Util.init_logging = function (level) { + "use strict"; if (typeof level === 'undefined') { level = Util._log_level; } else { @@ -163,26 +195,34 @@ Util.init_logging = function (level) { window.console = { 'log' : window.opera.postError, 'warn' : window.opera.postError, - 'error': window.opera.postError }; + 'error': window.opera.postError + }; } else { window.console = { - 'log' : function(m) {}, - 'warn' : function(m) {}, - 'error': function(m) {}}; + 'log' : function (m) {}, + 'warn' : function (m) {}, + 'error': function (m) {} + }; } } Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; + /* jshint -W086 */ switch (level) { - case 'debug': Util.Debug = function (msg) { console.log(msg); }; - case 'info': Util.Info = function (msg) { console.log(msg); }; - case 'warn': Util.Warn = function (msg) { console.warn(msg); }; - case 'error': Util.Error = function (msg) { console.error(msg); }; + case 'debug': + Util.Debug = function (msg) { console.log(msg); }; + case 'info': + Util.Info = function (msg) { console.log(msg); }; + case 'warn': + Util.Warn = function (msg) { console.warn(msg); }; + case 'error': + Util.Error = function (msg) { console.error(msg); }; case 'none': break; default: - throw("invalid logging type '" + level + "'"); + throw new Error("invalid logging type '" + level + "'"); } + /* jshint +W086 */ }; Util.get_logging = function () { return Util._log_level; @@ -190,93 +230,133 @@ Util.get_logging = function () { // Initialize logging level Util.init_logging(); +Util.make_property = function (proto, name, mode, type) { + "use strict"; -// Set configuration default for Crockford style function namespaces -Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) { - var getter, setter; - - // Default getter function - getter = function (idx) { - if ((type in {'arr':1, 'array':1}) && - (typeof idx !== 'undefined')) { - return cfg[v][idx]; - } else { - return cfg[v]; - } - }; - - // Default setter function - setter = function (val, idx) { - if (type in {'boolean':1, 'bool':1}) { - if ((!val) || (val in {'0':1, 'no':1, 'false':1})) { - val = false; + var getter; + if (type === 'arr') { + getter = function (idx) { + if (typeof idx !== 'undefined') { + return this['_' + name][idx]; } else { - val = true; + return this['_' + name]; } - } else if (type in {'integer':1, 'int':1}) { - val = parseInt(val, 10); - } else if (type === 'str') { - val = String(val); - } else if (type === 'func') { - if (!val) { - val = function () {}; - } - } - if (typeof idx !== 'undefined') { - cfg[v][idx] = val; - } else { - cfg[v] = val; - } - }; - - // Set the description - api[v + '_description'] = desc; - - // Set the getter function - if (typeof api['get_' + v] === 'undefined') { - api['get_' + v] = getter; - } - - // Set the setter function with extra sanity checks - if (typeof api['set_' + v] === 'undefined') { - api['set_' + v] = function (val, idx) { - if (mode in {'RO':1, 'ro':1}) { - throw(v + " is read-only"); - } else if ((mode in {'WO':1, 'wo':1}) && - (typeof cfg[v] !== 'undefined')) { - throw(v + " can only be set once"); - } - setter(val, idx); + }; + } else { + getter = function () { + return this['_' + name]; }; } - // Set the default value - if (typeof defaults[v] !== 'undefined') { - defval = defaults[v]; - } else if ((type in {'arr':1, 'array':1}) && - (! (defval instanceof Array))) { - defval = []; + var make_setter = function (process_val) { + if (process_val) { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = process_val(val); + } else { + this['_' + name] = process_val(val); + } + }; + } else { + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = val; + } else { + this['_' + name] = val; + } + }; + } + }; + + var setter; + if (type === 'bool') { + setter = make_setter(function (val) { + if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) { + return false; + } else { + return true; + } + }); + } else if (type === 'int') { + setter = make_setter(function (val) { return parseInt(val, 10); }); + } else if (type === 'float') { + setter = make_setter(parseFloat); + } else if (type === 'str') { + setter = make_setter(String); + } else if (type === 'func') { + setter = make_setter(function (val) { + if (!val) { + return function () {}; + } else { + return val; + } + }); + } else if (type === 'arr' || type === 'dom' || type == 'raw') { + setter = make_setter(); + } else { + throw new Error('Unknown property type ' + type); // some sanity checking } - // Coerce existing setting to the right type - //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]); - setter(defval); + + // set the getter + if (typeof proto['get_' + name] === 'undefined') { + proto['get_' + name] = getter; + } + + // set the setter if needed + if (typeof proto['set_' + name] === 'undefined') { + if (mode === 'rw') { + proto['set_' + name] = setter; + } else if (mode === 'wo') { + proto['set_' + name] = function (val, idx) { + if (typeof this['_' + name] !== 'undefined') { + throw new Error(name + " can only be set once"); + } + setter.call(this, val, idx); + }; + } + } + + // make a special setter that we can use in set defaults + proto['_raw_set_' + name] = function (val, idx) { + setter.call(this, val, idx); + //delete this['_init_set_' + name]; // remove it after use + }; }; -// Set group of configuration defaults -Util.conf_defaults = function(cfg, api, defaults, arr) { +Util.make_properties = function (constructor, arr) { + "use strict"; + for (var i = 0; i < arr.length; i++) { + Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]); + } +}; + +Util.set_defaults = function (obj, conf, defaults) { + var defaults_keys = Object.keys(defaults); + var conf_keys = Object.keys(conf); + var keys_obj = {}; var i; - for (i = 0; i < arr.length; i++) { - Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1], - arr[i][2], arr[i][3], arr[i][4]); + for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; } + for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; } + var keys = Object.keys(keys_obj); + + for (i = 0; i < keys.length; i++) { + var setter = obj['_raw_set_' + keys[i]]; + + if (conf[keys[i]]) { + setter.call(obj, conf[keys[i]]); + } else { + setter.call(obj, defaults[keys[i]]); + } } }; /* * Decode from UTF-8 */ -Util.decodeUTF8 = function(utf8string) { +Util.decodeUTF8 = function (utf8string) { + "use strict"; return decodeURIComponent(escape(utf8string)); -} +}; @@ -291,42 +371,46 @@ Util.decodeUTF8 = function(utf8string) { // Handles the case where load_scripts is invoked from a script that // itself is loaded via load_scripts. Once all scripts are loaded the // window.onscriptsloaded handler is called (if set). -Util.get_include_uri = function() { +Util.get_include_uri = function () { return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/"; -} +}; Util._loading_scripts = []; Util._pending_scripts = []; -Util.load_scripts = function(files) { +Util.load_scripts = function (files) { + "use strict"; var head = document.getElementsByTagName('head')[0], script, ls = Util._loading_scripts, ps = Util._pending_scripts; - for (var f=0; f 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + + for (var f = 0; f < files.length; f++) { script = document.createElement('script'); script.type = 'text/javascript'; script.src = Util.get_include_uri() + files[f]; //console.log("loading script: " + script.src); - script.onload = script.onreadystatechange = function (e) { - while (ls.length > 0 && (ls[0].readyState === 'loaded' || - ls[0].readyState === 'complete')) { - // For IE, append the script to trigger execution - var s = ls.shift(); - //console.log("loaded script: " + s.src); - head.appendChild(s); - } - if (!this.readyState || - (Util.Engine.presto && this.readyState === 'loaded') || - this.readyState === 'complete') { - if (ps.indexOf(this) >= 0) { - this.onload = this.onreadystatechange = null; - //console.log("completed script: " + this.src); - ps.splice(ps.indexOf(this), 1); - - // Call window.onscriptsload after last script loads - if (ps.length === 0 && window.onscriptsload) { - window.onscriptsload(); - } - } - } - }; + script.onload = script.onreadystatechange = loadFunc; // In-order script execution tricks if (Util.Engine.trident) { // For IE wait until readyState is 'loaded' before @@ -341,20 +425,22 @@ Util.load_scripts = function(files) { } ps.push(script); } -} +}; // Get DOM element position on page // This solution is based based on http://www.greywyvern.com/?post=331 // Thanks to Brian Huisman AKA GreyWyvern! -Util.getPosition = (function() { +Util.getPosition = (function () { + "use strict"; function getStyle(obj, styleProp) { + var y; if (obj.currentStyle) { - var y = obj.currentStyle[styleProp]; + y = obj.currentStyle[styleProp]; } else if (window.getComputedStyle) - var y = window.getComputedStyle(obj, null)[styleProp]; + y = window.getComputedStyle(obj, null)[styleProp]; return y; - }; + } function scrollDist() { var myScrollTop = 0, myScrollLeft = 0; @@ -383,7 +469,7 @@ Util.getPosition = (function() { } return [myScrollLeft, myScrollTop]; - }; + } return function (obj) { var curleft = 0, curtop = 0, scr = obj, fixed = false; @@ -403,7 +489,7 @@ Util.getPosition = (function() { do { curleft += obj.offsetLeft; curtop += obj.offsetTop; - } while (obj = obj.offsetParent); + } while ((obj = obj.offsetParent)); return {'x': curleft, 'y': curtop}; }; @@ -412,6 +498,7 @@ Util.getPosition = (function() { // Get mouse event position in DOM element Util.getEventPosition = function (e, obj, scale) { + "use strict"; var evt, docX, docY, pos; //if (!e) evt = window.event; evt = (e ? e : window.event); @@ -431,38 +518,41 @@ Util.getEventPosition = function (e, obj, scale) { } var realx = docX - pos.x; var realy = docY - pos.y; - var x = Math.max(Math.min(realx, obj.width-1), 0); - var y = Math.max(Math.min(realy, obj.height-1), 0); + var x = Math.max(Math.min(realx, obj.width - 1), 0); + var y = Math.max(Math.min(realy, obj.height - 1), 0); return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; }; // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events -Util.addEvent = function (obj, evType, fn){ - if (obj.attachEvent){ - var r = obj.attachEvent("on"+evType, fn); +Util.addEvent = function (obj, evType, fn) { + "use strict"; + if (obj.attachEvent) { + var r = obj.attachEvent("on" + evType, fn); return r; - } else if (obj.addEventListener){ - obj.addEventListener(evType, fn, false); + } else if (obj.addEventListener) { + obj.addEventListener(evType, fn, false); return true; } else { - throw("Handler could not be attached"); + throw new Error("Handler could not be attached"); } }; -Util.removeEvent = function(obj, evType, fn){ - if (obj.detachEvent){ - var r = obj.detachEvent("on"+evType, fn); +Util.removeEvent = function (obj, evType, fn) { + "use strict"; + if (obj.detachEvent) { + var r = obj.detachEvent("on" + evType, fn); return r; - } else if (obj.removeEventListener){ + } else if (obj.removeEventListener) { obj.removeEventListener(evType, fn, false); return true; } else { - throw("Handler could not be removed"); + throw new Error("Handler could not be removed"); } }; -Util.stopEvent = function(e) { +Util.stopEvent = function (e) { + "use strict"; if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } @@ -474,41 +564,88 @@ Util.stopEvent = function(e) { // Set browser engine versions. Based on mootools. Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; -Util.Engine = { - // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) - //'presto': (function() { - // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), - 'presto': (function() { return (!window.opera) ? false : true; }()), +(function () { + "use strict"; + // 'presto': (function () { return (!window.opera) ? false : true; }()), + var detectPresto = function () { + return !!window.opera; + }; - 'trident': (function() { - return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), - 'webkit': (function() { - try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), - //'webkit': (function() { - // return ((typeof navigator.taintEnabled !== "unknown") && navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); }()), - 'gecko': (function() { - return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19 : 18); }()) -}; -if (Util.Engine.webkit) { - // Extract actual webkit version if available - Util.Engine.webkit = (function(v) { - var re = new RegExp('WebKit/([0-9\.]*) '); - v = (navigator.userAgent.match(re) || ['', v])[1]; - return parseFloat(v, 10); - })(Util.Engine.webkit); -} + // 'trident': (function () { return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); + var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } + }; -Util.Flash = (function(){ + // 'webkit': (function () { try { return (navigator.taintEnabled) ? false : ((Util.Features.xpath) ? ((Util.Features.query) ? 525 : 420) : 419); } catch (e) { return false; } }()), + var detectInitialWebkit = function () { + try { + if (navigator.taintEnabled) { + return false; + } else { + if (Util.Features.xpath) { + return (Util.Features.query) ? 525 : 420; + } else { + return 419; + } + } + } catch (e) { + return false; + } + }; + + var detectActualWebkit = function (initial_ver) { + var re = /WebKit\/([0-9\.]*) /; + var str_ver = (navigator.userAgent.match(re) || ['', initial_ver])[1]; + return parseFloat(str_ver, 10); + }; + + // 'gecko': (function () { return (!document.getBoxObjectFor && window.mozInnerScreenX == null) ? false : ((document.getElementsByClassName) ? 19ssName) ? 19 : 18 : 18); }()) + var detectGecko = function () { + /* jshint -W041 */ + if (!document.getBoxObjectFor && window.mozInnerScreenX == null) { + return false; + } else { + return (document.getElementsByClassName) ? 19 : 18; + } + /* jshint +W041 */ + }; + + Util.Engine = { + // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) + //'presto': (function() { + // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), + 'presto': detectPresto(), + 'trident': detectTrident(), + 'webkit': detectInitialWebkit(), + 'gecko': detectGecko(), + }; + + if (Util.Engine.webkit) { + // Extract actual webkit version if available + Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit); + } +})(); + +Util.Flash = (function () { + "use strict"; var v, version; try { v = navigator.plugins['Shockwave Flash'].description; - } catch(err1) { + } catch (err1) { try { v = new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version'); - } catch(err2) { + } catch (err2) { v = '0 r0'; } } version = v.match(/\d+/g); return {version: parseInt(version[0] || 0 + '.' + version[1], 10) || 0, build: parseInt(version[2], 10) || 0}; -}()); +}()); diff --git a/tests/test.util.js b/tests/test.util.js new file mode 100644 index 00000000..63f51b7c --- /dev/null +++ b/tests/test.util.js @@ -0,0 +1,105 @@ +// requires local modules: util +/* jshint expr: true */ + +var assert = chai.assert; +var expect = chai.expect; + +describe('Utils', function() { + "use strict"; + + describe('Array instance methods', function () { + describe('push8', function () { + it('should push a byte on to the array', function () { + var arr = [1]; + arr.push8(128); + expect(arr).to.deep.equal([1, 128]); + }); + + it('should only use the least significant byte of any number passed in', function () { + var arr = [1]; + arr.push8(0xABCD); + expect(arr).to.deep.equal([1, 0xCD]); + }); + }); + + describe('push16', function () { + it('should push two bytes on to the array', function () { + var arr = [1]; + arr.push16(0xABCD); + expect(arr).to.deep.equal([1, 0xAB, 0xCD]); + }); + + it('should only use the two least significant bytes of any number passed in', function () { + var arr = [1]; + arr.push16(0xABCDEF); + expect(arr).to.deep.equal([1, 0xCD, 0xEF]); + }); + }); + + describe('push32', function () { + it('should push four bytes on to the array', function () { + var arr = [1]; + arr.push32(0xABCDEF12); + expect(arr).to.deep.equal([1, 0xAB, 0xCD, 0xEF, 0x12]); + }); + + it('should only use the four least significant bytes of any number passed in', function () { + var arr = [1]; + arr.push32(0xABCDEF1234); + expect(arr).to.deep.equal([1, 0xCD, 0xEF, 0x12, 0x34]); + }); + }); + }); + + describe('logging functions', function () { + beforeEach(function () { + sinon.spy(console, 'log'); + sinon.spy(console, 'warn'); + sinon.spy(console, 'error'); + }); + + afterEach(function () { + console.log.restore(); + console.warn.restore(); + console.error.restore(); + }); + + it('should use noop for levels lower than the min level', function () { + Util.init_logging('warn'); + Util.Debug('hi'); + Util.Info('hello'); + expect(console.log).to.not.have.been.called; + }); + + it('should use console.log for Debug and Info', function () { + Util.init_logging('debug'); + Util.Debug('dbg'); + Util.Info('inf'); + expect(console.log).to.have.been.calledWith('dbg'); + expect(console.log).to.have.been.calledWith('inf'); + }); + + it('should use console.warn for Warn', function () { + Util.init_logging('warn'); + Util.Warn('wrn'); + expect(console.warn).to.have.been.called; + expect(console.warn).to.have.been.calledWith('wrn'); + }); + + it('should use console.error for Error', function () { + Util.init_logging('error'); + Util.Error('err'); + expect(console.error).to.have.been.called; + expect(console.error).to.have.been.calledWith('err'); + }); + }); + + // TODO(directxman12): test the conf_default and conf_defaults methods + // TODO(directxman12): test decodeUTF8 + // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent) + // TODO(directxman12): figure out a good way to test getPosition and getEventPosition + // TODO(directxman12): figure out how to test the browser detection functions properly + // (we can't really test them against the browsers, except for Gecko + // via PhantomJS, the default test driver) + // TODO(directxman12): figure out how to test Util.Flash +}); From d6e281baf66b6aa59e54ea0e1c775b46f9698af2 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 20 May 2014 18:29:52 -0400 Subject: [PATCH 007/527] Cleanup: Input code File: input.js Tests Added: False (already present partially -- see below) Changes: - Fixed JSHint Errors - Converted to normal non-Crockford constructors NOTE: while there are tests for the actual key-detecting functionality, the tests do not cover the actual Keyboard and Mouse objects themselves. --- include/input.js | 713 +++++++++++++++++++++++------------------------ 1 file changed, 350 insertions(+), 363 deletions(-) diff --git a/include/input.js b/include/input.js index 392b4107..5d9e209e 100644 --- a/include/input.js +++ b/include/input.js @@ -5,397 +5,384 @@ * Licensed under MPL 2.0 or any later version (see LICENSE.txt) */ -/*jslint browser: true, white: false, bitwise: false */ +/*jslint browser: true, white: false */ /*global window, Util */ +var Keyboard, Mouse; -// -// Keyboard event handler -// +(function () { + "use strict"; -function Keyboard(defaults) { -"use strict"; + // + // Keyboard event handler + // -var that = {}, // Public API methods - conf = {}, // Configuration attributes + Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) - keyDownList = []; // List of depressed keys - // (even if they are happy) + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'], - ['focused', 'rw', 'bool', true, 'Capture and send key events'], + // create the keyboard handler + this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(), + VerifyCharModifier( /* jshint newcap: false */ + TrackKeyState( + EscapeModifiers(this._handleRfbEvent.bind(this)) + ) + ) + ); /* jshint newcap: true */ - ['onKeyPress', 'rw', 'func', null, 'Handler for key press/release'] - ]); + // keep these here so we can refer to them later + this._eventHandlers = { + 'keyup': this._handleKeyUp.bind(this), + 'keydown': this._handleKeyDown.bind(this), + 'keypress': this._handleKeyPress.bind(this), + 'blur': this._allKeysUp.bind(this) + }; + }; + Keyboard.prototype = { + // private methods -// -// Private functions -// - -/////// setup - -function onRfbEvent(evt) { - if (conf.onKeyPress) { - Util.Debug("onKeyPress " + (evt.type == 'keydown' ? "down" : "up") - + ", keysym: " + evt.keysym.keysym + "(" + evt.keysym.keyname + ")"); - conf.onKeyPress(evt.keysym.keysym, evt.type == 'keydown'); - } -} - -// create the keyboard handler -var k = KeyEventDecoder(kbdUtil.ModifierSync(), - VerifyCharModifier( - TrackKeyState( - EscapeModifiers(onRfbEvent) - ) - ) -); - -function onKeyDown(e) { - if (! conf.focused) { - return true; - } - if (k.keydown(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; - } -} -function onKeyPress(e) { - if (! conf.focused) { - return true; - } - if (k.keypress(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; - } -} - -function onKeyUp(e) { - if (! conf.focused) { - return true; - } - if (k.keyup(e)) { - // Suppress bubbling/default actions - Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; - } -} - -function onOther(e) { - k.syncModifiers(e); -} - -function allKeysUp() { - Util.Debug(">> Keyboard.allKeysUp"); - - k.releaseAll(); - Util.Debug("<< Keyboard.allKeysUp"); -} - -// -// Public API interface functions -// - -that.grab = function() { - //Util.Debug(">> Keyboard.grab"); - var c = conf.target; - - Util.addEvent(c, 'keydown', onKeyDown); - Util.addEvent(c, 'keyup', onKeyUp); - Util.addEvent(c, 'keypress', onKeyPress); - - // Release (key up) if window loses focus - Util.addEvent(window, 'blur', allKeysUp); - - //Util.Debug("<< Keyboard.grab"); -}; - -that.ungrab = function() { - //Util.Debug(">> Keyboard.ungrab"); - var c = conf.target; - - Util.removeEvent(c, 'keydown', onKeyDown); - Util.removeEvent(c, 'keyup', onKeyUp); - Util.removeEvent(c, 'keypress', onKeyPress); - Util.removeEvent(window, 'blur', allKeysUp); - - // Release (key up) all keys that are in a down state - allKeysUp(); - - //Util.Debug(">> Keyboard.ungrab"); -}; - -that.sync = function(e) { - k.syncModifiers(e); -} - -return that; // Return the public API interface - -} // End of Keyboard() - - -// -// Mouse event handler -// - -function Mouse(defaults) { -"use strict"; - -var that = {}, // Public API methods - conf = {}, // Configuration attributes - mouseCaptured = false; - -var doubleClickTimer = null, - lastTouchPos = null; - -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'ro', 'dom', document, 'DOM element that captures mouse input'], - ['notify', 'ro', 'func', null, 'Function to call to notify whenever a mouse event is received'], - ['focused', 'rw', 'bool', true, 'Capture and send mouse clicks/movement'], - ['scale', 'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'], - - ['onMouseButton', 'rw', 'func', null, 'Handler for mouse button click/release'], - ['onMouseMove', 'rw', 'func', null, 'Handler for mouse movement'], - ['touchButton', 'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)'] - ]); - -function captureMouse() { - // capturing the mouse ensures we get the mouseup event - if (conf.target.setCapture) { - conf.target.setCapture(); - } - - // some browsers give us mouseup events regardless, - // so if we never captured the mouse, we can disregard the event - mouseCaptured = true; -} - -function releaseMouse() { - if (conf.target.releaseCapture) { - conf.target.releaseCapture(); - } - mouseCaptured = false; -} -// -// Private functions -// - -function resetDoubleClickTimer() { - doubleClickTimer = null; -} - -function onMouseButton(e, down) { - var evt, pos, bmask; - if (! conf.focused) { - return true; - } - - if (conf.notify) { - conf.notify(e); - } - - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - - if (e.touches || e.changedTouches) { - // Touch device - - // When two touches occur within 500 ms of each other and are - // closer than 20 pixels together a double click is triggered. - if (down == 1) { - if (doubleClickTimer == null) { - lastTouchPos = pos; - } else { - clearTimeout(doubleClickTimer); - - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. - - var xs = lastTouchPos.x - pos.x; - var ys = lastTouchPos.y - pos.y; - var d = Math.sqrt((xs * xs) + (ys * ys)); - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - if (d < 20 * window.devicePixelRatio) { - pos = lastTouchPos; - } + _handleRfbEvent: function (e) { + if (this._onKeyPress) { + Util.Debug("onKeyPress " + (e.type == 'keydown' ? "down" : "up") + + ", keysym: " + e.keysym.keysym + "(" + e.keysym.keyname + ")"); + this._onKeyPress(e.keysym.keysym, e.type == 'keydown'); } - doubleClickTimer = setTimeout(resetDoubleClickTimer, 500); + }, + + _handleKeyDown: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keydown(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyPress: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keypress(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _handleKeyUp: function (e) { + if (!this._focused) { return true; } + + if (this._handler.keyup(e)) { + // Suppress bubbling/default actions + Util.stopEvent(e); + return false; + } else { + // Allow the event to bubble and become a keyPress event which + // will have the character code translated + return true; + } + }, + + _allKeysUp: function () { + Util.Debug(">> Keyboard.allKeysUp"); + this._handler.releaseAll(); + Util.Debug("<< Keyboard.allKeysUp"); + }, + + // Public methods + + grab: function () { + //Util.Debug(">> Keyboard.grab"); + var c = this._target; + + Util.addEvent(c, 'keydown', this._eventHandlers.keydown); + Util.addEvent(c, 'keyup', this._eventHandlers.keyup); + Util.addEvent(c, 'keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + Util.addEvent(window, 'blur', this._eventHandlers.blur); + + //Util.Debug("<< Keyboard.grab"); + }, + + ungrab: function () { + //Util.Debug(">> Keyboard.ungrab"); + var c = this._target; + + Util.removeEvent(c, 'keydown', this._eventHandlers.keydown); + Util.removeEvent(c, 'keyup', this._eventHandlers.keyup); + Util.removeEvent(c, 'keypress', this._eventHandlers.keypress); + Util.removeEvent(window, 'blur', this._eventHandlers.blur); + + // Release (key up) all keys that are in a down state + this._allKeysUp(); + + //Util.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); } - bmask = conf.touchButton; - // If bmask is set - } else if (evt.which) { - /* everything except IE */ - bmask = 1 << evt.button; - } else { - /* IE including 9 */ - bmask = (evt.button & 0x1) + // Left - (evt.button & 0x2) * 2 + // Right - (evt.button & 0x4) / 2; // Middle - } - //Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down + - // " bmask: " + bmask + "(evt.button: " + evt.button + ")"); - if (conf.onMouseButton) { - Util.Debug("onMouseButton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - conf.onMouseButton(pos.x, pos.y, down, bmask); - } - Util.stopEvent(e); - return false; -} + }; -function onMouseDown(e) { - captureMouse(); - onMouseButton(e, 1); -} + Util.make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events -function onMouseUp(e) { - if (!mouseCaptured) { - return; - } + ['onKeyPress', 'rw', 'func'] // Handler for key press/release + ]); - onMouseButton(e, 0); - releaseMouse(); -} + // + // Mouse event handler + // -function onMouseWheel(e) { - var evt, pos, bmask, wheelData; - if (! conf.focused) { - return true; - } - if (conf.notify) { - conf.notify(e); - } + Mouse = function (defaults) { + this._mouseCaptured = false; - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; - if (wheelData > 0) { - bmask = 1 << 3; - } else { - bmask = 1 << 4; - } - //Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y); - if (conf.onMouseButton) { - conf.onMouseButton(pos.x, pos.y, 1, bmask); - conf.onMouseButton(pos.x, pos.y, 0, bmask); - } - Util.stopEvent(e); - return false; -} + this._doubleClickTimer = null; + this._lastTouchPos = null; -function onMouseMove(e) { - var evt, pos; - if (! conf.focused) { - return true; - } - if (conf.notify) { - conf.notify(e); - } + // Configuration attributes + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'scale': 1.0, + 'touchButton': 1 + }); - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - //Util.Debug('mouse ' + evt.which + '/' + evt.button + ' up:' + pos.x + "," + pos.y); - if (conf.onMouseMove) { - conf.onMouseMove(pos.x, pos.y); - } - Util.stopEvent(e); - return false; -} + this._eventHandlers = { + 'mousedown': this._handleMouseDown.bind(this), + 'mouseup': this._handleMouseUp.bind(this), + 'mousemove': this._handleMouseMove.bind(this), + 'mousewheel': this._handleMouseWheel.bind(this), + 'mousedisable': this._handleMouseDisable.bind(this) + }; + }; -function onMouseDisable(e) { - var evt, pos; - if (! conf.focused) { - return true; - } - evt = (e ? e : window.event); - pos = Util.getEventPosition(e, conf.target, conf.scale); - /* Stop propagation if inside canvas area */ - if ((pos.realx >= 0) && (pos.realy >= 0) && - (pos.realx < conf.target.offsetWidth) && - (pos.realy < conf.target.offsetHeight)) { - //Util.Debug("mouse event disabled"); - Util.stopEvent(e); - return false; - } - //Util.Debug("mouse event not disabled"); - return true; -} + Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + if (this._target.setCapture) { + this._target.setCapture(); + } -// -// Public API interface functions -// + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, -that.grab = function() { - //Util.Debug(">> Mouse.grab"); - var c = conf.target; + _releaseMouse: function () { + if (this._target.releaseCapture) { + this._target.releaseCapture(); + } + this._mouseCaptured = false; + }, - if ('ontouchstart' in document.documentElement) { - Util.addEvent(c, 'touchstart', onMouseDown); - Util.addEvent(window, 'touchend', onMouseUp); - Util.addEvent(c, 'touchend', onMouseUp); - Util.addEvent(c, 'touchmove', onMouseMove); - } else { - Util.addEvent(c, 'mousedown', onMouseDown); - Util.addEvent(window, 'mouseup', onMouseUp); - Util.addEvent(c, 'mouseup', onMouseUp); - Util.addEvent(c, 'mousemove', onMouseMove); - Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - onMouseWheel); - } + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, - /* Work around right and middle click browser behaviors */ - Util.addEvent(document, 'click', onMouseDisable); - Util.addEvent(document.body, 'contextmenu', onMouseDisable); + _handleMouseButton: function (e, down) { + if (!this._focused) { return true; } - //Util.Debug("<< Mouse.grab"); -}; + if (this._notify) { + this._notify(e); + } -that.ungrab = function() { - //Util.Debug(">> Mouse.ungrab"); - var c = conf.target; + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); - if ('ontouchstart' in document.documentElement) { - Util.removeEvent(c, 'touchstart', onMouseDown); - Util.removeEvent(window, 'touchend', onMouseUp); - Util.removeEvent(c, 'touchend', onMouseUp); - Util.removeEvent(c, 'touchmove', onMouseMove); - } else { - Util.removeEvent(c, 'mousedown', onMouseDown); - Util.removeEvent(window, 'mouseup', onMouseUp); - Util.removeEvent(c, 'mouseup', onMouseUp); - Util.removeEvent(c, 'mousemove', onMouseMove); - Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', - onMouseWheel); - } + var bmask; + if (e.touches || e.changedTouches) { + // Touch device - /* Work around right and middle click browser behaviors */ - Util.removeEvent(document, 'click', onMouseDisable); - Util.removeEvent(document.body, 'contextmenu', onMouseDisable); + // When two touches occur within 500 ms of each other and are + // closer than 20 pixels together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._doubleClickTimer); - //Util.Debug(">> Mouse.ungrab"); -}; + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. -return that; // Return the public API interface + var xs = this._lastTouchPos.x - pos.x; + var ys = this._lastTouchPos.y - pos.y; + var d = Math.sqrt((xs * xs) + (ys * ys)); -} // End of Mouse() + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + if (d < 20 * window.devicePixelRatio) { + pos = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this._touchButton; + // If bmask is set + } else if (evt.which) { + /* everything except IE */ + bmask = 1 << evt.button; + } else { + /* IE including 9 */ + bmask = (evt.button & 0x1) + // Left + (evt.button & 0x2) * 2 + // Right + (evt.button & 0x4) / 2; // Middle + } + + if (this._onMouseButton) { + Util.Debug("onMouseButton " + (down ? "down" : "up") + + ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); + this._onMouseButton(pos.x, pos.y, down, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDown: function (e) { + this._captureMouse(); + this._handleMouseButton(e, 1); + }, + + _handleMouseUp: function (e) { + if (!this._mouseCaptured) { return; } + + this._handleMouseButton(e, 0); + this._releaseMouse(); + }, + + _handleMouseWheel: function (e) { + if (!this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40; + var bmask; + if (wheelData > 0) { + bmask = 1 << 3; + } else { + bmask = 1 << 4; + } + + if (this._onMouseButton) { + this._onMouseButton(pos.x, pos.y, 1, bmask); + this._onMouseButton(pos.x, pos.y, 0, bmask); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseMove: function (e) { + if (! this._focused) { return true; } + + if (this._notify) { + this._notify(e); + } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + if (this._onMouseMove) { + this._onMouseMove(pos.x, pos.y); + } + Util.stopEvent(e); + return false; + }, + + _handleMouseDisable: function (e) { + if (!this._focused) { return true; } + + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + /* Stop propagation if inside canvas area */ + if ((pos.realx >= 0) && (pos.realy >= 0) && + (pos.realx < this._target.offsetWidth) && + (pos.realy < this._target.offsetHeight)) { + //Util.Debug("mouse event disabled"); + Util.stopEvent(e); + return false; + } + + return true; + }, + + + // Public methods + grab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.addEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.addEvent(document, 'click', this._eventHandlers.mousedisable); + Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + }, + + ungrab: function () { + var c = this._target; + + if ('ontouchstart' in document.documentElement) { + Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown); + Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup); + Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove); + } else { + Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown); + Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup); + Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove); + Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + this._eventHandlers.mousewheel); + } + + /* Work around right and middle click browser behaviors */ + Util.removeEvent(document, 'click', this._eventHandlers.mousedisable); + Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + + } + }; + + Util.make_properties(Mouse, [ + ['target', 'ro', 'dom'], // DOM element that captures mouse input + ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received + ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement + ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0 + + ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release + ['onMouseMove', 'rw', 'func'], // Handler for mouse movement + ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + ]); +})(); From 31f169e86fe7b8065c5e1406ed2885549920e822 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 20 May 2014 19:16:01 -0400 Subject: [PATCH 008/527] Cleanup: Keyboard code File: keyboard.js Tests Added: False (already present) Changes: - Fixed JSHint Errors - Moved functions outside loops - Added proper include directives to tests --- include/keyboard.js | 29 +++++++++++++++++++---------- tests/test.helper.js | 4 +++- tests/test.keyboard.js | 5 +++-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/include/keyboard.js b/include/keyboard.js index c89411cc..60443214 100644 --- a/include/keyboard.js +++ b/include/keyboard.js @@ -15,7 +15,7 @@ var kbdUtil = (function() { var sub = substitutions[cp]; return sub ? sub : cp; - }; + } function isMac() { return navigator && !!(/mac/i).exec(navigator.platform); @@ -387,17 +387,22 @@ function VerifyCharModifier(next) { if (timer) { return; } + + var delayProcess = function () { + clearTimeout(timer); + timer = null; + process(); + }; + while (queue.length !== 0) { var cur = queue[0]; queue = queue.splice(1); switch (cur.type) { case 'stall': // insert a delay before processing available events. - timer = setTimeout(function() { - clearTimeout(timer); - timer = null; - process(); - }, 5); + /* jshint loopfunc: true */ + timer = setTimeout(delayProcess, 5); + /* jshint loopfunc: false */ return; case 'keydown': // is the next element a keypress? Then we should merge the two @@ -489,23 +494,25 @@ function TrackKeyState(next) { var item = state.splice(idx, 1)[0]; // for each keysym tracked by this key entry, clone the current event and override the keysym + var clone = (function(){ + function Clone(){} + return function (obj) { Clone.prototype=obj; return new Clone(); }; + }()); for (var key in item.keysyms) { - var clone = (function(){ - function Clone(){} - return function (obj) { Clone.prototype=obj; return new Clone(); }; - }()); var out = clone(evt); out.keysym = item.keysyms[key]; next(out); } break; case 'releaseall': + /* jshint shadow: true */ for (var i = 0; i < state.length; ++i) { for (var key in state[i].keysyms) { var keysym = state[i].keysyms[key]; next({keyId: 0, keysym: keysym, type: 'keyup'}); } } + /* jshint shadow: false */ state = []; } }; @@ -527,8 +534,10 @@ function EscapeModifiers(next) { // send the character event next(evt); // redo modifiers + /* jshint shadow: true */ for (var i = 0; i < evt.escape.length; ++i) { next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); } + /* jshint shadow: false */ }; } diff --git a/tests/test.helper.js b/tests/test.helper.js index d9e8e144..98009d2a 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -1,4 +1,6 @@ -var assert = chai.assert; +// requires local modules: keysym, keysymdef, keyboard + +var assert = chai.assert; var expect = chai.expect; describe('Helpers', function() { diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 80d1fee4..2ac65af3 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -1,7 +1,8 @@ +// requires local modules: input, keyboard, keysymdef var assert = chai.assert; var expect = chai.expect; - +/* jshint newcap: false, expr: true */ describe('Key Event Pipeline Stages', function() { "use strict"; describe('Decode Keyboard Events', function() { @@ -50,7 +51,7 @@ describe('Key Event Pipeline Stages', function() { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'}); done(); - }).keydown({keyCode: 0x41}) + }).keydown({keyCode: 0x41}); }); it('should forward keyup events with the right type', function(done) { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { From ee7d4c61c6c2b39bc257d9fc1c20d2b2a479d370 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 21 May 2014 15:12:20 -0400 Subject: [PATCH 009/527] Cleanup: Webutil code File: webutil.js Tests Added: False Changes: - Fixed JSHint Errors (global "use strict", spaces) - added some newline characters when appropriate for readability - moved variable declarations to the places they were actually used for readability --- include/webutil.js | 102 ++++++++++++++++++++++++++------------------- tests/test.util.js | 12 +++--- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/include/webutil.js b/include/webutil.js index e95fc807..e674bf94 100644 --- a/include/webutil.js +++ b/include/webutil.js @@ -7,8 +7,7 @@ * See README.md for usage and integration instructions. */ -"use strict"; -/*jslint bitwise: false, white: false */ +/*jslint bitwise: false, white: false, browser: true, devel: true */ /*global Util, window, document */ // Globals defined here @@ -31,45 +30,47 @@ if (!window.$D) { } -/* +/* * ------------------------------------------------------ * Namespaced in WebUtil * ------------------------------------------------------ */ // init log level reading the logging HTTP param -WebUtil.init_logging = function(level) { +WebUtil.init_logging = function (level) { + "use strict"; if (typeof level !== "undefined") { Util._log_level = level; } else { - Util._log_level = (document.location.href.match( - /logging=([A-Za-z0-9\._\-]*)/) || - ['', Util._log_level])[1]; + var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/); + Util._log_level = (param || ['', Util._log_level])[1]; } Util.init_logging(); }; WebUtil.dirObj = function (obj, depth, parent) { - var i, msg = "", val = ""; - if (! depth) { depth=2; } - if (! parent) { parent= ""; } + "use strict"; + if (! depth) { depth = 2; } + if (! parent) { parent = ""; } - // Print the properties of the passed-in object - for (i in obj) { - if ((depth > 1) && (typeof obj[i] === "object")) { + // Print the properties of the passed-in object + var msg = ""; + for (var i in obj) { + if ((depth > 1) && (typeof obj[i] === "object")) { // Recurse attributes that are objects - msg += WebUtil.dirObj(obj[i], depth-1, parent + "." + i); + msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i); } else { //val = new String(obj[i]).replace("\n", " "); + var val = ""; if (typeof(obj[i]) === "undefined") { val = "undefined"; } else { val = obj[i].toString().replace("\n", " "); } if (val.length > 30) { - val = val.substr(0,30) + "..."; - } + val = val.substr(0, 30) + "..."; + } msg += parent + "." + i + ": " + val + "\n"; } } @@ -77,7 +78,8 @@ WebUtil.dirObj = function (obj, depth, parent) { }; // Read a query string variable -WebUtil.getQueryVar = function(name, defVal) { +WebUtil.getQueryVar = function (name, defVal) { + "use strict"; var re = new RegExp('.*[?&]' + name + '=([^&#]*)'), match = document.location.href.match(re); if (typeof defVal === 'undefined') { defVal = null; } @@ -94,42 +96,50 @@ WebUtil.getQueryVar = function(name, defVal) { */ // No days means only for this browser session -WebUtil.createCookie = function(name,value,days) { - var date, expires, secure; +WebUtil.createCookie = function (name, value, days) { + "use strict"; + var date, expires; if (days) { date = new Date(); - date.setTime(date.getTime()+(days*24*60*60*1000)); - expires = "; expires="+date.toGMTString(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); } else { expires = ""; } + + var secure; if (document.location.protocol === "https:") { secure = "; secure"; } else { secure = ""; } - document.cookie = name+"="+value+expires+"; path=/"+secure; + document.cookie = name + "=" + value + expires + "; path=/" + secure; }; -WebUtil.readCookie = function(name, defaultValue) { - var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); - for(i=0; i < ca.length; i += 1) { - c = ca[i]; - while (c.charAt(0) === ' ') { c = c.substring(1,c.length); } - if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); } +WebUtil.readCookie = function (name, defaultValue) { + "use strict"; + var nameEQ = name + "=", + ca = document.cookie.split(';'); + + for (var i = 0; i < ca.length; i += 1) { + var c = ca[i]; + while (c.charAt(0) === ' ') { c = c.substring(1, c.length); } + if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); } } return (typeof defaultValue !== 'undefined') ? defaultValue : null; }; -WebUtil.eraseCookie = function(name) { - WebUtil.createCookie(name,"",-1); +WebUtil.eraseCookie = function (name) { + "use strict"; + WebUtil.createCookie(name, "", -1); }; /* * Setting handling. */ -WebUtil.initSettings = function(callback) { +WebUtil.initSettings = function (callback /*, ...callbackArgs */) { + "use strict"; var callbackArgs = Array.prototype.slice.call(arguments, 1); if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.get(function (cfg) { @@ -148,7 +158,8 @@ WebUtil.initSettings = function(callback) { }; // No days means only for this browser session -WebUtil.writeSetting = function(name, value) { +WebUtil.writeSetting = function (name, value) { + "use strict"; if (window.chrome && window.chrome.storage) { //console.log("writeSetting:", name, value); if (WebUtil.settings[name] !== value) { @@ -160,7 +171,8 @@ WebUtil.writeSetting = function(name, value) { } }; -WebUtil.readSetting = function(name, defaultValue) { +WebUtil.readSetting = function (name, defaultValue) { + "use strict"; var value; if (window.chrome && window.chrome.storage) { value = WebUtil.settings[name]; @@ -177,7 +189,8 @@ WebUtil.readSetting = function(name, defaultValue) { } }; -WebUtil.eraseSetting = function(name) { +WebUtil.eraseSetting = function (name) { + "use strict"; if (window.chrome && window.chrome.storage) { window.chrome.storage.sync.remove(name); delete WebUtil.settings[name]; @@ -189,9 +202,12 @@ WebUtil.eraseSetting = function(name) { /* * Alternate stylesheet selection */ -WebUtil.getStylesheets = function() { var i, links, sheets = []; - links = document.getElementsByTagName("link"); - for (i = 0; i < links.length; i += 1) { +WebUtil.getStylesheets = function () { + "use strict"; + var links = document.getElementsByTagName("link"); + var sheets = []; + + for (var i = 0; i < links.length; i += 1) { if (links[i].title && links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { sheets.push(links[i]); @@ -202,14 +218,16 @@ WebUtil.getStylesheets = function() { var i, links, sheets = []; // No sheet means try and use value from cookie, null sheet used to // clear all alternates. -WebUtil.selectStylesheet = function(sheet) { - var i, link, sheets = WebUtil.getStylesheets(); +WebUtil.selectStylesheet = function (sheet) { + "use strict"; if (typeof sheet === 'undefined') { sheet = 'default'; } - for (i=0; i < sheets.length; i += 1) { - link = sheets[i]; - if (link.title === sheet) { + + var sheets = WebUtil.getStylesheets(); + for (var i = 0; i < sheets.length; i += 1) { + var link = sheets[i]; + if (link.title === sheet) { Util.Debug("Using stylesheet " + sheet); link.disabled = false; } else { diff --git a/tests/test.util.js b/tests/test.util.js index 63f51b7c..7d6524a3 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -24,13 +24,13 @@ describe('Utils', function() { describe('push16', function () { it('should push two bytes on to the array', function () { - var arr = [1]; + var arr = [1]; arr.push16(0xABCD); expect(arr).to.deep.equal([1, 0xAB, 0xCD]); }); it('should only use the two least significant bytes of any number passed in', function () { - var arr = [1]; + var arr = [1]; arr.push16(0xABCDEF); expect(arr).to.deep.equal([1, 0xCD, 0xEF]); }); @@ -38,19 +38,19 @@ describe('Utils', function() { describe('push32', function () { it('should push four bytes on to the array', function () { - var arr = [1]; + var arr = [1]; arr.push32(0xABCDEF12); expect(arr).to.deep.equal([1, 0xAB, 0xCD, 0xEF, 0x12]); }); it('should only use the four least significant bytes of any number passed in', function () { - var arr = [1]; + var arr = [1]; arr.push32(0xABCDEF1234); expect(arr).to.deep.equal([1, 0xCD, 0xEF, 0x12, 0x34]); }); }); }); - + describe('logging functions', function () { beforeEach(function () { sinon.spy(console, 'log'); @@ -98,7 +98,7 @@ describe('Utils', function() { // TODO(directxman12): test decodeUTF8 // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent) // TODO(directxman12): figure out a good way to test getPosition and getEventPosition - // TODO(directxman12): figure out how to test the browser detection functions properly + // TODO(directxman12): figure out how to test the browser detection functions properly // (we can't really test them against the browsers, except for Gecko // via PhantomJS, the default test driver) // TODO(directxman12): figure out how to test Util.Flash From 2cccf7530c272de3627fa871da8e6ed44f437edb Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 22 May 2014 16:57:55 -0400 Subject: [PATCH 010/527] Cleanup: WebSocket Helper File: websock.js Tests Added: True Changes: - Cleaned up JSHint errors - Converted to normal JS constructor pattern with "private" fields and methods now simply being prepended by underscores - Added a "bind" polyfill for use in PhantomJS 1.x in util.js - Added FakeWebSocket to fill in for actual WebSocket objects when testing - Made exception handler actually log exception name and message, to console, in addition to stack trace --- include/websock.js | 642 +++++++++++++++++++--------------------- tests/fake.websocket.js | 96 ++++++ tests/test.websock.js | 480 ++++++++++++++++++++++++++++++ 3 files changed, 873 insertions(+), 345 deletions(-) create mode 100644 tests/fake.websocket.js create mode 100644 tests/test.websock.js diff --git a/include/websock.js b/include/websock.js index 0e4718ab..bd3179a6 100644 --- a/include/websock.js +++ b/include/websock.js @@ -14,7 +14,7 @@ * read binary data off of the receive queue. */ -/*jslint browser: true, bitwise: false, plusplus: false */ +/*jslint browser: true, bitwise: true */ /*global Util, Base64 */ @@ -43,382 +43,334 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { } Util.load_scripts(["web-socket-js/swfobject.js", "web-socket-js/web_socket.js"]); - }()); + })(); } function Websock() { -"use strict"; + "use strict"; -var api = {}, // Public API - websocket = null, // WebSocket object - mode = 'base64', // Current WebSocket mode: 'binary', 'base64' - rQ = [], // Receive queue - rQi = 0, // Receive queue index - rQmax = 10000, // Max receive queue size before compacting - sQ = [], // Send queue + this._websocket = null; // WebSocket object + this._rQ = []; // Receive queue + this._rQi = 0; // Receive queue index + this._rQmax = 10000; // Max receive queue size before compacting + this._sQ = []; // Send queue - eventHandlers = { - 'message' : function() {}, - 'open' : function() {}, - 'close' : function() {}, - 'error' : function() {} - }, + this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64' + this.maxBufferedAmount = 200; - test_mode = false; - - -// -// Queue public functions -// - -function get_sQ() { - return sQ; + this._eventHandlers = { + 'message': function () {}, + 'open': function () {}, + 'close': function () {}, + 'error': function () {} + }; } -function get_rQ() { - return rQ; -} -function get_rQi() { - return rQi; -} -function set_rQi(val) { - rQi = val; -} +(function () { + "use strict"; + Websock.prototype = { + // Getters and Setters + get_sQ: function () { + return this._sQ; + }, -function rQlen() { - return rQ.length - rQi; -} + get_rQ: function () { + return this._rQ; + }, -function rQpeek8() { - return (rQ[rQi] ); -} -function rQshift8() { - return (rQ[rQi++] ); -} -function rQunshift8(num) { - if (rQi === 0) { - rQ.unshift(num); - } else { - rQi -= 1; - rQ[rQi] = num; - } + get_rQi: function () { + return this._rQi; + }, -} -function rQshift16() { - return (rQ[rQi++] << 8) + - (rQ[rQi++] ); -} -function rQshift32() { - return (rQ[rQi++] << 24) + - (rQ[rQi++] << 16) + - (rQ[rQi++] << 8) + - (rQ[rQi++] ); -} -function rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = rQlen(); } - var arr = rQ.slice(rQi, rQi + len); - rQi += len; - return String.fromCharCode.apply(null, arr); -} -function rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = rQlen(); } - rQi += len; - return rQ.slice(rQi-len, rQi); -} + set_rQi: function (val) { + this._rQi = val; + }, -function rQslice(start, end) { - if (end) { - return rQ.slice(rQi + start, rQi + end); - } else { - return rQ.slice(rQi + start); - } -} + // Receive Queue + rQlen: function () { + return this._rQ.length - this._rQi; + }, -// Check to see if we must wait for 'num' bytes (default to FBU.bytes) -// to be available in the receive queue. Return true if we need to -// wait (and possibly print a debug message), otherwise false. -function rQwait(msg, num, goback) { - var rQlen = rQ.length - rQi; // Skip rQlen() function call - if (rQlen < num) { - if (goback) { - if (rQi < goback) { - throw("rQwait cannot backup " + goback + " bytes"); + rQpeek8: function () { + return this._rQ[this._rQi]; + }, + + rQshift8: function () { + return this._rQ[this._rQi++]; + }, + + rQunshift8: function (num) { + if (this._rQi === 0) { + this._rQ.unshift(num); + } else { + this._rQi--; + this._rQ[this._rQi] = num; } - rQi -= goback; - } - //Util.Debug(" waiting for " + (num-rQlen) + - // " " + msg + " byte(s)"); - return true; // true means need more data - } - return false; -} + }, -// -// Private utility routines -// + rQshift16: function () { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, -function encode_message() { - if (mode === 'binary') { - // Put in a binary arraybuffer - return (new Uint8Array(sQ)).buffer; - } else { - // base64 encode - return Base64.encode(sQ); - } -} + rQshift32: function () { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, -function decode_message(data) { - //Util.Debug(">> decode_message: " + data); - if (mode === 'binary') { - // push arraybuffer values onto the end - var u8 = new Uint8Array(data); - for (var i = 0; i < u8.length; i++) { - rQ.push(u8[i]); - } - } else { - // base64 decode and concat to the end - rQ = rQ.concat(Base64.decode(data, 0)); - } - //Util.Debug(">> decode_message, rQ: " + rQ); -} + rQshiftStr: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + var arr = this._rQ.slice(this._rQi, this._rQi + len); + this._rQi += len; + return String.fromCharCode.apply(null, arr); + }, + rQshiftBytes: function (len) { + if (typeof(len) === 'undefined') { len = this.rQlen(); } + this._rQi += len; + return this._rQ.slice(this._rQi - len, this._rQi); + }, -// -// Public Send functions -// - -function flush() { - if (websocket.bufferedAmount !== 0) { - Util.Debug("bufferedAmount: " + websocket.bufferedAmount); - } - if (websocket.bufferedAmount < api.maxBufferedAmount) { - //Util.Debug("arr: " + arr); - //Util.Debug("sQ: " + sQ); - if (sQ.length > 0) { - websocket.send(encode_message(sQ)); - sQ = []; - } - return true; - } else { - Util.Info("Delaying send, bufferedAmount: " + - websocket.bufferedAmount); - return false; - } -} - -// overridable for testing -function send(arr) { - //Util.Debug(">> send_array: " + arr); - sQ = sQ.concat(arr); - return flush(); -} - -function send_string(str) { - //Util.Debug(">> send_string: " + str); - api.send(str.split('').map( - function (chr) { return chr.charCodeAt(0); } ) ); -} - -// -// Other public functions - -function recv_message(e) { - //Util.Debug(">> recv_message: " + e.data.length); - - try { - decode_message(e.data); - if (rQlen() > 0) { - eventHandlers.message(); - // Compact the receive queue - if (rQ.length > rQmax) { - //Util.Debug("Compacting receive queue"); - rQ = rQ.slice(rQi); - rQi = 0; + rQslice: function (start, end) { + if (end) { + return this._rQ.slice(this._rQi + start, this._rQi + end); + } else { + return this._rQ.slice(this._rQi + start); } - } else { - Util.Debug("Ignoring empty message"); - } - } catch (exc) { - if (typeof exc.stack !== 'undefined') { - Util.Warn("recv_message, caught exception: " + exc.stack); - } else if (typeof exc.description !== 'undefined') { - Util.Warn("recv_message, caught exception: " + exc.description); - } else { - Util.Warn("recv_message, caught exception:" + exc); - } - if (typeof exc.name !== 'undefined') { - eventHandlers.error(exc.name + ": " + exc.message); - } else { - eventHandlers.error(exc); - } - } - //Util.Debug("<< recv_message"); -} + }, + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait: function (msg, num, goback) { + var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error("rQwait cannot backup " + goback + " bytes"); + } + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + }, -// Set event handlers -function on(evt, handler) { - eventHandlers[evt] = handler; -} + // Send Queue -function init(protocols, ws_schema) { - rQ = []; - rQi = 0; - sQ = []; - websocket = null; + flush: function () { + if (this._websocket.bufferedAmount !== 0) { + Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); + } - var bt = false, - wsbt = false, - try_binary = false; + if (this._websocket.bufferedAmount < this.maxBufferedAmount) { + if (this._sQ.length > 0) { + this._websocket.send(this._encode_message()); + this._sQ = []; + } - // Check for full typed array support - if (('Uint8Array' in window) && - ('set' in Uint8Array.prototype)) { - bt = true; - } - // Check for full binary type support in WebSocket - // Inspired by: - // https://github.com/Modernizr/Modernizr/issues/370 - // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js - try { - if (bt && ('binaryType' in WebSocket.prototype || - !!(new WebSocket(ws_schema + '://.').binaryType))) { - Util.Info("Detected binaryType support in WebSockets"); - wsbt = true; - } - } catch (exc) { - // Just ignore failed test localhost connections - } + return true; + } else { + Util.Info("Delaying send, bufferedAmount: " + + this._websocket.bufferedAmount); + return false; + } + }, - // Default protocols if not specified - if (typeof(protocols) === "undefined") { - if (wsbt) { - protocols = ['binary', 'base64']; - } else { - protocols = 'base64'; - } - } + send: function (arr) { + this._sQ = this._sQ.concat(arr); + return this.flush(); + }, - // If no binary support, make sure it was not requested - if (!wsbt) { - if (protocols === 'binary') { - throw("WebSocket binary sub-protocol requested but not supported"); - } - if (typeof(protocols) === "object") { - var new_protocols = []; - for (var i = 0; i < protocols.length; i++) { - if (protocols[i] === 'binary') { - Util.Error("Skipping unsupported WebSocket binary sub-protocol"); + send_string: function (str) { + this.send(str.split('').map(function (chr) { + return chr.charCodeAt(0); + })); + }, + + // Event Handlers + on: function (evt, handler) { + this._eventHandlers[evt] = handler; + }, + + init: function (protocols, ws_schema) { + this._rQ = []; + this._rQi = 0; + this._sQ = []; + this._websocket = null; + + // Check for full typed array support + var bt = false; + if (('Uint8Array' in window) && + ('set' in Uint8Array.prototype)) { + bt = true; + } + + // Check for full binary type support in WebSockets + // Inspired by: + // https://github.com/Modernizr/Modernizr/issues/370 + // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js + var wsbt = false; + try { + if (bt && ('binaryType' in WebSocket.prototype || + !!(new WebSocket(ws_schema + '://.').binaryType))) { + Util.Info("Detected binaryType support in WebSockets"); + wsbt = true; + } + } catch (exc) { + // Just ignore failed test localhost connection + } + + // Default protocols if not specified + if (typeof(protocols) === "undefined") { + if (wsbt) { + protocols = ['binary', 'base64']; } else { - new_protocols.push(protocols[i]); + protocols = 'base64'; } } - if (new_protocols.length > 0) { - protocols = new_protocols; + + if (!wsbt) { + if (protocols === 'binary') { + throw new Error('WebSocket binary sub-protocol requested but not supported'); + } + + if (typeof(protocols) === 'object') { + var new_protocols = []; + + for (var i = 0; i < protocols.length; i++) { + if (protocols[i] === 'binary') { + Util.Error('Skipping unsupported WebSocket binary sub-protocol'); + } else { + new_protocols.push(protocols[i]); + } + } + + if (new_protocols.length > 0) { + protocols = new_protocols; + } else { + throw new Error("Only WebSocket binary sub-protocol was requested and is not supported."); + } + } + } + + return protocols; + }, + + open: function (uri, protocols) { + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + protocols = this.init(protocols, ws_schema); + + this._websocket = new WebSocket(uri, protocols); + + if (protocols.indexOf('binary') >= 0) { + this._websocket.binaryType = 'arraybuffer'; + } + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = (function () { + Util.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + this._mode = this._websocket.protocol; + Util.Info("Server choose sub-protocol: " + this._websocket.protocol); + } else { + this._mode = 'base64'; + Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); + } + this._eventHandlers.open(); + Util.Debug("<< WebSock.onopen"); + }).bind(this); + this._websocket.onclose = (function (e) { + Util.Debug(">> WebSock.onclose"); + this._eventHandlers.close(e); + Util.Debug("<< WebSock.onclose"); + }).bind(this); + this._websocket.onerror = (function (e) { + Util.Debug(">> WebSock.onerror: " + e); + this._eventHandlers.error(e); + Util.Debug("<< WebSock.onerror: " + e); + }).bind(this); + }, + + close: function () { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) || + (this._websocket.readyState === WebSocket.CONNECTING)) { + Util.Info("Closing WebSocket connection"); + this._websocket.close(); + } + + this._websocket.onmessage = function (e) { return; }; + } + }, + + // private methods + _encode_message: function () { + if (this._mode === 'binary') { + // Put in a binary arraybuffer + return (new Uint8Array(this._sQ)).buffer; } else { - throw("Only WebSocket binary sub-protocol was requested and not supported."); + // base64 encode + return Base64.encode(this._sQ); + } + }, + + _decode_message: function (data) { + if (this._mode === 'binary') { + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + for (var i = 0; i < u8.length; i++) { + this._rQ.push(u8[i]); + } + } else { + // base64 decode and concat to end + this._rQ = this._rQ.concat(Base64.decode(data, 0)); + } + }, + + _recv_message: function (e) { + try { + this._decode_message(e.data); + if (this.rQlen() > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQ.length > this._rQmax) { + this._rQ = this._rQ.slice(this._rQi); + this._rQi = 0; + } + } else { + Util.Debug("Ignoring empty message"); + } + } catch (exc) { + var exception_str = ""; + if (exc.name) { + exception_str += "\n name: " + exc.name + "\n"; + exception_str += " message: " + exc.message + "\n"; + } + + if (typeof exc.description !== 'undefined') { + exception_str += " description: " + exc.description + "\n"; + } + + if (typeof exc.stack !== 'undefined') { + exception_str += exc.stack; + } + + if (exception_str.length > 0) { + Util.Error("recv_message, caught exception: " + exception_str); + } else { + Util.Error("recv_message, caught exception: " + exc); + } + + if (typeof exc.name !== 'undefined') { + this._eventHandlers.error(exc.name + ": " + exc.message); + } else { + this._eventHandlers.error(exc); + } } } - } - - return protocols; -} - -function open(uri, protocols) { - var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; - protocols = init(protocols, ws_schema); - - if (test_mode) { - websocket = {}; - } else { - websocket = new WebSocket(uri, protocols); - if (protocols.indexOf('binary') >= 0) { - websocket.binaryType = 'arraybuffer'; - } - } - - websocket.onmessage = recv_message; - websocket.onopen = function() { - Util.Debug(">> WebSock.onopen"); - if (websocket.protocol) { - mode = websocket.protocol; - Util.Info("Server chose sub-protocol: " + websocket.protocol); - } else { - mode = 'base64'; - Util.Error("Server select no sub-protocol!: " + websocket.protocol); - } - eventHandlers.open(); - Util.Debug("<< WebSock.onopen"); }; - websocket.onclose = function(e) { - Util.Debug(">> WebSock.onclose"); - eventHandlers.close(e); - Util.Debug("<< WebSock.onclose"); - }; - websocket.onerror = function(e) { - Util.Debug(">> WebSock.onerror: " + e); - eventHandlers.error(e); - Util.Debug("<< WebSock.onerror"); - }; -} - -function close() { - if (websocket) { - if ((websocket.readyState === WebSocket.OPEN) || - (websocket.readyState === WebSocket.CONNECTING)) { - Util.Info("Closing WebSocket connection"); - websocket.close(); - } - websocket.onmessage = function (e) { return; }; - } -} - -// Override internal functions for testing -// Takes a send function, returns reference to recv function -function testMode(override_send, data_mode) { - test_mode = true; - mode = data_mode; - api.send = override_send; - api.close = function () {}; - return recv_message; -} - -function constructor() { - // Configuration settings - api.maxBufferedAmount = 200; - - // Direct access to send and receive queues - api.get_sQ = get_sQ; - api.get_rQ = get_rQ; - api.get_rQi = get_rQi; - api.set_rQi = set_rQi; - - // Routines to read from the receive queue - api.rQlen = rQlen; - api.rQpeek8 = rQpeek8; - api.rQshift8 = rQshift8; - api.rQunshift8 = rQunshift8; - api.rQshift16 = rQshift16; - api.rQshift32 = rQshift32; - api.rQshiftStr = rQshiftStr; - api.rQshiftBytes = rQshiftBytes; - api.rQslice = rQslice; - api.rQwait = rQwait; - - api.flush = flush; - api.send = send; - api.send_string = send_string; - - api.on = on; - api.init = init; - api.open = open; - api.close = close; - api.testMode = testMode; - - return api; -} - -return constructor(); - -} +})(); diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js new file mode 100644 index 00000000..749c0eaf --- /dev/null +++ b/tests/fake.websocket.js @@ -0,0 +1,96 @@ +var FakeWebSocket; + +(function () { + // PhantomJS can't create Event objects directly, so we need to use this + function make_event(name, props) { + var evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + if (props) { + for (var prop in props) { + evt[prop] = props[prop]; + } + } + return evt; + } + + FakeWebSocket = function (uri, protocols) { + this.url = uri; + this.binaryType = "arraybuffer"; + this.extensions = ""; + + if (!protocols || typeof protocols === 'string') { + this.protocol = protocols; + } else { + this.protocol = protocols[0]; + } + + this._send_queue = new Uint8Array(20000); + + this.readyState = FakeWebSocket.CONNECTING; + this.bufferedAmount = 0; + + this.__is_fake = true; + }; + + FakeWebSocket.prototype = { + close: function (code, reason) { + this.readyState = FakeWebSocket.CLOSED; + if (this.onclose) { + this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true })); + } + }, + + send: function (data) { + if (this.protocol == 'base64') { + data = Base64.decode(data); + } else { + data = new Uint8Array(data); + } + this._send_queue.set(data, this.bufferedAmount); + this.bufferedAmount += data.length; + }, + + _get_sent_data: function () { + var arr = []; + for (var i = 0; i < this.bufferedAmount; i++) { + arr[i] = this._send_queue[i]; + } + + this.bufferedAmount = 0; + + return arr; + }, + + _open: function (data) { + this.readyState = FakeWebSocket.OPEN; + if (this.onopen) { + this.onopen(make_event('open')); + } + }, + + _receive_data: function (data) { + this.onmessage(make_event("message", { 'data': data })); + } + }; + + FakeWebSocket.OPEN = WebSocket.OPEN; + FakeWebSocket.CONNECTING = WebSocket.CONNECTING; + FakeWebSocket.CLOSING = WebSocket.CLOSING; + FakeWebSocket.CLOSED = WebSocket.CLOSED; + + FakeWebSocket.__is_fake = true; + + FakeWebSocket.replace = function () { + if (!WebSocket.__is_fake) { + var real_version = WebSocket; + WebSocket = FakeWebSocket; + FakeWebSocket.__real_version = real_version; + } + }; + + FakeWebSocket.restore = function () { + if (WebSocket.__is_fake) { + WebSocket = WebSocket.__real_version; + } + }; +})(); diff --git a/tests/test.websock.js b/tests/test.websock.js new file mode 100644 index 00000000..7d242d3e --- /dev/null +++ b/tests/test.websock.js @@ -0,0 +1,480 @@ +// requires local modules: websock, base64, util +// requires test modules: fake.websocket +/* jshint expr: true */ +var assert = chai.assert; +var expect = chai.expect; + +describe('Websock', function() { + "use strict"; + + describe('Queue methods', function () { + var sock; + var RQ_TEMPLATE = [0, 1, 2, 3, 4, 5, 6, 7]; + + beforeEach(function () { + sock = new Websock(); + for (var i = RQ_TEMPLATE.length - 1; i >= 0; i--) { + sock.rQunshift8(RQ_TEMPLATE[i]); + } + }); + describe('rQlen', function () { + it('should return the length of the receive queue', function () { + sock.set_rQi(0); + + expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length); + }); + + it("should return the proper length if we read some from the receive queue", function () { + sock.set_rQi(1); + + expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1); + }); + }); + + describe('rQpeek8', function () { + it('should peek at the next byte without poping it off the queue', function () { + var bef_len = sock.rQlen(); + var peek = sock.rQpeek8(); + expect(sock.rQpeek8()).to.equal(peek); + expect(sock.rQlen()).to.equal(bef_len); + }); + }); + + describe('rQshift8', function () { + it('should pop a single byte from the receive queue', function () { + var peek = sock.rQpeek8(); + var bef_len = sock.rQlen(); + expect(sock.rQshift8()).to.equal(peek); + expect(sock.rQlen()).to.equal(bef_len - 1); + }); + }); + + describe('rQunshift8', function () { + it('should place a byte at the front of the queue', function () { + sock.rQunshift8(255); + expect(sock.rQpeek8()).to.equal(255); + expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length + 1); + }); + }); + + describe('rQshift16', function () { + it('should pop two bytes from the receive queue and return a single number', function () { + var bef_len = sock.rQlen(); + var expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; + expect(sock.rQshift16()).to.equal(expected); + expect(sock.rQlen()).to.equal(bef_len - 2); + }); + }); + + describe('rQshift32', function () { + it('should pop four bytes from the receive queue and return a single number', function () { + var bef_len = sock.rQlen(); + var expected = (RQ_TEMPLATE[0] << 24) + + (RQ_TEMPLATE[1] << 16) + + (RQ_TEMPLATE[2] << 8) + + RQ_TEMPLATE[3]; + expect(sock.rQshift32()).to.equal(expected); + expect(sock.rQlen()).to.equal(bef_len - 4); + }); + }); + + describe('rQshiftStr', function () { + it('should shift the given number of bytes off of the receive queue and return a string', function () { + var bef_len = sock.rQlen(); + var bef_rQi = sock.get_rQi(); + var shifted = sock.rQshiftStr(3); + expect(shifted).to.be.a('string'); + expect(shifted).to.equal(String.fromCharCode.apply(null, RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3))); + expect(sock.rQlen()).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftStr(); + expect(sock.rQlen()).to.equal(0); + }); + }); + + describe('rQshiftBytes', function () { + it('should shift the given number of bytes of the receive queue and return an array', function () { + var bef_len = sock.rQlen(); + var bef_rQi = sock.get_rQi(); + var shifted = sock.rQshiftBytes(3); + expect(shifted).to.be.an.instanceof(Array); + expect(shifted).to.deep.equal(RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3)); + expect(sock.rQlen()).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftBytes(); + expect(sock.rQlen()).to.equal(0); + }); + }); + + describe('rQslice', function () { + beforeEach(function () { + sock.set_rQi(0); + }); + + it('should not modify the receive queue', function () { + var bef_len = sock.rQlen(); + sock.rQslice(0, 2); + expect(sock.rQlen()).to.equal(bef_len); + }); + + it('should return an array containing the given slice of the receive queue', function () { + var sl = sock.rQslice(0, 2); + expect(sl).to.be.an.instanceof(Array); + expect(sl).to.deep.equal(RQ_TEMPLATE.slice(0, 2)); + }); + + it('should use the rest of the receive queue if no end is given', function () { + var sl = sock.rQslice(1); + expect(sl).to.have.length(RQ_TEMPLATE.length - 1); + expect(sl).to.deep.equal(RQ_TEMPLATE.slice(1)); + }); + + it('should take the current rQi in to account', function () { + sock.set_rQi(1); + expect(sock.rQslice(0, 2)).to.deep.equal(RQ_TEMPLATE.slice(1, 3)); + }); + }); + + describe('rQwait', function () { + beforeEach(function () { + sock.set_rQi(0); + }); + + it('should return true if there are not enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true; + }); + + it('should return false if there are enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false; + }); + + it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { + sock.set_rQi(5); + expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; + expect(sock.get_rQi()).to.equal(1); + }); + + it('should raise an error if we try to go back more than possible', function () { + sock.set_rQi(5); + expect(function () { sock.rQwait('hi', RQ_TEMPLATE.length, 6); }).to.throw(Error); + }); + + it('should not reduce rQi if there are enough bytes', function () { + sock.set_rQi(5); + sock.rQwait('hi', 1, 6); + expect(sock.get_rQi()).to.equal(5); + }); + }); + + describe('flush', function () { + beforeEach(function () { + sock._websocket = { + send: sinon.spy() + }; + }); + + it('should actually send on the websocket if the websocket does not have too much buffered', function () { + sock.maxBufferedAmount = 10; + sock._websocket.bufferedAmount = 8; + sock._sQ = [1, 2, 3]; + var encoded = sock._encode_message(); + + sock.flush(); + expect(sock._websocket.send).to.have.been.calledOnce; + expect(sock._websocket.send).to.have.been.calledWith(encoded); + }); + + it('should return true if the websocket did not have too much buffered', function () { + sock.maxBufferedAmount = 10; + sock._websocket.bufferedAmount = 8; + + expect(sock.flush()).to.be.true; + }); + + it('should not call send if we do not have anything queued up', function () { + sock._sQ = []; + sock.maxBufferedAmount = 10; + sock._websocket.bufferedAmount = 8; + + sock.flush(); + + expect(sock._websocket.send).not.to.have.been.called; + }); + + it('should not send and return false if the websocket has too much buffered', function () { + sock.maxBufferedAmount = 10; + sock._websocket.bufferedAmount = 12; + + expect(sock.flush()).to.be.false; + expect(sock._websocket.send).to.not.have.been.called; + }); + }); + + describe('send', function () { + beforeEach(function () { + sock.flush = sinon.spy(); + }); + + it('should add to the send queue', function () { + sock.send([1, 2, 3]); + var sq = sock.get_sQ(); + expect(sock.get_sQ().slice(sq.length - 3)).to.deep.equal([1, 2, 3]); + }); + + it('should call flush', function () { + sock.send([1, 2, 3]); + expect(sock.flush).to.have.been.calledOnce; + }); + }); + + describe('send_string', function () { + beforeEach(function () { + sock.send = sinon.spy(); + }); + + it('should call send after converting the string to an array', function () { + sock.send_string("\x01\x02\x03"); + expect(sock.send).to.have.been.calledWith([1, 2, 3]); + }); + }); + }); + + describe('lifecycle methods', function () { + var old_WS; + before(function () { + old_WS = WebSocket; + }); + + var sock; + beforeEach(function () { + sock = new Websock(); + WebSocket = sinon.spy(); + WebSocket.OPEN = old_WS.OPEN; + WebSocket.CONNECTING = old_WS.CONNECTING; + WebSocket.CLOSING = old_WS.CLOSING; + WebSocket.CLOSED = old_WS.CLOSED; + }); + + describe('opening', function () { + it('should pick the correct protocols if none are given' , function () { + + }); + + it('should open the actual websocket', function () { + sock.open('ws://localhost:8675', 'base64'); + expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'base64'); + }); + + it('should fail if we try to use binary but do not support it', function () { + expect(function () { sock.open('ws:///', 'binary'); }).to.throw(Error); + }); + + it('should fail if we specified an array with only binary and we do not support it', function () { + expect(function () { sock.open('ws:///', ['binary']); }).to.throw(Error); + }); + + it('should skip binary if we have multiple options for encoding and do not support binary', function () { + sock.open('ws:///', ['binary', 'base64']); + expect(WebSocket).to.have.been.calledWith('ws:///', ['base64']); + }); + // it('should initialize the event handlers')? + }); + + describe('closing', function () { + beforeEach(function () { + sock.open('ws://'); + sock._websocket.close = sinon.spy(); + }); + + it('should close the actual websocket if it is open', function () { + sock._websocket.readyState = WebSocket.OPEN; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should close the actual websocket if it is connecting', function () { + sock._websocket.readyState = WebSocket.CONNECTING; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should not try to close the actual websocket if closing', function () { + sock._websocket.readyState = WebSocket.CLOSING; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should not try to close the actual websocket if closed', function () { + sock._websocket.readyState = WebSocket.CLOSED; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should reset onmessage to not call _recv_message', function () { + sinon.spy(sock, '_recv_message'); + sock.close(); + sock._websocket.onmessage(null); + try { + expect(sock._recv_message).not.to.have.been.called; + } finally { + sock._recv_message.restore(); + } + }); + }); + + describe('event handlers', function () { + beforeEach(function () { + sock._recv_message = sinon.spy(); + sock.on('open', sinon.spy()); + sock.on('close', sinon.spy()); + sock.on('error', sinon.spy()); + sock.open('ws://'); + }); + + it('should call _recv_message on a message', function () { + sock._websocket.onmessage(null); + expect(sock._recv_message).to.have.been.calledOnce; + }); + + it('should copy the mode over upon opening', function () { + sock._websocket.protocol = 'cheese'; + sock._websocket.onopen(); + expect(sock._mode).to.equal('cheese'); + }); + + it('should assume base64 if no protocol was available on opening', function () { + sock._websocket.protocol = null; + sock._websocket.onopen(); + expect(sock._mode).to.equal('base64'); + }); + + it('should call the open event handler on opening', function () { + sock._websocket.onopen(); + expect(sock._eventHandlers.open).to.have.been.calledOnce; + }); + + it('should call the close event handler on closing', function () { + sock._websocket.onclose(); + expect(sock._eventHandlers.close).to.have.been.calledOnce; + }); + + it('should call the error event handler on error', function () { + sock._websocket.onerror(); + expect(sock._eventHandlers.error).to.have.been.calledOnce; + }); + }); + + after(function () { + WebSocket = old_WS; + }); + }); + + describe('WebSocket Receiving', function () { + var sock; + beforeEach(function () { + sock = new Websock(); + }); + + it('should support decoding base64 string data to add it to the receive queue', function () { + var msg = { data: Base64.encode([1, 2, 3]) }; + sock._mode = 'base64'; + sock._recv_message(msg); + expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); + }); + + it('should support adding binary Uint8Array data to the receive queue', function () { + var msg = { data: new Uint8Array([1, 2, 3]) }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); + }); + + it('should call the message event handler if present', function () { + sock._eventHandlers.message = sinon.spy(); + var msg = { data: Base64.encode([1, 2, 3]) }; + sock._mode = 'base64'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).to.have.been.calledOnce; + }); + + it('should not call the message event handler if there is nothing in the receive queue', function () { + sock._eventHandlers.message = sinon.spy(); + var msg = { data: Base64.encode([]) }; + sock._mode = 'base64'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).not.to.have.been.called; + }); + + it('should compact the receive queue', function () { + // NB(sross): while this is an internal implementation detail, it's important to + // test, otherwise the receive queue could become very large very quickly + sock._rQ = [0, 1, 2, 3, 4, 5]; + sock.set_rQi(6); + sock._rQmax = 3; + var msg = { data: Base64.encode([1, 2, 3]) }; + sock._mode = 'base64'; + sock._recv_message(msg); + expect(sock._rQ.length).to.equal(3); + expect(sock.get_rQi()).to.equal(0); + }); + + it('should call the error event handler on an exception', function () { + sock._eventHandlers.error = sinon.spy(); + sock._eventHandlers.message = sinon.stub().throws(); + var msg = { data: Base64.encode([1, 2, 3]) }; + sock._mode = 'base64'; + sock._recv_message(msg); + expect(sock._eventHandlers.error).to.have.been.calledOnce; + }); + }); + + describe('Data encoding', function () { + before(function () { FakeWebSocket.replace(); }); + after(function () { FakeWebSocket.restore(); }); + + describe('as binary data', function () { + var sock; + beforeEach(function () { + sock = new Websock(); + sock.open('ws://', 'binary'); + sock._websocket._open(); + }); + + it('should convert the send queue into an ArrayBuffer', function () { + sock._sQ = [1, 2, 3]; + var res = sock._encode_message(); // An ArrayBuffer + expect(new Uint8Array(res)).to.deep.equal(new Uint8Array(res)); + }); + + it('should properly pass the encoded data off to the actual WebSocket', function () { + sock.send([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]); + }); + }); + + describe('as Base64 data', function () { + var sock; + beforeEach(function () { + sock = new Websock(); + sock.open('ws://', 'base64'); + sock._websocket._open(); + }); + + it('should convert the send queue into a Base64-encoded string', function () { + sock._sQ = [1, 2, 3]; + expect(sock._encode_message()).to.equal(Base64.encode([1, 2, 3])); + }); + + it('should properly pass the encoded data off to the actual WebSocket', function () { + sock.send([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]); + }); + + }); + + }); +}); From 1e13775bd5e37173b6c8a814811ea7f16e56f892 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 11 Jun 2014 13:35:53 -0400 Subject: [PATCH 011/527] Cleanup: Display Helper File: display.js Tests Added: True (preliminary) Changes: - De-crockford-ified the file NOTE: the tests included for display.js cover basic functionality, but are by no means nearly as comprehensive as the ones presented for rfb.js. --- include/display.js | 1422 ++++++++++++++++++++--------------------- tests/test.display.js | 332 ++++++++++ 2 files changed, 1025 insertions(+), 729 deletions(-) create mode 100644 tests/test.display.js diff --git a/include/display.js b/include/display.js index 9f2d6b89..e8ac63d8 100644 --- a/include/display.js +++ b/include/display.js @@ -6,765 +6,729 @@ * See README.md for usage and integration instructions. */ -/*jslint browser: true, white: false, bitwise: false */ +/*jslint browser: true, white: false */ /*global Util, Base64, changeCursor */ -function Display(defaults) { -"use strict"; +var Display; -var that = {}, // Public API methods - conf = {}, // Configuration attributes +(function () { + "use strict"; - // Private Display namespace variables - c_ctx = null, - c_forceCanvas = false, + Display = function (defaults) { + this._drawCtx = null; + this._c_forceCanvas = false; - // Queued drawing actions for in-order rendering - renderQ = [], + this._renderQ = []; // queue drawing actions for in-oder rendering - // Predefine function variables (jslint) - imageDataGet, rgbImageData, bgrxImageData, cmapImageData, - setFillColor, rescale, scan_renderQ, + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; - // The full frame buffer (logical canvas) size - fb_width = 0, - fb_height = 0, - // The visible "physical canvas" viewport - viewport = {'x': 0, 'y': 0, 'w' : 0, 'h' : 0 }, - cleanRect = {'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1}, + // the visible "physical canvas" viewport + this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; + this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; - c_prevStyle = "", - tile = null, - tile16x16 = null, - tile_x = 0, - tile_y = 0; + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; + Util.set_defaults(this, defaults, { + 'true_color': true, + 'colourMap': [], + 'scale': 1.0, + 'viewport': false, + 'render_mode': '' + }); -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', null, 'Canvas element for rendering'], - ['context', 'ro', 'raw', null, 'Canvas 2D context for rendering (read-only)'], - ['logo', 'rw', 'raw', null, 'Logo to display when cleared: {"width": width, "height": height, "data": data}'], - ['true_color', 'rw', 'bool', true, 'Use true-color pixel data'], - ['colourMap', 'rw', 'arr', [], 'Colour map array (when not true-color)'], - ['scale', 'rw', 'float', 1.0, 'Display area scale factor 0.0 - 1.0'], - ['viewport', 'rw', 'bool', false, 'Use a viewport set with viewportChange()'], - ['width', 'rw', 'int', null, 'Display area width'], - ['height', 'rw', 'int', null, 'Display area height'], + Util.Debug(">> Display.constructor"); - ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'], + if (!this._target) { + throw new Error("Target must be set"); + } - ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'], - ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI'] + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error("no getContext method"); + } + + if (!this._drawCtx) { + this._drawCtx = this._target.getContext('2d'); + } + + Util.Debug("User Agent: " + navigator.userAgent); + if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); } + if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); } + if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); } + if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); } + + this.clear(); + + // Check canvas features + if ('createImageData' in this._drawCtx) { + this._render_mode = 'canvas rendering'; + } else { + throw new Error("Canvas does not support createImageData"); + } + + if (this._prefer_js === null) { + Util.Info("Prefering javascript operations"); + this._prefer_js = true; + } + + // Determine browser support for setting the cursor via data URI scheme + var curDat = []; + for (var i = 0; i < 8 * 8 * 4; i++) { + curDat.push(255); + } + try { + var curSave = this._target.style.cursor; + Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8); + if (this._target.style.cursor) { + if (this._cursor_uri === null) { + this._cursor_uri = true; + } + Util.Info("Data URI scheme cursor supported"); + } else { + if (this._cursor_uri === null) { + this._cursor_uri = false; + } + Util.Warn("Data URI scheme cursor not supported"); + } + this._target.style.cursor = curSave; + } catch (exc) { + Util.Error("Data URI scheme cursor test exception: " + exc); + this._cursor_uri = false; + } + + Util.Debug("<< Display.constructor"); + }; + + Display.prototype = { + // Public methods + viewportChange: function (deltaX, deltaY, width, height) { + var vp = this._viewportLoc; + var cr = this._cleanRect; + var canvas = this._target; + + if (!this._viewport) { + Util.Debug("Setting viewport to full display region"); + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; + width = this._fb_width; + height = this._fb_height; + } + + if (typeof(deltaX) === "undefined") { deltaX = 0; } + if (typeof(deltaY) === "undefined") { deltaY = 0; } + if (typeof(width) === "undefined") { width = vp.w; } + if (typeof(height) === "undefined") { height = vp.h; } + + // Size change + if (width > this._fb_width) { width = this._fb_width; } + if (height > this._fb_height) { height = this._fb_height; } + + if (vp.w !== width || vp.h !== height) { + // Change width + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + vp.w = width; + + // Change height + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + vp.h = height; + + var saveImg = null; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + canvas.width = vp.w; + canvas.height = vp.h; + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } + + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } + + if (deltaX === 0 && deltaY === 0) { + return; + } + Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); + + vp.x += deltaX; + vx2 += deltaX; + vp.y += deltaY; + vy2 += deltaY; + + // Update the clean rectangle + if (vp.x > cr.x1) { + cr.x1 = vp.x; + } + if (vx2 < cr.x2) { + cr.x2 = vx2; + } + if (vp.y > cr.y1) { + cr.y1 = vp.y; + } + if (vy2 < cr.y2) { + cr.y2 = vy2; + } + + var x1, w; + if (deltaX < 0) { + // Shift viewport left, redraw left section + x1 = 0; + w = -deltaX; + } else { + // Shift viewport right, redraw right section + x1 = vp.w - deltaX; + w = deltaX; + } + + var y1, h; + if (deltaY < 0) { + // Shift viewport up, redraw top section + y1 = 0; + h = -deltaY; + } else { + // Shift viewport down, redraw bottom section + y1 = vp.h - deltaY; + h = deltaY; + } + + // Copy the valid part of the viewport to the shifted location + var saveStyle = this._drawCtx.fillStyle; + this._drawCtx.fillStyle = "rgb(255,255,255)"; + if (deltaX !== 0) { + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); + this._drawCtx.fillRect(x1, 0, w, vp.h); + } + if (deltaY !== 0) { + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h); + this._drawCtx.fillRect(0, y1, vp.w, h); + } + this._drawCtx.fillStyle = saveStyle; + }, + + // Return a map of clean and dirty areas of the viewport and reset the + // tracking of clean and dirty areas + // + // Returns: { 'cleanBox': { 'x': x, 'y': y, 'w': w, 'h': h}, + // 'dirtyBoxes': [{ 'x': x, 'y': y, 'w': w, 'h': h }, ...] } + getCleanDirtyReset: function () { + var vp = this._viewportLoc; + var cr = this._cleanRect; + + var cleanBox = { 'x': cr.x1, 'y': cr.y1, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y2 - cr.y1 + 1 }; + + var dirtyBoxes = []; + if (cr.x1 >= cr.x2 || cr.y1 >= cr.y2) { + // Whole viewport is dirty + dirtyBoxes.push({ 'x': vp.x, 'y': vp.y, 'w': vp.w, 'h': vp.h }); + } else { + // Redraw dirty regions + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + + if (vp.x < cr.x1) { + // left side dirty region + dirtyBoxes.push({'x': vp.x, 'y': vp.y, + 'w': cr.x1 - vp.x + 1, 'h': vp.h}); + } + if (vx2 > cr.x2) { + // right side dirty region + dirtyBoxes.push({'x': cr.x2 + 1, 'y': vp.y, + 'w': vx2 - cr.x2, 'h': vp.h}); + } + if(vp.y < cr.y1) { + // top/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': vp.y, + 'w': cr.x2 - cr.x1 + 1, 'h': cr.y1 - vp.y}); + } + if (vy2 > cr.y2) { + // bottom/middle dirty region + dirtyBoxes.push({'x': cr.x1, 'y': cr.y2 + 1, + 'w': cr.x2 - cr.x1 + 1, 'h': vy2 - cr.y2}); + } + } + + this._cleanRect = {'x1': vp.x, 'y1': vp.y, + 'x2': vp.x + vp.w - 1, 'y2': vp.y + vp.h - 1}; + + return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; + }, + + absX: function (x) { + return x + this._viewportLoc.x; + }, + + absY: function (y) { + return y + this._viewportLoc.y; + }, + + resize: function (width, height) { + this._prevDrawStyle = ""; + + this._fb_width = width; + this._fb_height = height; + + this._rescale(this._scale); + + this.viewportChange(); + }, + + clear: function () { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.blitStringImage(this._logo.data, 0, 0); + } else { + this.resize(640, 20); + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } + + this._renderQ = []; + }, + + fillRect: function (x, y, width, height, color) { + this._setFillColor(color); + this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + }, + + copyImage: function (old_x, old_y, new_x, new_y, w, h) { + var x1 = old_x - this._viewportLoc.x; + var y1 = old_y - this._viewportLoc.y; + var x2 = new_x - this._viewportLoc.x; + var y2 = new_y - this._viewportLoc.y; + + this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + }, + + // start updating a tile + startTile: function (x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); + } + + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + + var data = this._tile.data; + for (var i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } else { + this.fillRect(x, y, width, height, color); + } + }, + + // update sub-rectangle of the current tile + subTile: function (x, y, w, h, color) { + if (this._prefer_js) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + var red = bgr[2]; + var green = bgr[1]; + var blue = bgr[0]; + var xend = x + w; + var yend = y + h; + + var data = this._tile.data; + var width = this._tile.width; + for (var j = y; j < yend; j++) { + for (var i = x; i < xend; i++) { + var p = (i + (j * width)) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } else { + this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color); + } + }, + + // draw the current tile to the screen + finishTile: function () { + if (this._prefer_js) { + this._drawCtx.putImageData(this._tile, this._tile_x - this._viewportLoc.x, + this._tile_y - this._viewportLoc.y); + } + // else: No-op -- already done by setSubTile + }, + + 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); + } 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 () { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }.bind(this); + img.src = str; + return img; // for debugging purposes + }, + + // wrap ctx.drawImage but relative to viewport + drawImage: function (img, x, y) { + this._drawCtx.drawImage(img, x - this._viewportLoc.x, y - this._viewportLoc.y); + }, + + renderQ_push: function (action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will start polling the queue (every + // requestAnimationFrame interval) + this._scan_renderQ(); + } + }, + + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } + + if (this._true_color) { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + } else { + Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap); + } + }, + + defaultCursor: function () { + this._target.style.cursor = "default"; + }, + + // Overridden getters/setters + get_context: function () { + return this._drawCtx; + }, + + set_scale: function (scale) { + this._rescale(scale); + }, + + set_width: function (w) { + this.resize(w, this._fb_height); + }, + get_width: function () { + return this._fb_width; + }, + + set_height: function (h) { + this.resize(this._fb_width, h); + }, + get_height: function () { + return this._fb_height; + }, + + // Private Methods + _rescale: function (factor) { + var canvas = this._target; + var properties = ['transform', 'WebkitTransform', 'MozTransform']; + var transform_prop; + while ((transform_prop = properties.shift())) { + if (typeof canvas.style[transform_prop] !== 'undefined') { + break; + } + } + + if (transform_prop === null) { + Util.Debug("No scaling support"); + return; + } + + if (typeof(factor) === "undefined") { + factor = this._scale; + } else if (factor > 1.0) { + factor = 1.0; + } else if (factor < 0.1) { + factor = 0.1; + } + + if (this._scale === factor) { + return; + } + + this._scale = factor; + var x = canvas.width - (canvas.width * factor); + var y = canvas.height - (canvas.height * factor); + canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)'; + }, + + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; + } else { + bgr = this._colourMap[color[0]]; + } + + var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + }, + + _rgbImageData: 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) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + 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; + var cmap = this._colourMap; + for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) { + var bgr = cmap[arr[j]]; + data[i] = bgr[2]; + data[i + 1] = bgr[1]; + data[i + 2] = bgr[0]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x - vx, y - vy); + }, + + _scan_renderQ: function () { + var ready = true; + while (ready && this._renderQ.length > 0) { + var a = this._renderQ[0]; + switch (a.type) { + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color); + break; + 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); + } else { + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } + } + + if (this._renderQ.length > 0) { + requestAnimFrame(this._scan_renderQ.bind(this)); + } + }, + }; + + Util.make_properties(Display, [ + ['target', 'wo', 'dom'], // Canvas element for rendering + ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only) + ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "data": data} + ['true_color', 'rw', 'bool'], // Use true-color pixel data + ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) + ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 + ['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange() + ['width', 'rw', 'int'], // Display area width + ['height', 'rw', 'int'], // Display area height + + ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) + + ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods + ['cursor_uri', 'rw', 'raw'] // Can we render cursor using data URI ]); -// Override some specific getters/setters -that.get_context = function () { return c_ctx; }; - -that.set_scale = function(scale) { rescale(scale); }; - -that.set_width = function (val) { that.resize(val, fb_height); }; -that.get_width = function() { return fb_width; }; - -that.set_height = function (val) { that.resize(fb_width, val); }; -that.get_height = function() { return fb_height; }; - - - -// -// Private functions -// - -// Create the public API interface -function constructor() { - Util.Debug(">> Display.constructor"); - - var c, func, i, curDat, curSave, - has_imageData = false, UE = Util.Engine; - - if (! conf.target) { throw("target must be set"); } - - if (typeof conf.target === 'string') { - throw("target must be a DOM element"); - } - - c = conf.target; - - if (! c.getContext) { throw("no getContext method"); } - - if (! c_ctx) { c_ctx = c.getContext('2d'); } - - Util.Debug("User Agent: " + navigator.userAgent); - if (UE.gecko) { Util.Debug("Browser: gecko " + UE.gecko); } - if (UE.webkit) { Util.Debug("Browser: webkit " + UE.webkit); } - if (UE.trident) { Util.Debug("Browser: trident " + UE.trident); } - if (UE.presto) { Util.Debug("Browser: presto " + UE.presto); } - - that.clear(); - - // Check canvas features - if ('createImageData' in c_ctx) { - conf.render_mode = "canvas rendering"; - } else { - throw("Canvas does not support createImageData"); - } - if (conf.prefer_js === null) { - Util.Info("Prefering javascript operations"); - conf.prefer_js = true; - } - - // Initialize cached tile imageData - tile16x16 = c_ctx.createImageData(16, 16); - - /* - * Determine browser support for setting the cursor via data URI - * scheme - */ - curDat = []; - for (i=0; i < 8 * 8 * 4; i += 1) { - curDat.push(255); - } - try { - curSave = c.style.cursor; - changeCursor(conf.target, curDat, curDat, 2, 2, 8, 8); - if (c.style.cursor) { - if (conf.cursor_uri === null) { - conf.cursor_uri = true; - } - Util.Info("Data URI scheme cursor supported"); + // Class Methods + Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) { + var w = w0; + var h = h0; + if (h < w) { + h = w; // increase h to make it square } else { - if (conf.cursor_uri === null) { - conf.cursor_uri = false; - } - Util.Warn("Data URI scheme cursor not supported"); - } - c.style.cursor = curSave; - } catch (exc2) { - Util.Error("Data URI scheme cursor test exception: " + exc2); - conf.cursor_uri = false; - } - - Util.Debug("<< Display.constructor"); - return that ; -} - -rescale = function(factor) { - var c, tp, x, y, - properties = ['transform', 'WebkitTransform', 'MozTransform', null]; - c = conf.target; - tp = properties.shift(); - while (tp) { - if (typeof c.style[tp] !== 'undefined') { - break; - } - tp = properties.shift(); - } - - if (tp === null) { - Util.Debug("No scaling support"); - return; - } - - - if (typeof(factor) === "undefined") { - factor = conf.scale; - } else if (factor > 1.0) { - factor = 1.0; - } else if (factor < 0.1) { - factor = 0.1; - } - - if (conf.scale === factor) { - //Util.Debug("Display already scaled to '" + factor + "'"); - return; - } - - conf.scale = factor; - x = c.width - c.width * factor; - y = c.height - c.height * factor; - c.style[tp] = "scale(" + conf.scale + ") translate(-" + x + "px, -" + y + "px)"; -}; - -setFillColor = function(color) { - var bgr, newStyle; - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - newStyle = "rgb(" + bgr[2] + "," + bgr[1] + "," + bgr[0] + ")"; - if (newStyle !== c_prevStyle) { - c_ctx.fillStyle = newStyle; - c_prevStyle = newStyle; - } -}; - - -// -// Public API interface functions -// - -// Shift and/or resize the visible viewport -that.viewportChange = function(deltaX, deltaY, width, height) { - var c = conf.target, v = viewport, cr = cleanRect, - saveImg = null, saveStyle, x1, y1, vx2, vy2, w, h; - - if (!conf.viewport) { - Util.Debug("Setting viewport to full display region"); - deltaX = -v.w; // Clamped later if out of bounds - deltaY = -v.h; // Clamped later if out of bounds - width = fb_width; - height = fb_height; - } - - if (typeof(deltaX) === "undefined") { deltaX = 0; } - if (typeof(deltaY) === "undefined") { deltaY = 0; } - if (typeof(width) === "undefined") { width = v.w; } - if (typeof(height) === "undefined") { height = v.h; } - - // Size change - - if (width > fb_width) { width = fb_width; } - if (height > fb_height) { height = fb_height; } - - if ((v.w !== width) || (v.h !== height)) { - // Change width - if ((width < v.w) && (cr.x2 > v.x + width -1)) { - cr.x2 = v.x + width - 1; - } - v.w = width; - - // Change height - if ((height < v.h) && (cr.y2 > v.y + height -1)) { - cr.y2 = v.y + height - 1; - } - v.h = height; - - - if (v.w > 0 && v.h > 0 && c.width > 0 && c.height > 0) { - saveImg = c_ctx.getImageData(0, 0, - (c.width < v.w) ? c.width : v.w, - (c.height < v.h) ? c.height : v.h); + w = h; // increase w to make it square } - c.width = v.w; - c.height = v.h; + var cur = []; - if (saveImg) { - c_ctx.putImageData(saveImg, 0, 0); - } - } + // Push multi-byte little-endian values + cur.push16le = function (num) { + this.push(num & 0xFF, (num >> 8) & 0xFF); + }; + cur.push32le = function (num) { + this.push(num & 0xFF, + (num >> 8) & 0xFF, + (num >> 16) & 0xFF, + (num >> 24) & 0xFF); + }; - vx2 = v.x + v.w - 1; - vy2 = v.y + v.h - 1; + var IHDRsz = 40; + var RGBsz = w * h * 4; + var XORsz = Math.ceil((w * h) / 8.0); + var ANDsz = Math.ceil((w * h) / 8.0); + cur.push16le(0); // 0: Reserved + cur.push16le(2); // 2: .CUR type + cur.push16le(1); // 4: Number of images, 1 for non-animated ico - // Position change + // Cursor #1 header (ICONDIRENTRY) + cur.push(w); // 6: width + cur.push(h); // 7: height + cur.push(0); // 8: colors, 0 -> true-color + cur.push(0); // 9: reserved + cur.push16le(hotx); // 10: hotspot x coordinate + cur.push16le(hoty); // 12: hotspot y coordinate + cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); + // 14: cursor data byte size + cur.push32le(22); // 18: offset of cursor data in the file - if ((deltaX < 0) && ((v.x + deltaX) < 0)) { - deltaX = - v.x; - } - if ((vx2 + deltaX) >= fb_width) { - deltaX -= ((vx2 + deltaX) - fb_width + 1); - } + // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) + cur.push32le(IHDRsz); // 22: InfoHeader size + cur.push32le(w); // 26: Cursor width + cur.push32le(h * 2); // 30: XOR+AND height + cur.push16le(1); // 34: number of planes + cur.push16le(32); // 36: bits per pixel + cur.push32le(0); // 38: Type of compression - if ((v.y + deltaY) < 0) { - deltaY = - v.y; - } - if ((vy2 + deltaY) >= fb_height) { - deltaY -= ((vy2 + deltaY) - fb_height + 1); - } + cur.push32le(XORsz + ANDsz); + // 42: Size of Image + cur.push32le(0); // 46: reserved + cur.push32le(0); // 50: reserved + cur.push32le(0); // 54: reserved + cur.push32le(0); // 58: reserved - if ((deltaX === 0) && (deltaY === 0)) { - //Util.Debug("skipping viewport change"); - return; - } - Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); - - v.x += deltaX; - vx2 += deltaX; - v.y += deltaY; - vy2 += deltaY; - - // Update the clean rectangle - if (v.x > cr.x1) { - cr.x1 = v.x; - } - if (vx2 < cr.x2) { - cr.x2 = vx2; - } - if (v.y > cr.y1) { - cr.y1 = v.y; - } - if (vy2 < cr.y2) { - cr.y2 = vy2; - } - - if (deltaX < 0) { - // Shift viewport left, redraw left section - x1 = 0; - w = - deltaX; - } else { - // Shift viewport right, redraw right section - x1 = v.w - deltaX; - w = deltaX; - } - if (deltaY < 0) { - // Shift viewport up, redraw top section - y1 = 0; - h = - deltaY; - } else { - // Shift viewport down, redraw bottom section - y1 = v.h - deltaY; - h = deltaY; - } - - // Copy the valid part of the viewport to the shifted location - saveStyle = c_ctx.fillStyle; - c_ctx.fillStyle = "rgb(255,255,255)"; - if (deltaX !== 0) { - //that.copyImage(0, 0, -deltaX, 0, v.w, v.h); - //that.fillRect(x1, 0, w, v.h, [255,255,255]); - c_ctx.drawImage(c, 0, 0, v.w, v.h, -deltaX, 0, v.w, v.h); - c_ctx.fillRect(x1, 0, w, v.h); - } - if (deltaY !== 0) { - //that.copyImage(0, 0, 0, -deltaY, v.w, v.h); - //that.fillRect(0, y1, v.w, h, [255,255,255]); - c_ctx.drawImage(c, 0, 0, v.w, v.h, 0, -deltaY, v.w, v.h); - c_ctx.fillRect(0, y1, v.w, h); - } - c_ctx.fillStyle = saveStyle; -}; - - -// Return a map of clean and dirty areas of the viewport and reset the -// tracking of clean and dirty areas. -// -// Returns: {'cleanBox': {'x': x, 'y': y, 'w': w, 'h': h}, -// 'dirtyBoxes': [{'x': x, 'y': y, 'w': w, 'h': h}, ...]} -that.getCleanDirtyReset = function() { - var v = viewport, c = cleanRect, cleanBox, dirtyBoxes = [], - vx2 = v.x + v.w - 1, vy2 = v.y + v.h - 1; - - - // Copy the cleanRect - cleanBox = {'x': c.x1, 'y': c.y1, - 'w': c.x2 - c.x1 + 1, 'h': c.y2 - c.y1 + 1}; - - if ((c.x1 >= c.x2) || (c.y1 >= c.y2)) { - // Whole viewport is dirty - dirtyBoxes.push({'x': v.x, 'y': v.y, 'w': v.w, 'h': v.h}); - } else { - // Redraw dirty regions - if (v.x < c.x1) { - // left side dirty region - dirtyBoxes.push({'x': v.x, 'y': v.y, - 'w': c.x1 - v.x + 1, 'h': v.h}); - } - if (vx2 > c.x2) { - // right side dirty region - dirtyBoxes.push({'x': c.x2 + 1, 'y': v.y, - 'w': vx2 - c.x2, 'h': v.h}); - } - if (v.y < c.y1) { - // top/middle dirty region - dirtyBoxes.push({'x': c.x1, 'y': v.y, - 'w': c.x2 - c.x1 + 1, 'h': c.y1 - v.y}); - } - if (vy2 > c.y2) { - // bottom/middle dirty region - dirtyBoxes.push({'x': c.x1, 'y': c.y2 + 1, - 'w': c.x2 - c.x1 + 1, 'h': vy2 - c.y2}); - } - } - - // Reset the cleanRect to the whole viewport - cleanRect = {'x1': v.x, 'y1': v.y, - 'x2': v.x + v.w - 1, 'y2': v.y + v.h - 1}; - - return {'cleanBox': cleanBox, 'dirtyBoxes': dirtyBoxes}; -}; - -// Translate viewport coordinates to absolute coordinates -that.absX = function(x) { - return x + viewport.x; -}; -that.absY = function(y) { - return y + viewport.y; -}; - - -that.resize = function(width, height) { - c_prevStyle = ""; - - fb_width = width; - fb_height = height; - - rescale(conf.scale); - that.viewportChange(); -}; - -that.clear = function() { - - if (conf.logo) { - that.resize(conf.logo.width, conf.logo.height); - that.blitStringImage(conf.logo.data, 0, 0); - } else { - that.resize(640, 20); - c_ctx.clearRect(0, 0, viewport.w, viewport.h); - } - - renderQ = []; - - // No benefit over default ("source-over") in Chrome and firefox - //c_ctx.globalCompositeOperation = "copy"; -}; - -that.fillRect = function(x, y, width, height, color) { - setFillColor(color); - c_ctx.fillRect(x - viewport.x, y - viewport.y, width, height); -}; - -that.copyImage = function(old_x, old_y, new_x, new_y, w, h) { - var x1 = old_x - viewport.x, y1 = old_y - viewport.y, - x2 = new_x - viewport.x, y2 = new_y - viewport.y; - c_ctx.drawImage(conf.target, x1, y1, w, h, x2, y2, w, h); -}; - - -// Start updating a tile -that.startTile = function(x, y, width, height, color) { - var data, bgr, red, green, blue, i; - tile_x = x; - tile_y = y; - if ((width === 16) && (height === 16)) { - tile = tile16x16; - } else { - tile = c_ctx.createImageData(width, height); - } - data = tile.data; - if (conf.prefer_js) { - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - red = bgr[2]; - green = bgr[1]; - blue = bgr[0]; - for (i = 0; i < (width * height * 4); i+=4) { - data[i ] = red; - data[i + 1] = green; - data[i + 2] = blue; - data[i + 3] = 255; - } - } else { - that.fillRect(x, y, width, height, color); - } -}; - -// Update sub-rectangle of the current tile -that.subTile = function(x, y, w, h, color) { - var data, p, bgr, red, green, blue, width, j, i, xend, yend; - if (conf.prefer_js) { - data = tile.data; - width = tile.width; - if (conf.true_color) { - bgr = color; - } else { - bgr = conf.colourMap[color[0]]; - } - red = bgr[2]; - green = bgr[1]; - blue = bgr[0]; - xend = x + w; - yend = y + h; - for (j = y; j < yend; j += 1) { - for (i = x; i < xend; i += 1) { - p = (i + (j * width) ) * 4; - data[p ] = red; - data[p + 1] = green; - data[p + 2] = blue; - data[p + 3] = 255; - } - } - } else { - that.fillRect(tile_x + x, tile_y + y, w, h, color); - } -}; - -// Draw the current tile to the screen -that.finishTile = function() { - if (conf.prefer_js) { - c_ctx.putImageData(tile, tile_x - viewport.x, tile_y - viewport.y); - } - // else: No-op, if not prefer_js then already done by setSubTile -}; - -rgbImageData = function(x, y, vx, vy, width, height, arr, offset) { - var img, i, j, data; - /* - if ((x - v.x >= v.w) || (y - v.y >= v.h) || - (x - v.x + width < 0) || (y - v.y + height < 0)) { - // Skipping because outside of viewport - return; - } - */ - img = c_ctx.createImageData(width, height); - data = img.data; - for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+3) { - data[i ] = arr[j ]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j + 2]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - vx, y - vy); -}; - -bgrxImageData = function(x, y, vx, vy, width, height, arr, offset) { - var img, i, j, data; - /* - if ((x - v.x >= v.w) || (y - v.y >= v.h) || - (x - v.x + width < 0) || (y - v.y + height < 0)) { - // Skipping because outside of viewport - return; - } - */ - img = c_ctx.createImageData(width, height); - data = img.data; - for (i=0, j=offset; i < (width * height * 4); i=i+4, j=j+4) { - data[i ] = arr[j + 2]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j ]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - vx, y - vy); -}; - -cmapImageData = function(x, y, vx, vy, width, height, arr, offset) { - var img, i, j, data, bgr, cmap; - img = c_ctx.createImageData(width, height); - data = img.data; - cmap = conf.colourMap; - for (i=0, j=offset; i < (width * height * 4); i+=4, j+=1) { - bgr = cmap[arr[j]]; - data[i ] = bgr[2]; - data[i + 1] = bgr[1]; - data[i + 2] = bgr[0]; - data[i + 3] = 255; // Set Alpha - } - c_ctx.putImageData(img, x - vx, y - vy); -}; - -that.blitImage = function(x, y, width, height, arr, offset) { - if (conf.true_color) { - bgrxImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); - } else { - cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); - } -}; - -that.blitRgbImage = function(x, y, width, height, arr, offset) { - if (conf.true_color) { - rgbImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); - } else { - // prolly wrong... - cmapImageData(x, y, viewport.x, viewport.y, width, height, arr, offset); - } -}; - -that.blitStringImage = function(str, x, y) { - var img = new Image(); - img.onload = function () { - c_ctx.drawImage(img, x - viewport.x, y - viewport.y); - }; - img.src = str; -}; - -// Wrap ctx.drawImage but relative to viewport -that.drawImage = function(img, x, y) { - c_ctx.drawImage(img, x - viewport.x, y - viewport.y); -}; - -that.renderQ_push = function(action) { - renderQ.push(action); - if (renderQ.length === 1) { - // If this can be rendered immediately it will be, otherwise - // the scanner will start polling the queue (every - // requestAnimationFrame interval) - scan_renderQ(); - } -}; - -scan_renderQ = function() { - var a, ready = true; - while (ready && renderQ.length > 0) { - a = renderQ[0]; - switch (a.type) { - case 'copy': - that.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); - break; - case 'fill': - that.fillRect(a.x, a.y, a.width, a.height, a.color); - break; - case 'blit': - that.blitImage(a.x, a.y, a.width, a.height, a.data, 0); - break; - case 'blitRgb': - that.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); - break; - case 'img': - if (a.img.complete) { - that.drawImage(a.img, a.x, a.y); + // 62: color data (RGBQUAD icColors[]) + var y, x; + for (y = h - 1; y >= 0; y--) { + for (x = 0; x < w; x++) { + if (x >= w0 || y >= h0) { + cur.push(0); // blue + cur.push(0); // green + cur.push(0); // red + cur.push(0); // alpha } else { - // We need to wait for this image to 'load' - // to keep things in-order - ready = false; - } - break; - } - if (ready) { - a = renderQ.shift(); - } - } - if (renderQ.length > 0) { - requestAnimFrame(scan_renderQ); - } -}; - - -that.changeCursor = function(pixels, mask, hotx, hoty, w, h) { - if (conf.cursor_uri === false) { - Util.Warn("changeCursor called but no cursor data URI support"); - return; - } - - if (conf.true_color) { - changeCursor(conf.target, pixels, mask, hotx, hoty, w, h); - } else { - changeCursor(conf.target, pixels, mask, hotx, hoty, w, h, conf.colourMap); - } -}; - -that.defaultCursor = function() { - conf.target.style.cursor = "default"; -}; - -return constructor(); // Return the public API interface - -} // End of Display() - - -/* Set CSS cursor property using data URI encoded cursor file */ -function changeCursor(target, pixels, mask, hotx, hoty, w0, h0, cmap) { - "use strict"; - var cur = [], rgb, IHDRsz, RGBsz, ANDsz, XORsz, url, idx, alpha, x, y; - //Util.Debug(">> changeCursor, x: " + hotx + ", y: " + hoty + ", w0: " + w0 + ", h0: " + h0); - - var w = w0; - var h = h0; - if (h < w) - h = w; // increase h to make it square - else - w = h; // increace w to make it square - - // Push multi-byte little-endian values - cur.push16le = function (num) { - this.push((num ) & 0xFF, - (num >> 8) & 0xFF ); - }; - cur.push32le = function (num) { - this.push((num ) & 0xFF, - (num >> 8) & 0xFF, - (num >> 16) & 0xFF, - (num >> 24) & 0xFF ); - }; - - IHDRsz = 40; - RGBsz = w * h * 4; - XORsz = Math.ceil( (w * h) / 8.0 ); - ANDsz = Math.ceil( (w * h) / 8.0 ); - - // Main header - cur.push16le(0); // 0: Reserved - cur.push16le(2); // 2: .CUR type - cur.push16le(1); // 4: Number of images, 1 for non-animated ico - - // Cursor #1 header (ICONDIRENTRY) - cur.push(w); // 6: width - cur.push(h); // 7: height - cur.push(0); // 8: colors, 0 -> true-color - cur.push(0); // 9: reserved - cur.push16le(hotx); // 10: hotspot x coordinate - cur.push16le(hoty); // 12: hotspot y coordinate - cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz); - // 14: cursor data byte size - cur.push32le(22); // 18: offset of cursor data in the file - - - // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO) - cur.push32le(IHDRsz); // 22: Infoheader size - cur.push32le(w); // 26: Cursor width - cur.push32le(h*2); // 30: XOR+AND height - cur.push16le(1); // 34: number of planes - cur.push16le(32); // 36: bits per pixel - cur.push32le(0); // 38: Type of compression - - cur.push32le(XORsz + ANDsz); // 43: Size of Image - // Gimp leaves this as 0 - - cur.push32le(0); // 46: reserved - cur.push32le(0); // 50: reserved - cur.push32le(0); // 54: reserved - cur.push32le(0); // 58: reserved - - // 62: color data (RGBQUAD icColors[]) - for (y = h-1; y >= 0; y -= 1) { - for (x = 0; x < w; x += 1) { - if (x >= w0 || y >= h0) { - cur.push(0); // blue - cur.push(0); // green - cur.push(0); // red - cur.push(0); // alpha - } else { - idx = y * Math.ceil(w0 / 8) + Math.floor(x/8); - alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - if (cmap) { - idx = (w0 * y) + x; - rgb = cmap[pixels[idx]]; - cur.push(rgb[2]); // blue - cur.push(rgb[1]); // green - cur.push(rgb[0]); // red - cur.push(alpha); // alpha - } else { - idx = ((w0 * y) + x) * 4; - cur.push(pixels[idx + 2]); // blue - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx ]); // red - cur.push(alpha); // alpha + var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8); + var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + if (cmap) { + idx = (w0 * y) + x; + var rgb = cmap[pixels[idx]]; + cur.push(rgb[2]); // blue + cur.push(rgb[1]); // green + cur.push(rgb[0]); // red + cur.push(alpha); // alpha + } } } } - } - // XOR/bitmask data (BYTE icXOR[]) - // (ignored, just needs to be right size) - for (y = 0; y < h; y += 1) { - for (x = 0; x < Math.ceil(w / 8); x += 1) { - cur.push(0x00); + // XOR/bitmask data (BYTE icXOR[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } } - } - // AND/bitmask data (BYTE icAND[]) - // (ignored, just needs to be right size) - for (y = 0; y < h; y += 1) { - for (x = 0; x < Math.ceil(w / 8); x += 1) { - cur.push(0x00); + // AND/bitmask data (BYTE icAND[]) + // (ignored, just needs to be the right size) + for (y = 0; y < h; y++) { + for (x = 0; x < Math.ceil(w / 8); x++) { + cur.push(0); + } } - } - url = "data:image/x-icon;base64," + Base64.encode(cur); - target.style.cursor = "url(" + url + ") " + hotx + " " + hoty + ", default"; - //Util.Debug("<< changeCursor, cur.length: " + cur.length); -} + var url = 'data:image/x-icon;base64,' + Base64.encode(cur); + target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; + }; +})(); diff --git a/tests/test.display.js b/tests/test.display.js new file mode 100644 index 00000000..c4535a03 --- /dev/null +++ b/tests/test.display.js @@ -0,0 +1,332 @@ +// requires local modules: util, base64, display +/* jshint expr: true */ +var expect = chai.expect; + +chai.use(function (_chai, utils) { + _chai.Assertion.addMethod('displayed', function (target_data) { + var obj = this._obj; + var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + var data = new Uint8Array(data_cl); + this.assert(utils.eql(data, target_data), + "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", + "expected #{this} not to have displayed the image #{act}", + target_data, + data); + }); +}); + +describe('Display/Canvas Helper', function () { + var checked_data = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + checked_data = new Uint8Array(checked_data); + + var basic_data = [0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]; + basic_data = new Uint8Array(basic_data); + + function make_image_canvas (input_data) { + var canvas = document.createElement('canvas'); + canvas.width = 4; + canvas.height = 4; + var ctx = canvas.getContext('2d'); + var data = ctx.createImageData(4, 4); + for (var i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; } + ctx.putImageData(data, 0, 0); + return canvas; + } + + describe('viewport handling', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(5, 5); + display.viewportChange(1, 1, 3, 3); + display.getCleanDirtyReset(); + }); + + it('should take viewport location into consideration when drawing images', function () { + display.resize(4, 4); + display.viewportChange(0, 0, 2, 2); + display.drawImage(make_image_canvas(basic_data), 1, 1); + + var expected = new Uint8Array(16); + var i; + for (i = 0; i < 8; i++) { expected[i] = basic_data[i]; } + for (i = 8; i < 16; i++) { expected[i] = 0; } + expect(display).to.have.displayed(expected); + }); + + it('should redraw the left side when shifted left', function () { + display.viewportChange(-1, 0, 3, 3); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 1, w: 2, h: 3 }); + }); + + it('should redraw the right side when shifted right', function () { + display.viewportChange(1, 0, 3, 3); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 4, y: 1, w: 1, h: 3 }); + }); + + it('should redraw the top part when shifted up', function () { + display.viewportChange(0, -1, 3, 3); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 0, w: 3, h: 1 }); + }); + + it('should redraw the bottom part when shifted down', function () { + display.viewportChange(0, 1, 3, 3); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 1, y: 4, w: 3, h: 1 }); + }); + + it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () { + display.viewportChange(0, 1, 3, 3); + var cdr1 = display.getCleanDirtyReset(); + var cdr2 = display.getCleanDirtyReset(); + expect(cdr1).to.not.deep.equal(cdr2); + expect(cdr2.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 3 }); + expect(cdr2.dirtyBoxes).to.be.empty; + }); + + it('should simply mark the whole display area as dirty if not using viewports', function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: false }); + display.resize(5, 5); + var cdr = display.getCleanDirtyReset(); + expect(cdr.cleanBox).to.deep.equal({ x: 0, y: 0, w: 0, h: 0 }); + expect(cdr.dirtyBoxes).to.have.length(1); + expect(cdr.dirtyBoxes[0]).to.deep.equal({ x: 0, y: 0, w: 5, h: 5 }); + }); + }); + + describe('resizing', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + }); + + it('should change the size of the logical canvas', function () { + display.resize(5, 7); + expect(display._fb_width).to.equal(5); + expect(display._fb_height).to.equal(7); + }); + + it('should update the viewport dimensions', function () { + sinon.spy(display, 'viewportChange'); + display.resize(2, 2); + expect(display.viewportChange).to.have.been.calledOnce; + }); + }); + + describe('drawing', function () { + + // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the + // basic cases + function drawing_tests (pref_js) { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: pref_js }); + display.resize(4, 4); + }); + + it('should clear the screen on #clear without a logo set', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display._logo = null; + display.clear(); + display.resize(4, 4); + var empty = []; + for (var i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; } + expect(display).to.have.displayed(new Uint8Array(empty)); + }); + + it('should draw the logo on #clear with a logo set', function (done) { + display._logo = { width: 4, height: 4, data: make_image_canvas(checked_data).toDataURL() }; + display._drawCtx._act_drawImg = display._drawCtx.drawImage; + display._drawCtx.drawImage = function (img, x, y) { + this._act_drawImg(img, x, y); + expect(display).to.have.displayed(checked_data); + done(); + }; + display.clear(); + expect(display._fb_width).to.equal(4); + expect(display._fb_height).to.equal(4); + }); + + it('should support filling a rectangle with particular color via #fillRect', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0]); + display.fillRect(2, 2, 2, 2, [0xff, 0, 0]); + expect(display).to.have.displayed(checked_data); + }); + + it('should support copying an portion of the canvas via #copyImage', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]); + display.copyImage(0, 0, 2, 2, 2, 2); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing tile data with a background color and sub tiles', function () { + display.startTile(0, 0, 4, 4, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + 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 () { + 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); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing blit images from a data URL via #blitStringImage', function (done) { + var img_url = make_image_canvas(checked_data).toDataURL(); + display._drawCtx._act_drawImg = display._drawCtx.drawImage; + display._drawCtx.drawImage = function (img, x, y) { + this._act_drawImg(img, x, y); + expect(display).to.have.displayed(checked_data); + done(); + }; + display.blitStringImage(img_url, 0, 0); + }); + + it('should support drawing solid colors with color maps', function () { + display._true_color = false; + display.set_colourMap({ 0: [0xff, 0, 0], 1: [0, 0xff, 0] }); + display.fillRect(0, 0, 4, 4, [1]); + display.fillRect(0, 0, 2, 2, [0]); + display.fillRect(2, 2, 2, 2, [0]); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing blit images with color maps', function () { + display._true_color = false; + display.set_colourMap({ 1: [0xff, 0, 0], 0: [0, 0xff, 0] }); + var data = [1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1].map(function (elem) { return [elem]; }); + display.blitImage(0, 0, 4, 4, data, 0); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing an image object via #drawImage', function () { + var img = make_image_canvas(checked_data); + display.drawImage(img, 0, 0); + expect(display).to.have.displayed(checked_data); + }); + } + + describe('(prefering native methods)', function () { drawing_tests.call(this, false); }); + describe('(prefering JavaScript)', function () { drawing_tests.call(this, true); }); + }); + + describe('the render queue processor', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false }); + display.resize(4, 4); + sinon.spy(display, '_scan_renderQ'); + this.old_requestAnimFrame = window.requestAnimFrame; + window.requestAnimFrame = function (cb) { + this.next_frame_cb = cb; + }.bind(this); + this.next_frame = function () { this.next_frame_cb(); }; + }); + + afterEach(function () { + window.requestAnimFrame = this.old_requestAnimFrame; + }); + + it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { + display.renderQ_push({ type: 'noop' }); // does nothing + expect(display._scan_renderQ).to.have.been.calledOnce; + }); + + it('should not try to process an item when it is pushed on if we are waiting for other items', function () { + display._renderQ.length = 2; + display.renderQ_push({ type: 'noop' }); + expect(display._scan_renderQ).to.not.have.been.called; + }); + + it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { + var img = { complete: false }; + display._renderQ = [{ type: 'img', x: 3, y: 4, img: img }, + { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }]; + display.drawImage = sinon.spy(); + display.fillRect = sinon.spy(); + + display._scan_renderQ(); + expect(display.drawImage).to.not.have.been.called; + expect(display.fillRect).to.not.have.been.called; + + display._renderQ[0].img.complete = true; + this.next_frame(); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledOnce; + }); + + it('should draw a blit image on type "blit"', function () { + display.blitImage = sinon.spy(); + display.renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); + expect(display.blitImage).to.have.been.calledOnce; + 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 }); + expect(display.copyImage).to.have.been.calledOnce; + expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); + }); + + it('should fill a rect with a given color on type "fill"', function () { + display.fillRect = sinon.spy(); + display.renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); + expect(display.fillRect).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); + }); + + it('should draw an image from an image object on type "img" (if complete)', function () { + display.drawImage = sinon.spy(); + display.renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); + }); + }); +}); From b1dee9478815b22bf5fee3ee9e44321d4bb46c91 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 19 Jun 2014 13:27:42 -0400 Subject: [PATCH 012/527] Cleanup: RFB Client File: rfb.js (also websock.js) Tests Added: True Changes: - De-Crockford-ified rfb.js - Added methods to websock.js to skip bytes in the receive queue --- include/rfb.js | 3731 +++++++++++++++++++++----------------------- include/websock.js | 8 + tests/test.rfb.js | 1696 ++++++++++++++++++++ 3 files changed, 3506 insertions(+), 1929 deletions(-) create mode 100644 tests/test.rfb.js diff --git a/include/rfb.js b/include/rfb.js index 75034ca5..9e96f715 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -10,2001 +10,1874 @@ * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca) */ -/*jslint white: false, browser: true, bitwise: false, plusplus: false */ +/*jslint white: false, browser: true */ /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */ +var RFB; -function RFB(defaults) { -"use strict"; - -var that = {}, // Public API methods - conf = {}, // Configuration attributes - - // Pre-declare private functions used before definitions (jslint) - init_vars, updateState, fail, handle_message, - init_msg, normal_msg, framebufferUpdate, print_stats, - - pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests, - keyEvent, pointerEvent, clientCutText, - - getTightCLength, extract_data_uri, - keyPress, mouseButton, mouseMove, - - checkEvents, // Overridable for testing - - - // - // Private RFB namespace variables - // - rfb_host = '', - rfb_port = 5900, - rfb_password = '', - rfb_path = '', - - rfb_state = 'disconnected', - rfb_version = 0, - rfb_max_version= 3.8, - rfb_auth_scheme= '', - rfb_tightvnc = false, - - rfb_xvp_ver = 0, - - - // In preference order - encodings = [ - ['COPYRECT', 0x01 ], - ['TIGHT', 0x07 ], - ['TIGHT_PNG', -260 ], - ['HEXTILE', 0x05 ], - ['RRE', 0x02 ], - ['RAW', 0x00 ], - ['DesktopSize', -223 ], - ['Cursor', -239 ], - - // Psuedo-encoding settings - //['JPEG_quality_lo', -32 ], - ['JPEG_quality_med', -26 ], - //['JPEG_quality_hi', -23 ], - //['compress_lo', -255 ], - ['compress_hi', -247 ], - ['last_rect', -224 ], - ['xvp', -309 ] - ], - - encHandlers = {}, - encNames = {}, - encStats = {}, // [rectCnt, rectCntTot] - - ws = null, // Websock object - display = null, // Display object - keyboard = null, // Keyboard input handler object - mouse = null, // Mouse input handler object - sendTimer = null, // Send Queue check timer - disconnTimer = null, // disconnection timer - msgTimer = null, // queued handle_message timer - - // Frame buffer update state - FBU = { - rects : 0, - subrects : 0, // RRE - lines : 0, // RAW - tiles : 0, // HEXTILE - bytes : 0, - x : 0, - y : 0, - width : 0, - height : 0, - encoding : 0, - subencoding : -1, - background : null, - zlibs : [] // TIGHT zlib streams - }, - - fb_Bpp = 4, - fb_depth = 3, - fb_width = 0, - fb_height = 0, - fb_name = "", - - rre_chunk_sz = 100, - - timing = { - last_fbu : 0, - fbu_total : 0, - fbu_total_cnt : 0, - full_fbu_total : 0, - full_fbu_cnt : 0, - - fbu_rt_start : 0, - fbu_rt_total : 0, - fbu_rt_cnt : 0, - pixels : 0 - }, - - test_mode = false, - - /* Mouse state */ - mouse_buttonMask = 0, - mouse_arr = [], - viewportDragging = false, - viewportDragPos = {}; - -// Configuration attributes -Util.conf_defaults(conf, that, defaults, [ - ['target', 'wo', 'dom', null, 'VNC display rendering Canvas object'], - ['focusContainer', 'wo', 'dom', document, 'DOM element that captures keyboard input'], - - ['encrypt', 'rw', 'bool', false, 'Use TLS/SSL/wss encryption'], - ['true_color', 'rw', 'bool', true, 'Request true color pixel data'], - ['local_cursor', 'rw', 'bool', false, 'Request locally rendered cursor'], - ['shared', 'rw', 'bool', true, 'Request shared mode'], - ['view_only', 'rw', 'bool', false, 'Disable client mouse/keyboard'], - ['xvp_password_sep', 'rw', 'str', '@', 'Separator for XVP password fields'], - ['disconnectTimeout', 'rw', 'int', 3, 'Time (s) to wait for disconnection'], - - ['wsProtocols', 'rw', 'arr', ['binary', 'base64'], - 'Protocols to use in the WebSocket connection'], - - // UltraVNC repeater ID to connect to - ['repeaterID', 'rw', 'str', '', 'RepeaterID to connect to'], - - ['viewportDrag', 'rw', 'bool', false, 'Move the viewport on mouse drags'], - - // Callback functions - ['onUpdateState', 'rw', 'func', function() { }, - 'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '], - ['onPasswordRequired', 'rw', 'func', function() { }, - 'onPasswordRequired(rfb): VNC password is required '], - ['onClipboard', 'rw', 'func', function() { }, - 'onClipboard(rfb, text): RFB clipboard contents received'], - ['onBell', 'rw', 'func', function() { }, - 'onBell(rfb): RFB Bell message received '], - ['onFBUReceive', 'rw', 'func', function() { }, - 'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '], - ['onFBUComplete', 'rw', 'func', function() { }, - 'onFBUComplete(rfb, fbu): RFB FBU received and processed '], - ['onFBResize', 'rw', 'func', function() { }, - 'onFBResize(rfb, width, height): frame buffer resized'], - ['onDesktopName', 'rw', 'func', function() { }, - 'onDesktopName(rfb, name): desktop name received'], - ['onXvpInit', 'rw', 'func', function() { }, - 'onXvpInit(version): XVP extensions active for this connection'], - - // These callback names are deprecated - ['updateState', 'rw', 'func', function() { }, - 'obsolete, use onUpdateState'], - ['clipboardReceive', 'rw', 'func', function() { }, - 'obsolete, use onClipboard'] - ]); - - -// Override/add some specific configuration getters/setters -that.set_local_cursor = function(cursor) { - if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) { - conf.local_cursor = false; - } else { - if (display.get_cursor_uri()) { - conf.local_cursor = true; - } else { - Util.Warn("Browser does not support local cursor"); - } - } -}; - -// These are fake configuration getters -that.get_display = function() { return display; }; - -that.get_keyboard = function() { return keyboard; }; - -that.get_mouse = function() { return mouse; }; - - - -// -// Setup routines -// - -// Create the public API interface and initialize values that stay -// constant across connect/disconnect -function constructor() { - var i, rmode; - Util.Debug(">> RFB.constructor"); - - // Create lookup tables based encoding number - for (i=0; i < encodings.length; i+=1) { - encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]]; - encNames[encodings[i][1]] = encodings[i][0]; - encStats[encodings[i][1]] = [0, 0]; - } - // Initialize display, mouse, keyboard, and websock - try { - display = new Display({'target': conf.target}); - } catch (exc) { - Util.Error("Display exception: " + exc); - updateState('fatal', "No working Display"); - } - keyboard = new Keyboard({'target': conf.focusContainer, - 'onKeyPress': keyPress}); - mouse = new Mouse({'target': conf.target, - 'onMouseButton': mouseButton, - 'onMouseMove': mouseMove, - 'notify': keyboard.sync}); - - rmode = display.get_render_mode(); - - ws = new Websock(); - ws.on('message', handle_message); - ws.on('open', function() { - if (rfb_state === "connect") { - updateState('ProtocolVersion', "Starting VNC handshake"); - } else { - fail("Got unexpected WebSockets connection"); - } - }); - ws.on('close', function(e) { - Util.Warn("WebSocket on-close event"); - var msg = ""; - if (e.code) { - msg = " (code: " + e.code; - if (e.reason) { - msg += ", reason: " + e.reason; - } - msg += ")"; - } - if (rfb_state === 'disconnect') { - updateState('disconnected', 'VNC disconnected' + msg); - } else if (rfb_state === 'ProtocolVersion') { - fail('Failed to connect to server' + msg); - } else if (rfb_state in {'failed':1, 'disconnected':1}) { - Util.Error("Received onclose while disconnected" + msg); - } else { - fail('Server disconnected' + msg); - } - }); - ws.on('error', function(e) { - Util.Warn("WebSocket on-error event"); - //fail("WebSock reported an error"); - }); - - - init_vars(); - - /* Check web-socket-js if no builtin WebSocket support */ - if (Websock_native) { - Util.Info("Using native WebSockets"); - updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); - } else { - Util.Warn("Using web-socket-js bridge. Flash version: " + - Util.Flash.version); - if ((! Util.Flash) || - (Util.Flash.version < 9)) { - updateState('fatal', "WebSockets or Adobe Flash<\/a> is required"); - } else if (document.location.href.substr(0, 7) === "file://") { - updateState('fatal', - "'file://' URL is incompatible with Adobe Flash"); - } else { - updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); - } - } - - Util.Debug("<< RFB.constructor"); - return that; // Return the public API interface -} - -function connect() { - Util.Debug(">> RFB.connect"); - var uri; - - if (typeof UsingSocketIO !== "undefined") { - uri = "http"; - } else { - uri = conf.encrypt ? "wss" : "ws"; - } - uri += "://" + rfb_host + ":" + rfb_port + "/" + rfb_path; - Util.Info("connecting to " + uri); - - ws.open(uri, conf.wsProtocols); - - Util.Debug("<< RFB.connect"); -} - -// Initialize variables that are reset before each connection -init_vars = function() { - var i; - - /* Reset state */ - ws.init(); - - FBU.rects = 0; - FBU.subrects = 0; // RRE and HEXTILE - FBU.lines = 0; // RAW - FBU.tiles = 0; // HEXTILE - FBU.zlibs = []; // TIGHT zlib encoders - mouse_buttonMask = 0; - mouse_arr = []; - rfb_tightvnc = false; - - // Clear the per connection encoding stats - for (i=0; i < encodings.length; i+=1) { - encStats[encodings[i][1]][0] = 0; - } - - for (i=0; i < 4; i++) { - //FBU.zlibs[i] = new InflateStream(); - FBU.zlibs[i] = new TINF(); - FBU.zlibs[i].init(); - } -}; - -// Print statistics -print_stats = function() { - var i, s; - Util.Info("Encoding stats for this connection:"); - for (i=0; i < encodings.length; i+=1) { - s = encStats[encodings[i][1]]; - if ((s[0] + s[1]) > 0) { - Util.Info(" " + encodings[i][0] + ": " + - s[0] + " rects"); - } - } - Util.Info("Encoding stats since page load:"); - for (i=0; i < encodings.length; i+=1) { - s = encStats[encodings[i][1]]; - if ((s[0] + s[1]) > 0) { - Util.Info(" " + encodings[i][0] + ": " + - s[1] + " rects"); - } - } -}; - -// -// Utility routines -// - - -/* - * Page states: - * loaded - page load, equivalent to disconnected - * disconnected - idle state - * connect - starting to connect (to ProtocolVersion) - * normal - connected - * disconnect - starting to disconnect - * failed - abnormal disconnect - * fatal - failed to load page, or fatal error - * - * RFB protocol initialization states: - * ProtocolVersion - * Security - * Authentication - * password - waiting for password, not part of RFB - * SecurityResult - * ClientInitialization - not triggered by server message - * ServerInitialization (to normal) - */ -updateState = function(state, statusMsg) { - var func, cmsg, oldstate = rfb_state; - - if (state === oldstate) { - /* Already here, ignore */ - Util.Debug("Already in state '" + state + "', ignoring."); - return; - } - - /* - * These are disconnected states. A previous connect may - * asynchronously cause a connection so make sure we are closed. - */ - if (state in {'disconnected':1, 'loaded':1, 'connect':1, - 'disconnect':1, 'failed':1, 'fatal':1}) { - if (sendTimer) { - clearInterval(sendTimer); - sendTimer = null; +(function () { + "use strict"; + RFB = function (defaults) { + if (!defaults) { + defaults = {}; } - if (msgTimer) { - clearTimeout(msgTimer); - msgTimer = null; + this._rfb_host = ''; + this._rfb_port = 5900; + this._rfb_password = ''; + this._rfb_path = ''; + + this._rfb_state = 'disconnected'; + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_auth_scheme = ''; + + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; + + // In preference order + this._encodings = [ + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], + + // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ], + ['xvp', -309 ] + ]; + + this._encHandlers = {}; + this._encNames = {}; + this._encStats = {}; + + this._sock = null; // Websock object + this._display = null; // Display object + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object + this._sendTimer = null; // Send Queue check timer + this._disconnTimer = null; // disconnection timer + this._msgTimer = null; // queued handle_msg timer + + // Frame buffer update state + this._FBU = { + rects: 0, + subrects: 0, // RRE + lines: 0, // RAW + tiles: 0, // HEXTILE + bytes: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: 0, + subencoding: -1, + background: null, + zlib: [] // TIGHT zlib streams + }; + + this._fb_Bpp = 4; + this._fb_depth = 3; + this._fb_width = 0; + this._fb_height = 0; + this._fb_name = ""; + + this._rre_chunk_sz = 100; + + this._timing = { + last_fbu: 0, + fbu_total: 0, + fbu_total_cnt: 0, + full_fbu_total: 0, + full_fbu_cnt: 0, + + fbu_rt_start: 0, + fbu_rt_total: 0, + fbu_rt_cnt: 0, + pixels: 0 + }; + + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + + // set the default value on user-facing properties + Util.set_defaults(this, defaults, { + 'target': 'null', // VNC display rendering Canvas object + 'focusContainer': document, // DOM element that captures keyboard input + 'encrypt': false, // Use TLS/SSL/wss encryption + 'true_color': true, // Request true color pixel data + 'local_cursor': false, // Request locally rendered cursor + 'shared': true, // Request shared mode + 'view_only': false, // Disable client mouse/keyboard + 'xvp_password_sep': '@', // Separator for XVP password fields + 'disconnectTimeout': 3, // Time (s) to wait for disconnection + 'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection + 'repeaterID': '', // [UltraVNC] RepeaterID to connect to + 'viewportDrag': false, // Move the viewport on mouse drags + + // Callback functions + 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change + 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required + 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received + 'onBell': function () { }, // onBell(rfb): RFB Bell message received + 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed + 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized + 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received + 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + }); + + // main setup + Util.Debug(">> RFB.constructor"); + + // populate encHandlers with bound versions + Object.keys(RFB.encodingHandlers).forEach(function (encName) { + this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this); + }.bind(this)); + + // Create lookup tables based on encoding number + for (var i = 0; i < this._encodings.length; i++) { + this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]]; + this._encNames[this._encodings[i][1]] = this._encodings[i][0]; + this._encStats[this._encodings[i][1]] = [0, 0]; } - if (display && display.get_context()) { - keyboard.ungrab(); - mouse.ungrab(); - display.defaultCursor(); - if ((Util.get_logging() !== 'debug') || - (state === 'loaded')) { - // Show noVNC logo on load and when disconnected if - // debug is off - display.clear(); - } + try { + this._display = new Display({target: this._target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + this._updateState('fatal', "No working Display"); } - ws.close(); - } + this._keyboard = new Keyboard({target: this._focusContainer, + onKeyPress: this._handleKeyPress.bind(this)}); - if (oldstate === 'fatal') { - Util.Error("Fatal error, cannot continue"); - } + this._mouse = new Mouse({target: this._target, + onMouseButton: this._handleMouseButton.bind(this), + onMouseMove: this._handleMouseMove.bind(this), + notify: this._keyboard.sync.bind(this._keyboard)}); - if ((state === 'failed') || (state === 'fatal')) { - func = Util.Error; - } else { - func = Util.Warn; - } - - cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; - func("New state '" + state + "', was '" + oldstate + "'." + cmsg); - - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Do disconnect action, but stay in failed state - rfb_state = 'failed'; - } else { - rfb_state = state; - } - - if (disconnTimer && (rfb_state !== 'disconnect')) { - Util.Debug("Clearing disconnect timer"); - clearTimeout(disconnTimer); - disconnTimer = null; - } - - switch (state) { - case 'normal': - if ((oldstate === 'disconnected') || (oldstate === 'failed')) { - Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); - } - - break; - - - case 'connect': - - init_vars(); - connect(); - - // WebSocket.onopen transitions to 'ProtocolVersion' - break; - - - case 'disconnect': - - if (! test_mode) { - disconnTimer = setTimeout(function () { - fail("Disconnect timeout"); - }, conf.disconnectTimeout * 1000); - } - - print_stats(); - - // WebSocket.onclose transitions to 'disconnected' - break; - - - case 'failed': - if (oldstate === 'disconnected') { - Util.Error("Invalid transition from 'disconnected' to 'failed'"); - } - if (oldstate === 'normal') { - Util.Error("Error while connected."); - } - if (oldstate === 'init') { - Util.Error("Error while initializing."); - } - - // Make sure we transition to disconnected - setTimeout(function() { updateState('disconnected'); }, 50); - - break; - - - default: - // No state change action to take - - } - - if ((oldstate === 'failed') && (state === 'disconnected')) { - // Leave the failed message - conf.updateState(that, state, oldstate); // Obsolete - conf.onUpdateState(that, state, oldstate); - } else { - conf.updateState(that, state, oldstate, statusMsg); // Obsolete - conf.onUpdateState(that, state, oldstate, statusMsg); - } -}; - -fail = function(msg) { - updateState('failed', msg); - return false; -}; - -handle_message = function() { - //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen()); - //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - if (ws.rQlen() === 0) { - Util.Warn("handle_message called on empty receive queue"); - return; - } - switch (rfb_state) { - case 'disconnected': - case 'failed': - Util.Error("Got data while disconnected"); - break; - case 'normal': - if (normal_msg() && ws.rQlen() > 0) { - // true means we can continue processing - // Give other events a chance to run - if (msgTimer === null) { - Util.Debug("More data to process, creating timer"); - msgTimer = setTimeout(function () { - msgTimer = null; - handle_message(); - }, 10); + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', function () { + if (this._rfb_state === 'connect') { + this._updateState('ProtocolVersion', "Starting VNC handshake"); } else { - Util.Debug("More data to process, existing timer"); + this._fail("Got unexpected WebSocket connection"); } - } - break; - default: - init_msg(); - break; - } -}; + }.bind(this)); + this._sock.on('close', function (e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (this._rfb_state === 'disconnect') { + this._updateState('disconnected', 'VNC disconnected' + msg); + } else if (this._rfb_state === 'ProtocolVersion') { + this._fail('Failed to connect to server' + msg); + } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + this._fail("Server disconnected" + msg); + } + }.bind(this)); + this._sock.on('error', function (e) { + Util.Warn("WebSocket on-error event"); + }); + this._init_vars(); -function genDES(password, challenge) { - var i, passwd = []; - for (i=0; i < password.length; i += 1) { - passwd.push(password.charCodeAt(i)); - } - return (new DES(passwd)).encrypt(challenge); -} - -// overridable for testing -checkEvents = function() { - if (rfb_state === 'normal' && !viewportDragging && mouse_arr.length > 0) { - ws.send(mouse_arr); - mouse_arr = []; - } -}; - -keyPress = function(keysym, down) { - if (conf.view_only) { return; } // View only, skip keyboard events - - ws.send(keyEvent(keysym, down)); -}; - -mouseButton = function(x, y, down, bmask) { - if (down) { - mouse_buttonMask |= bmask; - } else { - mouse_buttonMask ^= bmask; - } - - if (conf.viewportDrag) { - if (down && !viewportDragging) { - viewportDragging = true; - viewportDragPos = {'x': x, 'y': y}; - - // Skip sending mouse events - return; + var rmode = this._display.get_render_mode(); + if (Websock_native) { + Util.Info("Using native WebSockets"); + this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); } else { - viewportDragging = false; - } - } - - if (conf.view_only) { return; } // View only, skip mouse events - - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y)) ); - ws.send(mouse_arr); - mouse_arr = []; -}; - -mouseMove = function(x, y) { - //Util.Debug('>> mouseMove ' + x + "," + y); - var deltaX, deltaY; - - if (viewportDragging) { - //deltaX = x - viewportDragPos.x; // drag viewport - deltaX = viewportDragPos.x - x; // drag frame buffer - //deltaY = y - viewportDragPos.y; // drag viewport - deltaY = viewportDragPos.y - y; // drag frame buffer - viewportDragPos = {'x': x, 'y': y}; - - display.viewportChange(deltaX, deltaY); - - // Skip sending mouse events - return; - } - - if (conf.view_only) { return; } // View only, skip mouse events - - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y))); - - checkEvents(); -}; - - -// -// Server message handlers -// - -// RFB/VNC initialisation message handler -init_msg = function() { - //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); - - var strlen, reason, length, sversion, cversion, repeaterID, - i, types, num_types, challenge, response, bpp, depth, - big_endian, red_max, green_max, blue_max, red_shift, - green_shift, blue_shift, true_color, name_length, is_repeater, - xvp_sep, xvp_auth, xvp_auth_str; - - //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0)); - switch (rfb_state) { - - case 'ProtocolVersion' : - if (ws.rQlen() < 12) { - return fail("Incomplete protocol version"); - } - sversion = ws.rQshiftStr(12).substr(4,7); - Util.Info("Server ProtocolVersion: " + sversion); - is_repeater = 0; - switch (sversion) { - case "000.000": is_repeater = 1; break; // UltraVNC repeater - case "003.003": rfb_version = 3.3; break; - case "003.006": rfb_version = 3.3; break; // UltraVNC - case "003.889": rfb_version = 3.3; break; // Apple Remote Desktop - case "003.007": rfb_version = 3.7; break; - case "003.008": rfb_version = 3.8; break; - case "004.000": rfb_version = 3.8; break; // Intel AMT KVM - case "004.001": rfb_version = 3.8; break; // RealVNC 4.6 - default: - return fail("Invalid server version " + sversion); - } - if (is_repeater) { - repeaterID = conf.repeaterID; - while (repeaterID.length < 250) { - repeaterID += "\0"; + Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); + if (!Util.Flash || Util.Flash.version < 9) { + this._updateState('fatal', "WebSockets or Adobe Flash is required"); + } else if (document.location.href.substr(0, 7) === 'file://') { + this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); + } else { + this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); } - ws.send_string(repeaterID); - break; - } - if (rfb_version > rfb_max_version) { - rfb_version = rfb_max_version; } - if (! test_mode) { - sendTimer = setInterval(function() { - // Send updates either at a rate of one update - // every 50ms, or whatever slower rate the network - // can handle. - ws.flush(); - }, 50); - } + Util.Debug("<< RFB.constructor"); + }; - cversion = "00" + parseInt(rfb_version,10) + - ".00" + ((rfb_version * 10) % 10); - ws.send_string("RFB " + cversion + "\n"); - updateState('Security', "Sent ProtocolVersion: " + cversion); - break; + RFB.prototype = { + // Public methods + connect: function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; - case 'Security' : - if (rfb_version >= 3.7) { - // Server sends supported list, client decides - num_types = ws.rQshift8(); - if (ws.rQwait("security type", num_types, 1)) { return false; } - if (num_types === 0) { - strlen = ws.rQshift32(); - reason = ws.rQshiftStr(strlen); - return fail("Security failure: " + reason); + if (!this._rfb_host || !this._rfb_port) { + return this._fail("Must set host and port"); } - rfb_auth_scheme = 0; - types = ws.rQshiftBytes(num_types); - Util.Debug("Server security types: " + types); - for (i=0; i < types.length; i+=1) { - if ((types[i] > rfb_auth_scheme) && (types[i] <= 16 || types[i] == 22)) { - rfb_auth_scheme = types[i]; + + this._updateState('connect'); + }, + + disconnect: function () { + this._updateState('disconnect', 'Disconnecting'); + }, + + sendPassword: function (passwd) { + this._rfb_password = passwd; + this._rfb_state = 'Authentication'; + setTimeout(this._init_msg.bind(this), 1); + }, + + sendCtrlAltDel: function () { + if (this._rfb_state !== 'normal' || this._view_only) { return false; } + Util.Info("Sending Ctrl-Alt-Del"); + + var arr = []; + arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 1)); // Control + arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 1)); // Alt + arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 1)); // Delete + arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 0)); // Delete + arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 0)); // Alt + arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 0)); // Control + this._sock.send(arr); + }, + + xvpOp: function (ver, op) { + if (this._rfb_xvp_ver < ver) { return false; } + Util.Info("Sending XVP operation " + op + " (version " + ver + ")"); + this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); + return true; + }, + + xvpShutdown: function () { + return this.xvpOp(1, 2); + }, + + xvpReboot: function () { + return this.xvpOp(1, 3); + }, + + xvpReset: function () { + return this.xvpOp(1, 4); + }, + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey: function (code, down) { + if (this._rfb_state !== "normal" || this._view_only) { return false; } + var arr = []; + if (typeof down !== 'undefined') { + Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); + arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0)); + } else { + Util.Info("Sending key code (down + up): " + code); + arr = arr.concat(RFB.messages.keyEvent(code, 1)); + arr = arr.concat(RFB.messages.keyEvent(code, 0)); + } + this._sock.send(arr); + }, + + clipboardPasteFrom: function (text) { + if (this._rfb_state !== 'normal') { return; } + this._sock.send(RFB.messages.clientCutText(text)); + }, + + // Private methods + + _connect: function () { + Util.Debug(">> RFB.connect"); + + var uri; + if (typeof UsingSocketIO !== 'undefined') { + uri = 'http'; + } else { + uri = this._encrypt ? 'wss' : 'ws'; + } + + uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; + Util.Info("connecting to " + uri); + + this._sock.open(uri, this._sockProtocols); + + Util.Debug("<< RFB.connect"); + }, + + _init_vars: function () { + // reset state + this._sock.init(); + + this._FBU.rects = 0; + this._FBU.subrects = 0; // RRE and HEXTILE + this._FBU.lines = 0; // RAW + this._FBU.tiles = 0; // HEXTILE + this._FBU.zlibs = []; // TIGHT zlib encoders + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._rfb_tightvnc = false; + + // Clear the per connection encoding stats + var i; + for (i = 0; i < this._encodings.length; i++) { + this._encStats[this._encodings[i][1]][0] = 0; + } + + for (i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new TINF(); + this._FBU.zlibs[i].init(); + } + }, + + _print_stats: function () { + Util.Info("Encoding stats for this connection:"); + var i, s; + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + if (s[0] + s[1] > 0) { + Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects"); } } - if (rfb_auth_scheme === 0) { - return fail("Unsupported security types: " + types); - } - - ws.send([rfb_auth_scheme]); - } else { - // Server decides - if (ws.rQwait("security scheme", 4)) { return false; } - rfb_auth_scheme = ws.rQshift32(); - } - updateState('Authentication', - "Authenticating using scheme: " + rfb_auth_scheme); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - // Triggered by fallthough, not by server message - case 'Authentication' : - //Util.Debug("Security auth scheme: " + rfb_auth_scheme); - switch (rfb_auth_scheme) { - case 0: // connection failed - if (ws.rQwait("auth reason", 4)) { return false; } - strlen = ws.rQshift32(); - reason = ws.rQshiftStr(strlen); - return fail("Auth failure: " + reason); - case 1: // no authentication - if (rfb_version >= 3.8) { - updateState('SecurityResult'); + Util.Info("Encoding stats since page load:"); + for (i = 0; i < this._encodings.length; i++) { + s = this._encStats[this._encodings[i][1]]; + Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects"); + } + }, + + + /* + * Page states: + * loaded - page load, equivalent to disconnected + * disconnected - idle state + * connect - starting to connect (to ProtocolVersion) + * normal - connected + * disconnect - starting to disconnect + * failed - abnormal disconnect + * fatal - failed to load page, or fatal error + * + * RFB protocol initialization states: + * ProtocolVersion + * Security + * Authentication + * password - waiting for password, not part of RFB + * SecurityResult + * ClientInitialization - not triggered by server message + * ServerInitialization (to normal) + */ + _updateState: function (state, statusMsg) { + var oldstate = this._rfb_state; + + if (state === oldstate) { + // Already here, ignore + Util.Debug("Already in state '" + state + "', ignoring"); + } + + /* + * These are disconnected states. A previous connect may + * asynchronously cause a connection so make sure we are closed. + */ + if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, + 'disconnect': 1, 'failed': 1, 'fatal': 1}) { + + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } + + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } + + if (this._display && this._display.get_context()) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + this._display.defaultCursor(); + if (Util.get_logging() !== 'debug' || state === 'loaded') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + } + + this._sock.close(); + } + + if (oldstate === 'fatal') { + Util.Error('Fatal error, cannot continue'); + } + + var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : ""; + var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg; + if (state === 'failed' || state === 'fatal') { + Util.Error(cmsg); + } else { + Util.Warn(cmsg); + } + + if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state + this._rfb_state = 'failed'; + } else { + this._rfb_state = state; + } + + if (this._disconnTimer && this._rfb_state !== 'disconnect') { + Util.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + } + + switch (state) { + case 'normal': + if (oldstate === 'disconnected' || oldstate === 'failed') { + Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'"); + } + break; + + case 'connect': + this._init_vars(); + this._connect(); + // WebSocket.onopen transitions to 'ProtocolVersion' + break; + + case 'disconnect': + this._disconnTimer = setTimeout(function () { + this._fail("Disconnect timeout"); + }.bind(this), this._disconnectTimeout * 1000); + + this._print_stats(); + + // WebSocket.onclose transitions to 'disconnected' + break; + + case 'failed': + if (oldstate === 'disconnected') { + Util.Error("Invalid transition from 'disconnected' to 'failed'"); + } else if (oldstate === 'normal') { + Util.Error("Error while connected."); + } else if (oldstate === 'init') { + Util.Error("Error while initializing."); + } + + // Make sure we transition to disconnected + setTimeout(function () { + this._updateState('disconnected'); + }.bind(this), 50); + + break; + + default: + // No state change action to take + } + + if (oldstate === 'failed' && state === 'disconnected') { + this._onUpdateState(this, state, oldstate); + } else { + this._onUpdateState(this, state, oldstate, statusMsg); + } + }, + + _fail: function (msg) { + this._updateState('failed', msg); + return false; + }, + + _handle_message: function () { + if (this._sock.rQlen() === 0) { + Util.Warn("handle_message called on an empty receive queue"); + return; + } + + switch (this._rfb_state) { + case 'disconnected': + case 'failed': + Util.Error("Got data while disconnected"); + break; + case 'normal': + if (this._normal_msg() && this._sock.rQlen() > 0) { + // true means we can continue processing + // Give other events a chance to run + if (this._msgTimer === null) { + Util.Debug("More data to process, creating timer"); + this._msgTimer = setTimeout(function () { + this._msgTimer = null; + this._handle_message(); + }.bind(this), 10); + } else { + Util.Debug("More data to process, existing timer"); + } + } + break; + default: + this._init_msg(); + break; + } + }, + + _checkEvents: function () { + if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) { + this._sock.send(this._mouse_arr); + this._mouse_arr = []; + } + }, + + _handleKeyPress: function (keysym, down) { + if (this._view_only) { return; } // View only, skip keyboard, events + this._sock.send(RFB.messages.keyEvent(keysym, down)); + }, + + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMaks ^= bmask; + } + + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; + + // Skip sending mouse events return; + } else { + this._viewportDragging = false; } - // Fall through to ClientInitialisation - break; - case 22: // XVP authentication - xvp_sep = conf.xvp_password_sep; - xvp_auth = rfb_password.split(xvp_sep); - if (xvp_auth.length < 3) { - updateState('password', "XVP credentials required (user" + xvp_sep + - "target" + xvp_sep + "password) -- got only " + rfb_password); - conf.onPasswordRequired(that); - return; + } + + if (this._view_only) { return; } // View only, skip mouse events + + this._mouse_arr = this._mouse_arr.concat( + RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); + this._sock.send(this._mouse_arr); + this._mouse_arr = []; + }, + + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + this._viewportDragPos = {'x': x, 'y': y}; + + this._display.viewportChange(deltaX, deltaY); + + // Skip sending mouse events + return; + } + + if (this._view_only) { return; } // View only, skip mouse events + + this._mouse_arr = this._mouse_arr.concat( + RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); + + this._checkEvents(); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Util.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; } - xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + this._sock.send_string(repeaterID); + return true; + } + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; + } + + // Send updates either at a rate of 1 update per 50ms, or + // whatever slower rate the network can handle + this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); + + var cversion = "00" + parseInt(this._rfb_version, 10) + + ".00" + ((this._rfb_version * 10) % 10); + this._sock.send_string("RFB " + cversion + "\n"); + this._updateState('Security', 'Sent ProtocolVersion: ' + cversion); + }, + + _negotiate_security: function () { + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + var num_types = this._sock.rQshift8(); + if (this._sock.rQwait("security type", num_types, 1)) { return false; } + + if (num_types === 0) { + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Security failure: " + reason); + } + + this._rfb_auth_scheme = 0; + var types = this._sock.rQshiftBytes(num_types); + Util.Debug("Server security types: " + types); + for (var i = 0; i < types.length; i++) { + if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) { + this._rfb_auth_scheme = types[i]; + } + } + + if (this._rfb_auth_scheme === 0) { + return this._fail("Unsupported security types: " + types); + } + + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait("security scheme", 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } + + this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme); + return this._init_msg(); // jump to authentication + }, + + // authentication + _negotiate_xvp_auth: function () { + var xvp_sep = this._xvp_password_sep; + var xvp_auth = this._rfb_password.split(xvp_sep); + if (xvp_auth.length < 3) { + this._updateState('password', 'XVP credentials required (user' + xvp_sep + + 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password); + this._onPasswordRequired(this); + return false; + } + + var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + String.fromCharCode(xvp_auth[1].length) + xvp_auth[0] + xvp_auth[1]; - ws.send_string(xvp_auth_str); - rfb_password = xvp_auth.slice(2).join(xvp_sep); - rfb_auth_scheme = 2; - // Fall through to standard VNC authentication with remaining part of password - case 2: // VNC authentication - if (rfb_password.length === 0) { - // Notify via both callbacks since it is kind of - // a RFB state change and a UI interface issue. - updateState('password', "Password Required"); - conf.onPasswordRequired(that); - return; + this._sock.send_string(xvp_auth_str); + this._rfb_password = xvp_auth.slice(2).join(xvp_sep); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + }, + + _negotiate_std_vnc_auth: function () { + if (this._rfb_password.length === 0) { + // Notify via both callbacks since it's kind of + // an RFB state change and a UI interface issue + this._updateState('password', "Password Required"); + this._onPasswordRequired(this); + } + + if (this._sock.rQwait("auth challenge", 16)) { return false; } + + var challenge = this._sock.rQshiftBytes(16); + var response = RFB.genDES(this._rfb_password, challenge); + this._sock.send(response); + this._updateState("SecurityResult"); + return true; + }, + + _negotiate_tight_tunnels: function (numTunnels) { + var clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + var serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (var i = 0; i < numTunnels; i++) { + var cap_code = this._sock.rQshift32(); + var cap_vendor = this._sock.rQshiftStr(4); + var cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || + serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect vendor or signature"); } - if (ws.rQwait("auth challenge", 16)) { return false; } - challenge = ws.rQshiftBytes(16); - //Util.Debug("Password: " + rfb_password); - //Util.Debug("Challenge: " + challenge + - // " (" + challenge.length + ")"); - response = genDES(rfb_password, challenge); - //Util.Debug("Response: " + response + - // " (" + response.length + ")"); - - //Util.Debug("Sending DES encrypted auth response"); - ws.send(response); - updateState('SecurityResult'); - return; - case 16: // TightVNC Security Type - if (!rfb_tightvnc) { - // we haven't been through this before, so assume - // we need to check for tunnel support - if (ws.rQwait("num tunnels", 4)) { return false; } - var numTunnels = ws.rQshift32(); - //console.log("Number of tunnels: "+numTunnels); + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support the notunnel type"); + } + }, - rfb_tightvnc = true; + _negotiate_tight_auth: function () { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait("num tunnels", 4)) { return false; } + var numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } - var clientSupportedTunnelTypes = { - 0: { vender: 'TGHT', signature: 'NOTUNNEL' } - }; + this._rfb_tightvnc = true; - if (numTunnels > 0) { - var serverSupportedTunnelTypes = {} - // receive tunnel capabilities - for (var i = 0; i < numTunnels; i++) { - if (ws.rQwait("tunnel " + i.toString() + " capability", 16)) { return false; } - - var cap_code = ws.rQshift32(); - var cap_vendor = ws.rQshiftStr(4); - var cap_signature = ws.rQshiftStr(8); - serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; - } - - // choose an auth type - for (var cap_code in clientSupportedTunnelTypes) { - if (serverSupportedTunnelTypes[cap_code] != undefined) { - // TODO(directxman12): convert capcode back to U32 - ws.send([0,0,0,cap_code]); - return; - } - } - - fail("No supported tunnel types"); - return; - } - } // otherwise, we've dealt with tunnels, so jump right into auth - - var clientSupportedTypes = { - 'STDVNOAUTH__': 1, - 'STDVVNCAUTH_': 2 - }; - - - var serverSupportedTypes = []; - - if (ws.rQwait("sub auth count", 4)) { return false; } - var subAuthCount = ws.rQshift32(); - //console.log("Sub auth count: "+subAuthCount); - for (var i=0;i 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue } + } - for (var authType in clientSupportedTypes) - { - if (serverSupportedTypes.indexOf(authType) != -1) - { - //console.log("selected authType "+authType); - ws.send([0,0,0,clientSupportedTypes[authType]]); + // second pass, do the sub-auth negotiation + if (this._sock.rQwait("sub auth count", 4)) { return false; } + var subAuthCount = this._sock.rQshift32(); + if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } - switch (authType) - { - case 'STDVNOAUTH__': - // No authentication - updateState('SecurityResult'); - return; - case 'STDVVNCAUTH_': - // VNC Authentication. Reenter auth handler to complete auth - rfb_auth_scheme = 2; - init_msg(); - return; - default: - fail("Unsupported tiny auth scheme: " + authType); - return; - } + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; + + var serverSupportedTypes = []; + + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + for (var authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + + switch (authType) { + case 'STDVNOAUTH__': // no auth + this._updateState('SecurityResult'); + return true; + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; + return this._init_msg(); + default: + return this._fail("Unsupported tiny auth scheme: " + authType); } } + } + this._fail("No supported sub-auth types!"); + }, - return; - default: - fail("Unsupported auth scheme: " + rfb_auth_scheme); - return; - } - updateState('ClientInitialisation', "No auth required"); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Auth failure: " + reason); - case 'SecurityResult' : - if (ws.rQwait("VNC auth response ", 4)) { return false; } - switch (ws.rQshift32()) { - case 0: // OK - // Fall through to ClientInitialisation - break; - case 1: // failed - if (rfb_version >= 3.8) { - length = ws.rQshift32(); - if (ws.rQwait("SecurityResult reason", length, 8)) { + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._updateState('SecurityResult'); + return true; + } + this._updateState('ClientInitialisation', "No auth required"); + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); + } + }, + + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._updateState('ClientInitialisation', 'Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail(reason); + } else { + return this._fail("Authentication failure"); + } + return false; + case 2: + return this._fail("Too many auth attempts"); + } + }, + + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* Screen size */ + 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._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + var name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + var numServerMessages = this._sock.rQshift16(); + var numClientMessages = this._sock.rQshift16(); + var numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + var i; + for (i = 0; i < numServerMessages; i++) { + var srvMsg = this._sock.rQshiftStr(16); + } + + for (i = 0; i < numClientMessages; i++) { + var clientMsg = this._sock.rQshiftStr(16); + } + + for (i = 0; i < numEncodings; i++) { + var encoding = this._sock.rQshiftStr(16); + } + } + + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Util.Info("Screen: " + this._fb_width + "x" + this._fb_height + + ", bpp: " + 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"); + } + + // 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; + } + + this._display.set_true_color(this._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 = 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._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + this._sock.send(response); + + this._checkEvents(); + + if (this._encrypt) { + this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); + } else { + this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); + } + }, + + _init_msg: function () { + switch (this._rfb_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); + + case 'Security': + return this._negotiate_security(); + + case 'Authentication': + return this._negotiate_authentication(); + + case 'SecurityResult': + return this._handle_security_result(); + + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._updateState('ServerInitialisation', "Authentication OK"); + return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + } + }, + + _handle_set_colour_map_msg: function () { + Util.Debug("SetColorMapEntries"); + this._sock.rQskip8(); // Padding + + var first_colour = this._sock.rQshift16(); + var num_colours = this._sock.rQshift16(); + if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; } + + for (var c = 0; c < num_colours; c++) { + var red = parseInt(this._sock.rQshift16() / 256, 10); + var green = parseInt(this._sock.rQshift16() / 256, 10); + var blue = parseInt(this._sock.rQshift16() / 256, 10); + this._display.set_colourMap([blue, green, red], first_colour + c); + } + Util.Debug("colourMap: " + this._display.get_colourMap()); + Util.Info("Registered " + num_colours + " colourMap entries"); + + return true; + }, + + _handle_server_cut_text: function () { + Util.Debug("ServerCutText"); + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var length = this._sock.rQshift32(); + if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } + + var text = this._sock.rQshiftStr(length); + this._onClipboard(this, text); + + return true; + }, + + _handle_xvp_msg: function () { + if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + var xvp_ver = this._sock.rQshift8(); + var xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + this._updateState(this._rfb_state, "Operation Failed"); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); + this._onXvpInit(this._rfb_xvp_ver); + break; + default: + this._fail("Disconnected: illegal server XVP message " + xvp_msg); + break; + } + + return true; + }, + + _normal_msg: function () { + var msg_type; + + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + switch (msg_type) { + case 0: // FramebufferUpdate + var ret = this._framebufferUpdate(); + if (ret) { + this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), + this._fb_width, this._fb_height)); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Util.Debug("Bell"); + this._onBell(this); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail("Disconnected: illegal server message type " + msg_type); + Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); + return true; + } + }, + + _framebufferUpdate: function () { + var ret = true; + var now; + + if (this._FBU.rects === 0) { + if (this._sock.rQwait("FBU header", 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { + now = (new Date()).getTime(); + Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); + } + } + + while (this._FBU.rects > 0) { + if (this._rfb_state !== "normal") { return false; } + + if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait("rect header", 12)) { return false; } + /* New FramebufferUpdate */ + + var hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + this._onFBUReceive(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + if (!this._encNames[this._FBU.encoding]) { + this._fail("Disconnected: unsupported encoding " + + this._FBU.encoding); return false; } - reason = ws.rQshiftStr(length); - fail(reason); - } else { - fail("Authentication failed"); } - return; - case 2: // too-many - return fail("Too many auth attempts"); - } - updateState('ClientInitialisation', "Authentication OK"); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - // Triggered by fallthough, not by server message - case 'ClientInitialisation' : - ws.send([conf.shared ? 1 : 0]); // ClientInitialisation - updateState('ServerInitialisation', "Authentication OK"); - break; + this._timing.last_fbu = (new Date()).getTime(); - case 'ServerInitialisation' : - if (ws.rQwait("server initialization", 24)) { return false; } + ret = this._encHandlers[this._FBU.encoding](); - /* Screen size */ - fb_width = ws.rQshift16(); - fb_height = ws.rQshift16(); + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); - /* PIXEL_FORMAT */ - bpp = ws.rQshift8(); - depth = ws.rQshift8(); - big_endian = ws.rQshift8(); - true_color = ws.rQshift8(); - - red_max = ws.rQshift16(); - green_max = ws.rQshift16(); - blue_max = ws.rQshift16(); - red_shift = ws.rQshift8(); - green_shift = ws.rQshift8(); - blue_shift = ws.rQshift8(); - ws.rQshiftStr(3); // padding - - Util.Info("Screen: " + fb_width + "x" + fb_height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); - - if (big_endian !== 0) { - Util.Warn("Server native endian is not little endian"); - } - if (red_shift !== 16) { - Util.Warn("Server native red-shift is not 16"); - } - if (blue_shift !== 0) { - Util.Warn("Server native blue-shift is not 0"); - } - - /* Connection name/title */ - name_length = ws.rQshift32(); - fb_name = Util.decodeUTF8(ws.rQshiftStr(name_length)); - conf.onDesktopName(that, fb_name); - - if (conf.true_color && fb_name === "Intel(r) AMT KVM") - { - Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color"); - conf.true_color = false; - } - - if (rfb_tightvnc) - { - // In TightVNC mode, ServerInit message is extended - var numServerMessages = ws.rQshift16(); - var numClientMessages = ws.rQshift16(); - var numEncodings = ws.rQshift16(); - ws.rQshift16(); // padding - //console.log("numServerMessages "+numServerMessages); - //console.log("numClientMessages "+numClientMessages); - //console.log("numEncodings "+numEncodings); - - for (var i=0;i> normal_msg"); - - var ret = true, msg_type, length, text, - c, first_colour, num_colours, red, green, blue, - xvp_ver, xvp_msg; - - if (FBU.rects > 0) { - msg_type = 0; - } else { - msg_type = ws.rQshift8(); - } - switch (msg_type) { - case 0: // FramebufferUpdate - ret = framebufferUpdate(); // false means need more data - if (ret) { - // only allow one outstanding fbu-request at a time - ws.send(fbUpdateRequests()); - } - break; - case 1: // SetColourMapEntries - Util.Debug("SetColourMapEntries"); - ws.rQshift8(); // Padding - first_colour = ws.rQshift16(); // First colour - num_colours = ws.rQshift16(); - if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; } - - for (c=0; c < num_colours; c+=1) { - red = ws.rQshift16(); - //Util.Debug("red before: " + red); - red = parseInt(red / 256, 10); - //Util.Debug("red after: " + red); - green = parseInt(ws.rQshift16() / 256, 10); - blue = parseInt(ws.rQshift16() / 256, 10); - display.set_colourMap([blue, green, red], first_colour + c); - } - Util.Debug("colourMap: " + display.get_colourMap()); - Util.Info("Registered " + num_colours + " colourMap entries"); - //Util.Debug("colourMap: " + display.get_colourMap()); - break; - case 2: // Bell - Util.Debug("Bell"); - conf.onBell(that); - break; - case 3: // ServerCutText - Util.Debug("ServerCutText"); - if (ws.rQwait("ServerCutText header", 7, 1)) { return false; } - ws.rQshiftBytes(3); // Padding - length = ws.rQshift32(); - if (ws.rQwait("ServerCutText", length, 8)) { return false; } - - text = ws.rQshiftStr(length); - conf.clipboardReceive(that, text); // Obsolete - conf.onClipboard(that, text); - break; - case 250: // XVP - ws.rQshift8(); // Padding - xvp_ver = ws.rQshift8(); - xvp_msg = ws.rQshift8(); - switch (xvp_msg) { - case 0: // XVP_FAIL - updateState(rfb_state, "Operation failed"); - break; - case 1: // XVP_INIT - rfb_xvp_ver = xvp_ver; - Util.Info("XVP extensions enabled (version " + rfb_xvp_ver + ")"); - conf.onXvpInit(rfb_xvp_ver); - break; - default: - fail("Disconnected: illegal server XVP message " + xvp_msg); - break; - } - break; - default: - fail("Disconnected: illegal server message type " + msg_type); - Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); - break; - } - //Util.Debug("<< normal_msg"); - return ret; -}; - -framebufferUpdate = function() { - var now, hdr, fbu_rt_diff, ret = true; - - if (FBU.rects === 0) { - //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20)); - if (ws.rQwait("FBU header", 3)) { - ws.rQunshift8(0); // FBU msg_type - return false; - } - ws.rQshift8(); // padding - FBU.rects = ws.rQshift16(); - //Util.Debug("FramebufferUpdate, rects:" + FBU.rects); - FBU.bytes = 0; - timing.cur_fbu = 0; - if (timing.fbu_rt_start > 0) { - now = (new Date()).getTime(); - Util.Info("First FBU latency: " + (now - timing.fbu_rt_start)); - } - } - - while (FBU.rects > 0) { - if (rfb_state !== "normal") { - return false; - } - if (ws.rQwait("FBU", FBU.bytes)) { return false; } - if (FBU.bytes === 0) { - if (ws.rQwait("rect header", 12)) { return false; } - /* New FramebufferUpdate */ - - hdr = ws.rQshiftBytes(12); - FBU.x = (hdr[0] << 8) + hdr[1]; - FBU.y = (hdr[2] << 8) + hdr[3]; - FBU.width = (hdr[4] << 8) + hdr[5]; - FBU.height = (hdr[6] << 8) + hdr[7]; - FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); - - conf.onFBUReceive(that, - {'x': FBU.x, 'y': FBU.y, - 'width': FBU.width, 'height': FBU.height, - 'encoding': FBU.encoding, - 'encodingName': encNames[FBU.encoding]}); - - if (encNames[FBU.encoding]) { - // Debug: - /* - var msg = "FramebufferUpdate rects:" + FBU.rects; - msg += " x: " + FBU.x + " y: " + FBU.y; - msg += " width: " + FBU.width + " height: " + FBU.height; - msg += " encoding:" + FBU.encoding; - msg += "(" + encNames[FBU.encoding] + ")"; - msg += ", ws.rQlen(): " + ws.rQlen(); - Util.Debug(msg); - */ - } else { - fail("Disconnected: unsupported encoding " + - FBU.encoding); - return false; - } - } - - timing.last_fbu = (new Date()).getTime(); - - ret = encHandlers[FBU.encoding](); - - now = (new Date()).getTime(); - timing.cur_fbu += (now - timing.last_fbu); - - if (ret) { - encStats[FBU.encoding][0] += 1; - encStats[FBU.encoding][1] += 1; - timing.pixels += FBU.width * FBU.height; - } - - if (timing.pixels >= (fb_width * fb_height)) { - if (((FBU.width === fb_width) && - (FBU.height === fb_height)) || - (timing.fbu_rt_start > 0)) { - timing.full_fbu_total += timing.cur_fbu; - timing.full_fbu_cnt += 1; - Util.Info("Timing of full FBU, cur: " + - timing.cur_fbu + ", total: " + - timing.full_fbu_total + ", cnt: " + - timing.full_fbu_cnt + ", avg: " + - (timing.full_fbu_total / - timing.full_fbu_cnt)); - } - if (timing.fbu_rt_start > 0) { - fbu_rt_diff = now - timing.fbu_rt_start; - timing.fbu_rt_total += fbu_rt_diff; - timing.fbu_rt_cnt += 1; - Util.Info("full FBU round-trip, cur: " + - fbu_rt_diff + ", total: " + - timing.fbu_rt_total + ", cnt: " + - timing.fbu_rt_cnt + ", avg: " + - (timing.fbu_rt_total / - timing.fbu_rt_cnt)); - timing.fbu_rt_start = 0; - } - } - if (! ret) { - return ret; // false ret means need more data - } - } - - conf.onFBUComplete(that, - {'x': FBU.x, 'y': FBU.y, - 'width': FBU.width, 'height': FBU.height, - 'encoding': FBU.encoding, - 'encodingName': encNames[FBU.encoding]}); - - return true; // We finished this FBU -}; - -// -// FramebufferUpdate encodings -// - -encHandlers.RAW = function display_raw() { - //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)"); - - var cur_y, cur_height; - - if (FBU.lines === 0) { - FBU.lines = FBU.height; - } - FBU.bytes = FBU.width * fb_Bpp; // At least a line - if (ws.rQwait("RAW", FBU.bytes)) { return false; } - cur_y = FBU.y + (FBU.height - FBU.lines); - cur_height = Math.min(FBU.lines, - Math.floor(ws.rQlen()/(FBU.width * fb_Bpp))); - display.blitImage(FBU.x, cur_y, FBU.width, cur_height, - ws.get_rQ(), ws.get_rQi()); - ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp); - FBU.lines -= cur_height; - - if (FBU.lines > 0) { - FBU.bytes = FBU.width * fb_Bpp; // At least another line - } else { - FBU.rects -= 1; - FBU.bytes = 0; - } - //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)"); - return true; -}; - -encHandlers.COPYRECT = function display_copy_rect() { - //Util.Debug(">> display_copy_rect"); - - var old_x, old_y; - - FBU.bytes = 4; - if (ws.rQwait("COPYRECT", 4)) { return false; } - display.renderQ_push({ - 'type': 'copy', - 'old_x': ws.rQshift16(), - 'old_y': ws.rQshift16(), - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - FBU.rects -= 1; - FBU.bytes = 0; - return true; -}; - -encHandlers.RRE = function display_rre() { - //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)"); - var color, x, y, width, height, chunk; - - if (FBU.subrects === 0) { - FBU.bytes = 4+fb_Bpp; - if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; } - FBU.subrects = ws.rQshift32(); - color = ws.rQshiftBytes(fb_Bpp); // Background - display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color); - } - while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) { - color = ws.rQshiftBytes(fb_Bpp); - x = ws.rQshift16(); - y = ws.rQshift16(); - width = ws.rQshift16(); - height = ws.rQshift16(); - display.fillRect(FBU.x + x, FBU.y + y, width, height, color); - FBU.subrects -= 1; - } - //Util.Debug(" display_rre: rects: " + FBU.rects + - // ", FBU.subrects: " + FBU.subrects); - - if (FBU.subrects > 0) { - chunk = Math.min(rre_chunk_sz, FBU.subrects); - FBU.bytes = (fb_Bpp + 8) * chunk; - } else { - FBU.rects -= 1; - FBU.bytes = 0; - } - //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes); - return true; -}; - -encHandlers.HEXTILE = function display_hextile() { - //Util.Debug(">> display_hextile"); - var subencoding, subrects, color, cur_tile, - tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh, - rQ = ws.get_rQ(), rQi = ws.get_rQi(); - - if (FBU.tiles === 0) { - FBU.tiles_x = Math.ceil(FBU.width/16); - FBU.tiles_y = Math.ceil(FBU.height/16); - FBU.total_tiles = FBU.tiles_x * FBU.tiles_y; - FBU.tiles = FBU.total_tiles; - } - - /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */ - while (FBU.tiles > 0) { - FBU.bytes = 1; - if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; } - subencoding = rQ[rQi]; // Peek - if (subencoding > 30) { // Raw - fail("Disconnected: illegal hextile subencoding " + subencoding); - //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30)); - return false; - } - subrects = 0; - cur_tile = FBU.total_tiles - FBU.tiles; - tile_x = cur_tile % FBU.tiles_x; - tile_y = Math.floor(cur_tile / FBU.tiles_x); - x = FBU.x + tile_x * 16; - y = FBU.y + tile_y * 16; - w = Math.min(16, (FBU.x + FBU.width) - x); - h = Math.min(16, (FBU.y + FBU.height) - y); - - /* Figure out how much we are expecting */ - if (subencoding & 0x01) { // Raw - //Util.Debug(" Raw subencoding"); - FBU.bytes += w * h * fb_Bpp; - } else { - if (subencoding & 0x02) { // Background - FBU.bytes += fb_Bpp; - } - if (subencoding & 0x04) { // Foreground - FBU.bytes += fb_Bpp; - } - if (subencoding & 0x08) { // AnySubrects - FBU.bytes += 1; // Since we aren't shifting it off - if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; } - subrects = rQ[rQi + FBU.bytes-1]; // Peek - if (subencoding & 0x10) { // SubrectsColoured - FBU.bytes += subrects * (fb_Bpp + 2); - } else { - FBU.bytes += subrects * 2; + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; } - } - } - /* - Util.Debug(" tile:" + cur_tile + "/" + (FBU.total_tiles - 1) + - " (" + tile_x + "," + tile_y + ")" + - " [" + x + "," + y + "]@" + w + "x" + h + - ", subenc:" + subencoding + - "(last: " + FBU.lastsubencoding + "), subrects:" + - subrects + - ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes + - " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) + - " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10)); - */ - if (ws.rQwait("hextile", FBU.bytes)) { return false; } - - /* We know the encoding and have a whole tile */ - FBU.subencoding = rQ[rQi]; - rQi += 1; - if (FBU.subencoding === 0) { - if (FBU.lastsubencoding & 0x01) { - /* Weird: ignore blanks after RAW */ - Util.Debug(" Ignoring blank after RAW"); - } else { - display.fillRect(x, y, w, h, FBU.background); - } - } else if (FBU.subencoding & 0x01) { // Raw - display.blitImage(x, y, w, h, rQ, rQi); - rQi += FBU.bytes - 1; - } else { - if (FBU.subencoding & 0x02) { // Background - FBU.background = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; - } - if (FBU.subencoding & 0x04) { // Foreground - FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; - } - - display.startTile(x, y, w, h, FBU.background); - if (FBU.subencoding & 0x08) { // AnySubrects - subrects = rQ[rQi]; - rQi += 1; - for (s = 0; s < subrects; s += 1) { - if (FBU.subencoding & 0x10) { // SubrectsColoured - color = rQ.slice(rQi, rQi + fb_Bpp); - rQi += fb_Bpp; - } else { - color = FBU.foreground; + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Util.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); } - xy = rQ[rQi]; - rQi += 1; - sx = (xy >> 4); - sy = (xy & 0x0f); - wh = rQ[rQi]; - rQi += 1; - sw = (wh >> 4) + 1; - sh = (wh & 0x0f) + 1; - - display.subTile(sx, sy, sw, sh, color); - } - } - display.finishTile(); - } - ws.set_rQi(rQi); - FBU.lastsubencoding = FBU.subencoding; - FBU.bytes = 0; - FBU.tiles -= 1; - } - - if (FBU.tiles === 0) { - FBU.rects -= 1; - } - - //Util.Debug("<< display_hextile"); - return true; -}; - - -// Get 'compact length' header and data size -getTightCLength = function (arr) { - var header = 1, data = 0; - data += arr[0] & 0x7f; - if (arr[0] & 0x80) { - header += 1; - data += (arr[1] & 0x7f) << 7; - if (arr[1] & 0x80) { - header += 1; - data += arr[2] << 14; - } - } - return [header, data]; -}; - -function display_tight(isTightPNG) { - //Util.Debug(">> display_tight"); - - if (fb_depth === 1) { - fail("Tight protocol handler only implements true color mode"); - } - - var ctl, cmode, clength, color, img, data; - var filterId = -1, resetStreams = 0, streamId = -1; - var rQ = ws.get_rQ(), rQi = ws.get_rQi(); - - FBU.bytes = 1; // compression-control byte - if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; } - - var checksum = function(data) { - var sum=0, i; - for (i=0; i 65536) sum -= 65536; - } - return sum; - } - - var decompress = function(data) { - for (var i=0; i<4; i++) { - if ((resetStreams >> i) & 1) { - FBU.zlibs[i].reset(); - Util.Info("Reset zlib stream " + i); - } - } - var uncompressed = FBU.zlibs[streamId].uncompress(data, 0); - if (uncompressed.status !== 0) { - Util.Error("Invalid data in zlib stream"); - } - //Util.Warn("Decompressed " + data.length + " to " + - // uncompressed.data.length + " checksums " + - // checksum(data) + ":" + checksum(uncompressed.data)); - - return uncompressed.data; - } - - var indexedToRGB = function (data, numColors, palette, width, height) { - // Convert indexed (palette based) image data to RGB - // TODO: reduce number of calculations inside loop - var dest = []; - var x, y, b, w, w1, dp, sp; - if (numColors === 2) { - w = Math.floor((width + 7) / 8); - w1 = Math.floor(width / 8); - for (y = 0; y < height; y++) { - for (x = 0; x < w1; x++) { - for (b = 7; b >= 0; b--) { - dp = (y*width + x*8 + 7-b) * 3; - sp = (data[y*w + x] >> b & 1) * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; } } - for (b = 7; b >= 8 - width % 8; b--) { - dp = (y*width + x*8 + 7-b) * 3; - sp = (data[y*w + x] >> b & 1) * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; - } + + if (!ret) { return ret; } // need more data } - } else { - for (y = 0; y < height; y++) { - for (x = 0; x < width; x++) { - dp = (y*width + x) * 3; - sp = data[y*width + x] * 3; - dest[dp ] = palette[sp ]; - dest[dp+1] = palette[sp+1]; - dest[dp+2] = palette[sp+2]; - } - } - } - return dest; + + this._onFBUComplete(this, + {'x': this._FBU.x, 'y': this._FBU.y, + 'width': this._FBU.width, 'height': this._FBU.height, + 'encoding': this._FBU.encoding, + 'encodingName': this._encNames[this._FBU.encoding]}); + + return true; // We finished this FBU + }, }; - var handlePalette = function() { - var numColors = rQ[rQi + 2] + 1; - var paletteSize = numColors * fb_depth; - FBU.bytes += paletteSize; - if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; } - var bpp = (numColors <= 2) ? 1 : 8; - var rowSize = Math.floor((FBU.width * bpp + 7) / 8); - var raw = false; - if (rowSize * FBU.height < 12) { - raw = true; - clength = [0, rowSize * FBU.height]; + 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 + ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor + ['shared', 'rw', 'bool'], // Request shared mode + ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard + ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields + ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection + ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection + ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to + ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags + + // Callback functions + ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change + ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required + ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received + ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received + ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed + ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed + ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized + ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received + ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ]); + + RFB.prototype.set_local_cursor = function (cursor) { + if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { + this._local_cursor = false; } else { - clength = getTightCLength(ws.rQslice(3 + paletteSize, - 3 + paletteSize + 3)); + if (this._display.get_cursor_uri()) { + this._local_cursor = true; + } else { + Util.Warn("Browser does not support local cursor"); + } } - FBU.bytes += clength[0] + clength[1]; - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + }; - // Shift ctl, filter id, num colors, palette entries, and clength off - ws.rQshiftBytes(3); - var palette = ws.rQshiftBytes(paletteSize); - ws.rQshiftBytes(clength[0]); + RFB.prototype.get_display = function () { return this._display; }; + RFB.prototype.get_keyboard = function () { return this._keyboard; }; + RFB.prototype.get_mouse = function () { return this._mouse; }; - if (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); + // Class Methods + RFB.messages = { + keyEvent: function (keysym, down) { + var arr = [4]; + arr.push8(down); + arr.push16(0); + arr.push32(keysym); + return arr; + }, + + pointerEvent: function (x, y, mask) { + var arr = [5]; // msg-type + arr.push8(mask); + arr.push16(x); + arr.push16(y); + return arr; + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (text) { + var arr = [6]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + arr.push32(text.length); + var n = text.length; + for (var i = 0; i < n; i++) { + arr.push(text.charCodeAt(i)); + } + + return arr; + }, + + pixelFormat: function (bpp, depth, true_color) { + 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.push16(255); // red-max + arr.push16(255); // green-max + arr.push16(255); // blue-max + arr.push8(16); // red-shift + arr.push8(8); // green-shift + arr.push8(0); // blue-shift + + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + return arr; + }, + + clientEncodings: function (encodings, local_cursor, true_color) { + var i, encList = []; + + for (i = 0; i < encodings.length; i++) { + if (encodings[i][0] === "Cursor" && !local_cursor) { + Util.Debug("Skipping Cursor pseudo-encoding"); + } else if (encodings[i][0] === "TIGHT" && !true_color) { + // TODO: remove this when we have tight+non-true-color + Util.Warn("Skipping tight as it is only supported with true color"); + } else { + encList.push(encodings[i][1]); + } + } + + var arr = [2]; // msg-type + arr.push8(0); // padding + + arr.push16(encList.length); // encoding count + for (i = 0; i < encList.length; i++) { + arr.push32(encList[i]); + } + + return arr; + }, + + fbUpdateRequests: function (cleanDirty, fb_width, fb_height) { + var arr = []; + + var cb = cleanDirty.cleanBox; + var w, h; + if (cb.w > 0 && cb.h > 0) { + w = typeof cb.w === "undefined" ? fb_width : cb.w; + h = typeof cb.h === "undefined" ? fb_height : cb.h; + // Request incremental for clean box + arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h)); + } + + for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { + var db = cleanDirty.dirtyBoxes[i]; + // Force all (non-incremental) for dirty box + w = typeof db.w === "undefined" ? fb_width : db.w; + h = typeof db.h === "undefined" ? fb_height : db.h; + arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h)); + } + + return arr; + }, + + fbUpdateRequest: function (incremental, x, y, w, h) { + if (typeof(x) === "undefined") { x = 0; } + if (typeof(y) === "undefined") { y = 0; } + + var arr = [3]; // msg-type + arr.push8(incremental); + arr.push16(x); + arr.push16(y); + arr.push16(w); + arr.push16(h); + + return arr; } + }; - // Convert indexed (palette based) image data to RGB - var rgb = indexedToRGB(data, numColors, palette, FBU.width, FBU.height); - - // Add it to the render queue - display.renderQ_push({ - 'type': 'blitRgb', - 'data': rgb, - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - return true; - } - - var handleCopy = function() { - var raw = false; - var uncompressedSize = FBU.width * FBU.height * fb_depth; - if (uncompressedSize < 12) { - raw = true; - clength = [0, uncompressedSize]; - } else { - clength = getTightCLength(ws.rQslice(1, 4)); + RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); } - FBU.bytes = 1 + clength[0] + clength[1]; - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } + return (new DES(passwd)).encrypt(challenge); + }; - // Shift ctl, clength off - ws.rQshiftBytes(1 + clength[0]); + RFB.extract_data_uri = function (arr) { + return ";base64," + Base64.encode(arr); + }; - if (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); + RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } + + this._FBU.bytes = this._FBU.width * this._fb_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); + this._FBU.lines -= curr_height; + + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + COPYRECT: function () { + this._FBU.bytes = 4; + if (this._sock.rQwait("COPYRECT", 4)) { return false; } + this._display.renderQ_push({ + 'type': 'copy', + 'old_x': this._sock.rQshift16(), + 'old_y': this._sock.rQshift16(), + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE: function () { + var color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + this._fb_Bpp; + if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(this._fb_Bpp); // 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); + var x = this._sock.rQshift16(); + var y = this._sock.rQshift16(); + var width = this._sock.rQshift16(); + var height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (this._fb_Bpp + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + HEXTILE: function () { + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } + var subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail("Disconnected: illegal hextile subencoding " + subencoding); + return false; + } + + var subrects = 0; + var curr_tile = this._FBU.total_tiles - this._FBU.tiles; + var tile_x = curr_tile % this._FBU.tiles_x; + var tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + var x = this._FBU.x + tile_x * 16; + var y = this._FBU.y + tile_y * 16; + var w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + var h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * this._fb_Bpp; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += this._fb_Bpp; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += this._fb_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); + } else { + this._FBU.bytes += subrects * 2; + } + } + } + + if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Util.Debug(" Ignoring blank after RAW"); + } else { + this._display.fillRect(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + 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; + } + if (this._FBU.subencoding & 0x04) { // Foreground + this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); + rQi += this._fb_Bpp; + } + + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; + + for (var s = 0; s < subrects; s++) { + var color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + color = rQ.slice(rQi, rQi + this._fb_Bpp); + rQi += this._fb_Bpp; + } else { + color = this._FBU.foreground; + } + var xy = rQ[rQi]; + rQi++; + var sx = (xy >> 4); + var sy = (xy & 0x0f); + + var wh = rQ[rQi]; + rQi++; + var sw = (wh >> 4) + 1; + var sh = (wh & 0x0f) + 1; + + this._display.subTile(sx, sy, sw, sh, color); + } + } + this._display.finishTile(); + } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } + + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } + + return true; + }, + + getTightCLength: function (arr) { + var header = 1, data = 0; + data += arr[0] & 0x7f; + if (arr[0] & 0x80) { + header++; + data += (arr[1] & 0x7f) << 7; + if (arr[1] & 0x80) { + header++; + data += arr[2] << 14; + } + } + return [header, data]; + }, + + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Tight protocol handler only implements true color mode"); + } + + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } + + var checksum = function (data) { + var sum = 0; + for (var i = 0; i < data.length; i++) { + sum += data[i]; + if (sum > 65536) sum -= 65536; + } + return sum; + }; + + var resetStreams = 0; + var streamId = -1; + var decompress = function (data) { + for (var i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + Util.Info("Reset zlib stream " + i); + } + } + + var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + } + + return uncompressed.data; + }.bind(this); + + var indexedToRGB = function (data, numColors, palette, width, height) { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + var dest = []; + var x, y, dp, sp; + if (numColors === 2) { + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); + + for (y = 0; y < height; y++) { + var b; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 3; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 3; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + } else { + for (y = 0; y < height; y++) { + for (x = 0; x < width; x++) { + dp = (y * width + x) * 3; + sp = data[y * width + x] * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + } + } + } + + return dest; + }.bind(this); + + var rQ = this._sock.get_rQ(); + var rQi = this._sock.get_rQi(); + var cmode, clength, data; + + var handlePalette = function () { + var numColors = rQ[rQi + 2] + 1; + var paletteSize = numColors * this._fb_depth; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } + + var bpp = (numColors <= 2) ? 1 : 8; + var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + var raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + clength = [0, rowSize * this._FBU.height]; + } else { + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize, + 3 + paletteSize + 3)); + } + + this._FBU.bytes += clength[0] + clength[1]; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQskipBytes(clength[0]); + + if (raw) { + data = this._sock.rQshiftBytes(clength[1]); + } else { + data = decompress(this._sock.rQshiftBytes(clength[1])); + } + + // Convert indexed (palette based) image data to RGB + var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height); + + this._display.renderQ_push({ + 'type': 'blitRgb', + 'data': rgb, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + + return true; + }.bind(this); + + var handleCopy = function () { + var raw = false; + var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; + if (uncompressedSize < 12) { + raw = true; + clength = [0, uncompressedSize]; + } else { + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); + } + this._FBU.bytes = 1 + clength[0] + clength[1]; + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + clength[0]); + + if (raw) { + data = this._sock.rQshiftBytes(clength[1]); + } else { + data = decompress(this._sock.rQshiftBytes(clength[1])); + } + + this._display.renderQ_push({ + 'type': 'blitRgb', + 'data': data, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + }); + + return true; + }.bind(this); + + var ctl = this._sock.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl = ctl >> 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = "fill"; + else if (ctl === 0x09) cmode = "jpeg"; + else if (ctl === 0x0A) cmode = "png"; + else if (ctl & 0x04) cmode = "filter"; + else if (ctl < 0x04) cmode = "copy"; + else return this._fail("Illegal tight compression received, ctl: " + ctl); + + if (isTightPNG && (cmode === "filter" || cmode === "copy")) { + return this._fail("filter/copy received in tightPNG mode"); + } + + switch (cmode) { + // fill use fb_depth because TPIXELs drop the padding byte + case "fill": // TPIXEL + this._FBU.bytes += this._fb_depth; + break; + case "jpeg": // max clength + this._FBU.bytes += 3; + break; + case "png": // max clength + this._FBU.bytes += 3; + break; + case "filter": // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case "copy": + break; + } + + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + switch (cmode) { + case "fill": + this._sock.rQskip8(); // shift off ctl + 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]] + }); + break; + case "png": + case "jpeg": + clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); + this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data + if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + + // We have everything, render it + this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length + var img = new Image(); + img.src = "data: image/" + cmode + + RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1])); + this._display.renderQ_push({ + 'type': 'img', + 'img': img, + 'x': this._FBU.x, + 'y': this._FBU.y + }); + img = null; + break; + case "filter": + var filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + // TODO(directxman12): why aren't we just calling '_fail' here + throw new Error("Unsupported tight subencoding received, filter: " + filterId); + } + break; + case "copy": + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, + + last_rect: function () { + this._FBU.rects = 0; + return true; + }, + + DesktopSize: function () { + Util.Debug(">> set_desktopsize"); + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._onFBResize(this, this._fb_width, this._fb_height); + this._display.resize(this._fb_width, this._fb_height); + this._timing.fbu_rt_start = (new Date()).getTime(); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_desktopsize"); + return true; + }, + + Cursor: function () { + Util.Debug(">> set_cursor"); + var x = this._FBU.x; // hotspot-x + var y = this._FBU.y; // hotspot-y + var w = this._FBU.width; + var h = this._FBU.height; + + var pixelslength = w * h * this._fb_Bpp; + var masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } + + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Util.Debug("<< set_cursor"); + return true; + }, + + JPEG_quality_lo: function () { + Util.Error("Server sent jpeg_quality pseudo-encoding"); + }, + + compress_lo: function () { + Util.Error("Server sent compress level pseudo-encoding"); } - - display.renderQ_push({ - 'type': 'blitRgb', - 'data': data, - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height}); - return true; - } - - ctl = ws.rQpeek8(); - - // Keep tight reset bits - resetStreams = ctl & 0xF; - - // Figure out filter - ctl = ctl >> 4; - streamId = ctl & 0x3; - - if (ctl === 0x08) cmode = "fill"; - else if (ctl === 0x09) cmode = "jpeg"; - else if (ctl === 0x0A) cmode = "png"; - else if (ctl & 0x04) cmode = "filter"; - else if (ctl < 0x04) cmode = "copy"; - else return fail("Illegal tight compression received, ctl: " + ctl); - - if (isTightPNG && (cmode === "filter" || cmode === "copy")) { - return fail("filter/copy received in tightPNG mode"); - } - - switch (cmode) { - // fill uses fb_depth because TPIXELs drop the padding byte - case "fill": FBU.bytes += fb_depth; break; // TPIXEL - case "jpeg": FBU.bytes += 3; break; // max clength - case "png": FBU.bytes += 3; break; // max clength - case "filter": FBU.bytes += 2; break; // filter id + num colors if palette - case "copy": break; - } - - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - - //Util.Debug(" ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - //Util.Debug(" cmode: " + cmode); - - // Determine FBU.bytes - switch (cmode) { - case "fill": - ws.rQshift8(); // shift off ctl - color = ws.rQshiftBytes(fb_depth); - display.renderQ_push({ - 'type': 'fill', - 'x': FBU.x, - 'y': FBU.y, - 'width': FBU.width, - 'height': FBU.height, - 'color': [color[2], color[1], color[0]] }); - break; - case "png": - case "jpeg": - clength = getTightCLength(ws.rQslice(1, 4)); - FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - - // We have everything, render it - //Util.Debug(" jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " + - // clength[0] + ", clength[1]: " + clength[1]); - ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length - img = new Image(); - img.src = "data:image/" + cmode + - extract_data_uri(ws.rQshiftBytes(clength[1])); - display.renderQ_push({ - 'type': 'img', - 'img': img, - 'x': FBU.x, - 'y': FBU.y}); - img = null; - break; - case "filter": - filterId = rQ[rQi + 1]; - if (filterId === 1) { - if (!handlePalette()) { return false; } - } else { - // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter - // Filter 2, Gradient is valid but not used if jpeg is enabled - throw("Unsupported tight subencoding received, filter: " + filterId); - } - break; - case "copy": - if (!handleCopy()) { return false; } - break; - } - - FBU.bytes = 0; - FBU.rects -= 1; - //Util.Debug(" ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")"); - //Util.Debug("<< display_tight_png"); - return true; -} - -extract_data_uri = function(arr) { - //var i, stra = []; - //for (i=0; i< arr.length; i += 1) { - // stra.push(String.fromCharCode(arr[i])); - //} - //return "," + escape(stra.join('')); - return ";base64," + Base64.encode(arr); -}; - -encHandlers.TIGHT = function () { return display_tight(false); }; -encHandlers.TIGHT_PNG = function () { return display_tight(true); }; - -encHandlers.last_rect = function last_rect() { - //Util.Debug(">> last_rect"); - FBU.rects = 0; - //Util.Debug("<< last_rect"); - return true; -}; - -encHandlers.DesktopSize = function set_desktopsize() { - Util.Debug(">> set_desktopsize"); - fb_width = FBU.width; - fb_height = FBU.height; - conf.onFBResize(that, fb_width, fb_height); - display.resize(fb_width, fb_height); - timing.fbu_rt_start = (new Date()).getTime(); - - FBU.bytes = 0; - FBU.rects -= 1; - - Util.Debug("<< set_desktopsize"); - return true; -}; - -encHandlers.Cursor = function set_cursor() { - var x, y, w, h, pixelslength, masklength; - Util.Debug(">> set_cursor"); - x = FBU.x; // hotspot-x - y = FBU.y; // hotspot-y - w = FBU.width; - h = FBU.height; - - pixelslength = w * h * fb_Bpp; - masklength = Math.floor((w + 7) / 8) * h; - - FBU.bytes = pixelslength + masklength; - if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; } - - //Util.Debug(" set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h); - - display.changeCursor(ws.rQshiftBytes(pixelslength), - ws.rQshiftBytes(masklength), - x, y, w, h); - - FBU.bytes = 0; - FBU.rects -= 1; - - Util.Debug("<< set_cursor"); - return true; -}; - -encHandlers.JPEG_quality_lo = function set_jpeg_quality() { - Util.Error("Server sent jpeg_quality pseudo-encoding"); -}; - -encHandlers.compress_lo = function set_compress_level() { - Util.Error("Server sent compress level pseudo-encoding"); -}; - -/* - * Client message routines - */ - -pixelFormat = function() { - //Util.Debug(">> pixelFormat"); - var arr; - arr = [0]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - - arr.push8(fb_Bpp * 8); // bits-per-pixel - arr.push8(fb_depth * 8); // depth - arr.push8(0); // little-endian - arr.push8(conf.true_color ? 1 : 0); // true-color - - arr.push16(255); // red-max - arr.push16(255); // green-max - arr.push16(255); // blue-max - arr.push8(16); // red-shift - arr.push8(8); // green-shift - arr.push8(0); // blue-shift - - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - //Util.Debug("<< pixelFormat"); - return arr; -}; - -clientEncodings = function() { - //Util.Debug(">> clientEncodings"); - var arr, i, encList = []; - - for (i=0; i> fbUpdateRequest"); - if (typeof(x) === "undefined") { x = 0; } - if (typeof(y) === "undefined") { y = 0; } - if (typeof(xw) === "undefined") { xw = fb_width; } - if (typeof(yw) === "undefined") { yw = fb_height; } - var arr; - arr = [3]; // msg-type - arr.push8(incremental); - arr.push16(x); - arr.push16(y); - arr.push16(xw); - arr.push16(yw); - //Util.Debug("<< fbUpdateRequest"); - return arr; -}; - -// Based on clean/dirty areas, generate requests to send -fbUpdateRequests = function() { - var cleanDirty = display.getCleanDirtyReset(), - arr = [], i, cb, db; - - cb = cleanDirty.cleanBox; - if (cb.w > 0 && cb.h > 0) { - // Request incremental for clean box - arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h)); - } - for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) { - db = cleanDirty.dirtyBoxes[i]; - // Force all (non-incremental for dirty box - arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h)); - } - return arr; -}; - - - -keyEvent = function(keysym, down) { - //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down); - var arr; - arr = [4]; // msg-type - arr.push8(down); - arr.push16(0); - arr.push32(keysym); - //Util.Debug("<< keyEvent"); - return arr; -}; - -pointerEvent = function(x, y) { - //Util.Debug(">> pointerEvent, x,y: " + x + "," + y + - // " , mask: " + mouse_buttonMask); - var arr; - arr = [5]; // msg-type - arr.push8(mouse_buttonMask); - arr.push16(x); - arr.push16(y); - //Util.Debug("<< pointerEvent"); - return arr; -}; - -clientCutText = function(text) { - //Util.Debug(">> clientCutText"); - var arr, i, n; - arr = [6]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - arr.push32(text.length); - n = text.length; - for (i=0; i < n; i+=1) { - arr.push(text.charCodeAt(i)); - } - //Util.Debug("<< clientCutText:" + arr); - return arr; -}; - - - -// -// Public API interface functions -// - -that.connect = function(host, port, password, path) { - //Util.Debug(">> connect"); - - rfb_host = host; - rfb_port = port; - rfb_password = (password !== undefined) ? password : ""; - rfb_path = (path !== undefined) ? path : ""; - - if ((!rfb_host) || (!rfb_port)) { - return fail("Must set host and port"); - } - - updateState('connect'); - //Util.Debug("<< connect"); - -}; - -that.disconnect = function() { - //Util.Debug(">> disconnect"); - updateState('disconnect', 'Disconnecting'); - //Util.Debug("<< disconnect"); -}; - -that.sendPassword = function(passwd) { - rfb_password = passwd; - rfb_state = "Authentication"; - setTimeout(init_msg, 1); -}; - -that.sendCtrlAltDel = function() { - if (rfb_state !== "normal" || conf.view_only) { return false; } - Util.Info("Sending Ctrl-Alt-Del"); - var arr = []; - arr = arr.concat(keyEvent(0xFFE3, 1)); // Control - arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt - arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete - arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete - arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt - arr = arr.concat(keyEvent(0xFFE3, 0)); // Control - ws.send(arr); -}; - -that.xvpOp = function(ver, op) { - if (rfb_xvp_ver < ver) { return false; } - Util.Info("Sending XVP operation " + op + " (version " + ver + ")") - ws.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); - return true; -}; - -that.xvpShutdown = function() { - return that.xvpOp(1, 2); -}; - -that.xvpReboot = function() { - return that.xvpOp(1, 3); -}; - -that.xvpReset = function() { - return that.xvpOp(1, 4); -}; - -// Send a key press. If 'down' is not specified then send a down key -// followed by an up key. -that.sendKey = function(code, down) { - if (rfb_state !== "normal" || conf.view_only) { return false; } - var arr = []; - if (typeof down !== 'undefined') { - Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); - arr = arr.concat(keyEvent(code, down ? 1 : 0)); - } else { - Util.Info("Sending key code (down + up): " + code); - arr = arr.concat(keyEvent(code, 1)); - arr = arr.concat(keyEvent(code, 0)); - } - ws.send(arr); -}; - -that.clipboardPasteFrom = function(text) { - if (rfb_state !== "normal") { return; } - //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "..."); - ws.send(clientCutText(text)); - //Util.Debug("<< clipboardPasteFrom"); -}; - -// Override internal functions for testing -that.testMode = function(override_send, data_mode) { - test_mode = true; - that.recv_message = ws.testMode(override_send, data_mode); - - checkEvents = function () { /* Stub Out */ }; - that.connect = function(host, port, password) { - rfb_host = host; - rfb_port = port; - rfb_password = password; - init_vars(); - updateState('ProtocolVersion', "Starting VNC handshake"); - }; -}; - - -return constructor(); // Return the public API interface - -} // End of RFB() + }; +})(); diff --git a/include/websock.js b/include/websock.js index bd3179a6..1b89a91f 100644 --- a/include/websock.js +++ b/include/websock.js @@ -100,6 +100,14 @@ function Websock() { return this._rQ[this._rQi++]; }, + rQskip8: function () { + this._rQi++; + }, + + rQskipBytes: function (num) { + this._rQi += num; + }, + rQunshift8: function (num) { if (this._rQi === 0) { this._rQ.unshift(num); diff --git a/tests/test.rfb.js b/tests/test.rfb.js new file mode 100644 index 00000000..595548e1 --- /dev/null +++ b/tests/test.rfb.js @@ -0,0 +1,1696 @@ +// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, jsunzip, des, display +// requires test modules: fake.websocket +/* jshint expr: true */ +var assert = chai.assert; +var expect = chai.expect; + +function make_rfb (extra_opts) { + if (!extra_opts) { + extra_opts = {}; + } + + extra_opts.target = extra_opts.target || document.createElement('canvas'); + return new RFB(extra_opts); +} + +// some useful assertions for noVNC +chai.use(function (_chai, utils) { + _chai.Assertion.addMethod('displayed', function (target_data) { + var obj = this._obj; + var data_cl = obj._drawCtx.getImageData(0, 0, obj._fb_width, obj._fb_height).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + var data = new Uint8Array(data_cl); + this.assert(utils.eql(data, target_data), + "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", + "expected #{this} not to have displayed the image #{act}", + target_data, + data); + }); + + _chai.Assertion.addMethod('sent', function (target_data) { + var obj = this._obj; + var data = obj._websocket._get_sent_data(); + this.assert(utils.eql(data, target_data), + "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", + "expected #{this} not to have sent the data #{act}", + target_data, + data); + }); +}); + +describe('Remote Frame Buffer Protocol Client', function() { + "use strict"; + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); + + describe('Public API Basic Behavior', function () { + var client; + beforeEach(function () { + client = make_rfb(); + }); + + describe('#connect', function () { + beforeEach(function () { client._updateState = sinon.spy(); }); + + it('should set the current state to "connect"', function () { + client.connect('host', 8675); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('connect'); + }); + + it('should fail if we are missing a host', function () { + sinon.spy(client, '_fail'); + client.connect(undefined, 8675); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should fail if we are missing a port', function () { + sinon.spy(client, '_fail'); + client.connect('abc'); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should not update the state if we are missing a host or port', function () { + sinon.spy(client, '_fail'); + client.connect('abc'); + expect(client._fail).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('failed'); + }); + }); + + describe('#disconnect', function () { + beforeEach(function () { client._updateState = sinon.spy(); }); + + it('should set the current state to "disconnect"', function () { + client.disconnect(); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('disconnect'); + }); + }); + + describe('#sendPassword', function () { + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should set the state to "Authentication"', function () { + client._rfb_state = "blah"; + client.sendPassword('pass'); + expect(client._rfb_state).to.equal('Authentication'); + }); + + it('should call init_msg "soon"', function () { + client._init_msg = sinon.spy(); + client.sendPassword('pass'); + this.clock.tick(5); + expect(client._init_msg).to.have.been.calledOnce; + }); + }); + + describe('#sendCtrlAlDel', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { + var expected = []; + expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 1)); + expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 1)); + expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 1)); + expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 0)); + expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 0)); + expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 0)); + + client.sendCtrlAltDel(); + expect(client._sock).to.have.sent(expected); + }); + + it('should not send the keys if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.sendCtrlAltDel(); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send the keys if we are set as view_only', function () { + client._view_only = true; + client.sendCtrlAltDel(); + expect(client._sock.send).to.not.have.been.called; + }); + }); + + describe('#sendKey', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should send a single key with the given code and state (down = true)', function () { + var expected = RFB.messages.keyEvent(123, 1); + client.sendKey(123, true); + expect(client._sock).to.have.sent(expected); + }); + + it('should send both a down and up event if the state is not specified', function () { + var expected = RFB.messages.keyEvent(123, 1); + expected = expected.concat(RFB.messages.keyEvent(123, 0)); + client.sendKey(123); + expect(client._sock).to.have.sent(expected); + }); + + it('should not send the key if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.sendKey(123); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send the key if we are set as view_only', function () { + client._view_only = true; + client.sendKey(123); + expect(client._sock.send).to.not.have.been.called; + }); + }); + + describe('#clipboardPasteFrom', function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + }); + + it('should send the given text in a paste event', function () { + var expected = RFB.messages.clientCutText('abc'); + client.clipboardPasteFrom('abc'); + expect(client._sock).to.have.sent(expected); + }); + + it('should not send the text if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.clipboardPasteFrom('abc'); + expect(client._sock.send).to.not.have.been.called; + }); + }); + + describe("XVP operations", function () { + beforeEach(function () { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + client._rfb_xvp_ver = 1; + }); + + it('should send the shutdown signal on #xvpShutdown', function () { + client.xvpShutdown(); + expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x02]); + }); + + it('should send the reboot signal on #xvpReboot', function () { + client.xvpReboot(); + expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x03]); + }); + + it('should send the reset signal on #xvpReset', function () { + client.xvpReset(); + expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x04]); + }); + + it('should support sending arbitrary XVP operations via #xvpOp', function () { + client.xvpOp(1, 7); + expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x07]); + }); + + it('should not send XVP operations with higher versions than we support', function () { + expect(client.xvpOp(2, 7)).to.be.false; + expect(client._sock.send).to.not.have.been.called; + }); + }); + }); + + describe('Misc Internals', function () { + describe('#_updateState', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should clear the disconnect timer if the state is not disconnect', function () { + var spy = sinon.spy(); + client._disconnTimer = setTimeout(spy, 50); + client._updateState('normal'); + this.clock.tick(51); + expect(spy).to.not.have.been.called; + expect(client._disconnTimer).to.be.null; + }); + }); + }); + + describe('Page States', function () { + describe('loaded', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('loaded'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('disconnected', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('disconnected'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('connect', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should reset the variable states', function () { + sinon.spy(client, '_init_vars'); + client._updateState('connect'); + expect(client._init_vars).to.have.been.calledOnce; + }); + + it('should actually connect to the websocket', function () { + sinon.spy(client._sock, 'open'); + client._updateState('connect'); + expect(client._sock.open).to.have.been.calledOnce; + }); + + it('should use wss:// to connect if encryption is enabled', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(true); + client._updateState('connect'); + expect(client._sock.open.args[0][0]).to.contain('wss://'); + }); + + it('should use ws:// to connect if encryption is not enabled', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(true); + client._updateState('connect'); + expect(client._sock.open.args[0][0]).to.contain('wss://'); + }); + + it('should use a uri with the host, port, and path specified to connect', function () { + sinon.spy(client._sock, 'open'); + client.set_encrypt(false); + client._rfb_host = 'HOST'; + client._rfb_port = 8675; + client._rfb_path = 'PATH'; + client._updateState('connect'); + expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); + }); + + it('should attempt to close the websocket before we open an new one', function () { + sinon.spy(client._sock, 'close'); + client._updateState('connect'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + describe('disconnect', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + client.connect('host', 8675); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should fail if we do not call Websock.onclose within the disconnection timeout', function () { + client._sock._websocket.close = function () {}; // explicitly don't call onclose + client._updateState('disconnect'); + this.clock.tick(client.get_disconnectTimeout() * 1000); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { + client._updateState('disconnect'); + this.clock.tick(client.get_disconnectTimeout() * 500); + client._sock._websocket.close(); + this.clock.tick(client.get_disconnectTimeout() * 500 + 1); + expect(client._rfb_state).to.equal('disconnected'); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('disconnect'); + expect(client._sock.close).to.have.been.calledTwice; // once on loaded, once on disconnect + }); + }); + + describe('failed', function () { + var client; + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + client = make_rfb(); + client.connect('host', 8675); + }); + + afterEach(function () { + this.clock.restore(); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('failed'); + expect(client._sock.close).to.have.been.called; + }); + + it('should transition to disconnected but stay in failed state', function () { + client.set_onUpdateState(sinon.spy()); + client._updateState('failed'); + this.clock.tick(50); + expect(client._rfb_state).to.equal('failed'); + + var onUpdateState = client.get_onUpdateState(); + expect(onUpdateState).to.have.been.called; + // it should be specifically the last call + expect(onUpdateState.args[onUpdateState.args.length - 1][1]).to.equal('disconnected'); + expect(onUpdateState.args[onUpdateState.args.length - 1][2]).to.equal('failed'); + }); + + }); + + describe('fatal', function () { + var client; + beforeEach(function () { client = make_rfb(); }); + + it('should close any open WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateState('fatal'); + expect(client._sock.close).to.have.been.calledOnce; + }); + }); + + // NB(directxman12): Normal does *nothing* in updateState + }); + + describe('Protocol Initialization States', function () { + describe('ProtocolVersion', function () { + beforeEach(function () { + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function () { + this.clock.restore(); + }); + + function send_ver (ver, client) { + var arr = new Uint8Array(12); + for (var i = 0; i < ver.length; i++) { + arr[i+4] = ver.charCodeAt(i); + } + arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' '; + arr[11] = '\n'; + client._sock._websocket._receive_data(arr); + } + + describe('version parsing', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + }); + + it('should interpret version 000.000 as a repeater', function () { + client._repeaterID = '\x01\x02\x03\x04\x05'; + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); + + var sent_data = client._sock._websocket._get_sent_data(); + expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]); + }); + + it('should interpret version 003.003 as version 3.3', function () { + send_ver('003.003', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.006 as version 3.3', function () { + send_ver('003.006', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.889 as version 3.3', function () { + send_ver('003.889', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.007 as version 3.7', function () { + send_ver('003.007', client); + expect(client._rfb_version).to.equal(3.7); + }); + + it('should interpret version 003.008 as version 3.8', function () { + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.000 as version 3.8', function () { + send_ver('004.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.001 as version 3.8', function () { + send_ver('004.001', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should fail on an invalid version', function () { + send_ver('002.000', client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + var client; + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + }); + + it('should handle two step repeater negotiation', function () { + client._repeaterID = '\x01\x02\x03\x04\x05'; + + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); + var sent_data = client._sock._websocket._get_sent_data(); + expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]); + expect(sent_data).to.have.length(250); + + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should initialize the flush interval', function () { + client._sock.flush = sinon.spy(); + send_ver('003.008', client); + this.clock.tick(100); + expect(client._sock.flush).to.have.been.calledThrice; + }); + + it('should send back the interpreted version', function () { + send_ver('004.000', client); + + var expected_str = 'RFB 003.008\n'; + var expected = []; + for (var i = 0; i < expected_str.length; i++) { + expected[i] = expected_str.charCodeAt(i); + } + + expect(client._sock).to.have.sent(expected); + }); + + it('should transition to the Security state on successful negotiation', function () { + send_ver('003.008', client); + expect(client._rfb_state).to.equal('Security'); + }); + }); + + describe('Security', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + }); + + it('should simply receive the auth scheme when for versions < 3.7', function () { + client._rfb_version = 3.6; + var auth_scheme_raw = [1, 2, 3, 4]; + var auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) + + (auth_scheme_raw[2] << 8) + auth_scheme_raw[3]; + client._sock._websocket._receive_data(auth_scheme_raw); + expect(client._rfb_auth_scheme).to.equal(auth_scheme); + }); + + it('should choose for the most prefered scheme possible for versions >= 3.7', function () { + client._rfb_version = 3.7; + var auth_schemes = [2, 1, 2]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_auth_scheme).to.equal(2); + expect(client._sock).to.have.sent([2]); + }); + + it('should fail if there are no supported schemes for versions >= 3.7', function () { + client._rfb_version = 3.7; + var auth_schemes = [1, 32]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { + client._rfb_version = 3.7; + var failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(failure_data); + + expect(client._fail).to.have.been.calledTwice; + expect(client._fail).to.have.been.calledWith('Security failure: whoops'); + }); + + it('should transition to the Authentication state and continue on successful negotiation', function () { + client._rfb_version = 3.7; + var auth_schemes = [1, 1]; + client._negotiate_authentication = sinon.spy(); + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_state).to.equal('Authentication'); + expect(client._negotiate_authentication).to.have.been.calledOnce; + }); + }); + + describe('Authentication', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'Security'; + }); + + function send_security(type, cl) { + cl._sock._websocket._receive_data(new Uint8Array([1, type])); + } + + it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { + client._rfb_version = 3.6; + var err_msg = "Whoopsies"; + var data = [0, 0, 0, 0]; + var err_len = err_msg.length; + data.push32(err_len); + for (var i = 0; i < err_len; i++) { + data.push(err_msg.charCodeAt(i)); + } + + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(client._rfb_state).to.equal('failed'); + expect(client._fail).to.have.been.calledWith('Auth failure: Whoopsies'); + }); + + it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { + client._rfb_version = 3.8; + send_security(1, client); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + it('should transition straight to ClientInitialisation on "no auth" for versions < 3.8', function () { + client._rfb_version = 3.7; + sinon.spy(client, '_updateState'); + send_security(1, client); + expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); + expect(client._rfb_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an unknown auth scheme', function () { + client._rfb_version = 3.8; + send_security(57, client); + expect(client._rfb_state).to.equal('failed'); + }); + + describe('VNC Authentication (type 2) 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; + }); + + it('should transition to the "password" state if missing a password', function () { + send_security(2, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should encrypt the password with DES and then send it back', function () { + client._rfb_password = 'passwd'; + send_security(2, client); + client._sock._websocket._get_sent_data(); // skip the choice of auth reply + + var challenge = []; + for (var i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + var des_pass = RFB.genDES('passwd', challenge); + expect(client._sock).to.have.sent(des_pass); + }); + + it('should transition to SecurityResult immediately after sending the password', function () { + client._rfb_password = 'passwd'; + send_security(2, client); + + var challenge = []; + for (var i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); + + expect(client._rfb_state).to.equal('SecurityResult'); + }); + }); + + describe('XVP Authentication (type 22) 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; + }); + + it('should fall through to standard VNC authentication upon completion', function () { + client.set_xvp_password_sep('#'); + client._rfb_password = 'user#target#password'; + client._negotiate_std_vnc_auth = sinon.spy(); + send_security(22, client); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + }); + + it('should transition to the "password" state if the passwords is missing', function() { + send_security(22, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should transition to the "password" state if the passwords is improperly formatted', function() { + client._rfb_password = 'user@target'; + send_security(22, client); + expect(client._rfb_state).to.equal('password'); + }); + + it('should split the password, send the first two parts, and pass on the last part', function () { + client.set_xvp_password_sep('#'); + client._rfb_password = 'user#target#password'; + client._negotiate_std_vnc_auth = sinon.spy(); + + send_security(22, client); + + expect(client._rfb_password).to.equal('password'); + + var expected = [22, 4, 6]; // auth selection, len user, len target + for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } + + expect(client._sock).to.have.sent(expected); + }); + }); + + describe('TightVNC Authentication (type 16) 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 + }); + + function send_num_str_pairs(pairs, client) { + var pairs_len = pairs.length; + var data = []; + data.push32(pairs_len); + + for (var i = 0; i < pairs_len; i++) { + data.push32(pairs[i][0]); + var j; + for (j = 0; j < 4; j++) { + data.push(pairs[i][1].charCodeAt(j)); + } + for (j = 0; j < 8; j++) { + data.push(pairs[i][2].charCodeAt(j)); + } + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should skip tunnel negotiation if no tunnels are requested', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.true; + }); + + it('should fail if no supported tunnels are listed', function () { + send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should choose the notunnel tunnel type', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); + expect(client._sock).to.have.sent([0, 0, 0, 0]); + }); + + it('should continue to sub-auth negotiation after tunnel negotiation', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._get_sent_data(); // skip the tunnel choice here + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + /*it('should attempt to use VNC auth over no auth when possible', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client); + expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + });*/ // while this would make sense, the original code doesn't actually do this + + it('should accept the "no auth" auth type and transition to SecurityResult', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._rfb_state).to.equal('SecurityResult'); + }); + + it('should accept VNC authentication and transition to that', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); + expect(client._sock).to.have.sent([0, 0, 0, 2]); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); + }); + + it('should fail if there are no supported auth types', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[23, 'stdv', 'badval__']], client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + }); + + describe('SecurityResult', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'SecurityResult'; + }); + + it('should fall through to ClientInitialisation on a response code of 0', function () { + client._updateState = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('ClientInitialisation'); + }); + + it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { + client._rfb_version = 3.8; + sinon.spy(client, '_fail'); + var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(client._rfb_state).to.equal('failed'); + expect(client._fail).to.have.been.calledWith('whoops'); + }); + + it('should fail on an error code of 1 with a standard message for version < 3.8', function () { + client._rfb_version = 3.7; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + describe('ClientInitialisation', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'SecurityResult'; + }); + + it('should transition to the ServerInitialisation state', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_state).to.equal('ServerInitialisation'); + }); + + it('should send 1 if we are in shared mode', function () { + client.set_shared(true); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent([1]); + }); + + it('should send 0 if we are not in shared mode', function () { + client.set_shared(false); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent([0]); + }); + }); + + describe('ServerInitialisation', function () { + var client; + + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'ServerInitialisation'; + }); + + function send_server_init(opts, client) { + var full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0, + true_color: 1, red_max: 255, green_max: 255, blue_max: 255, + red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' }; + for (var opt in opts) { + full_opts[opt] = opts[opt]; + } + var data = []; + + data.push16(full_opts.width); + data.push16(full_opts.height); + + data.push(full_opts.bpp); + data.push(full_opts.depth); + data.push(full_opts.big_endian); + data.push(full_opts.true_color); + + data.push16(full_opts.red_max); + data.push16(full_opts.green_max); + data.push16(full_opts.blue_max); + data.push8(full_opts.red_shift); + data.push8(full_opts.green_shift); + data.push8(full_opts.blue_shift); + + // padding + data.push8(0); + data.push8(0); + data.push8(0); + + client._sock._websocket._receive_data(new Uint8Array(data)); + + var name_data = []; + name_data.push32(full_opts.name.length); + for (var i = 0; i < full_opts.name.length; i++) { + name_data.push(full_opts.name.charCodeAt(i)); + } + client._sock._websocket._receive_data(new Uint8Array(name_data)); + } + + it('should set the framebuffer width and height', function () { + send_server_init({ width: 32, height: 84 }, client); + expect(client._fb_width).to.equal(32); + 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); + + var spy = client.get_onDesktopName(); + expect(client._fb_name).to.equal('some name'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal('some name'); + }); + + it('should handle the extended init message of the tight encoding', function () { + // NB(sross): we don't actually do anything with it, so just test that we can + // read it w/o throwing an error + client._rfb_tightvnc = true; + send_server_init({}, client); + + var tight_data = []; + tight_data.push16(1); + tight_data.push16(2); + tight_data.push16(3); + tight_data.push16(0); + for (var i = 0; i < 16 + 32 + 48; i++) { + tight_data.push(i); + } + client._sock._websocket._receive_data(tight_data); + + 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'); + send_server_init({ width: 27, height: 32 }, client); + + var spy = client.get_onFBResize(); + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(27, 32); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal(27); + expect(spy.args[0][2]).to.equal(32); + }); + + it('should grab the mouse and keyboard', function () { + sinon.spy(client._keyboard, 'grab'); + sinon.spy(client._mouse, 'grab'); + send_server_init({}, client); + expect(client._keyboard.grab).to.have.been.calledOnce; + 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 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); + }); + + // 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); + 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 } ] }; + expected = expected.concat(RFB.messages.fbUpdateRequests(expected_cdr, 27, 32)); + + send_server_init({ width: 27, height: 32 }, client); + expect(client._sock).to.have.sent(expected); + }); + + it('should check for sending mouse events', function () { + // be lazy with our checking so we don't have to check through the whole sent buffer + sinon.spy(client, '_checkEvents'); + send_server_init({}, client); + expect(client._checkEvents).to.have.been.calledOnce; + }); + + it('should transition to the "normal" state', function () { + send_server_init({}, client); + expect(client._rfb_state).to.equal('normal'); + }); + }); + }); + + describe('Protocol Message Processing After Completing Initialization', 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._fb_width = 640; + client._fb_height = 20; + }); + + describe('Framebuffer Update Handling', 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._fb_width = 640; + client._fb_height = 20; + }); + + 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 + ]; + var target_data; + + var target_data_check_arr = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + var target_data_check; + + before(function () { + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray + target_data = new Uint8Array(target_data_arr); + target_data_check = new Uint8Array(target_data_check_arr); + }); + + function send_fbu_msg (rect_info, rect_data, client, rect_cnt) { + var data = []; + + if (!rect_cnt || rect_cnt > -1) { + // header + data.push(0); // msg type + data.push(0); // padding + data.push16(rect_cnt || rect_data.length); + } + + for (var i = 0; i < rect_data.length; i++) { + if (rect_info[i]) { + data.push16(rect_info[i].x); + data.push16(rect_info[i].y); + data.push16(rect_info[i].width); + data.push16(rect_info[i].height); + data.push32(rect_info[i].encoding); + } + data = data.concat(rect_data[i]); + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should send an update request if there is sufficient data', function () { + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, + dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] }; + var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20); + + client._framebufferUpdate = function () { return true; }; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg); + }); + + it('should not send an update request if we need more data', function () { + client._sock._websocket._receive_data(new Uint8Array([0])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should resume receiving an update if we previously did not have enough data', function () { + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, + dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] }; + var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20); + + // just enough to set FBU.rects + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._framebufferUpdate = function () { return true; }; // we magically have enough data + // 247 should *not* be used as the message type here + client._sock._websocket._receive_data(new Uint8Array([247])); + expect(client._sock).to.have.sent(expected_msg); + }); + + it('should parse out information from a header before any actual data comes in', function () { + client.set_onFBUReceive(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' }; + send_fbu_msg([rect_info], [[]], client); + + var spy = client.get_onFBUReceive(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, rect_info); + }); + + it('should fire onFBUComplete when the update is complete', function () { + client.set_onFBUComplete(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: -224, encodingName: 'last_rect' }; + send_fbu_msg([rect_info], [[]], client); // last_rect + + var spy = client.get_onFBUComplete(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, rect_info); + }); + + it('should not fire onFBUComplete if we have not finished processing the update', function () { + client.set_onFBUComplete(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x00, encodingName: 'RAW' }; + send_fbu_msg([rect_info], [[]], client); + expect(client.get_onFBUComplete()).to.not.have.been.called; + }); + + it('should call the appropriate encoding handler', function () { + client._encHandlers[0x02] = sinon.spy(); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02 }; + send_fbu_msg([rect_info], [[]], client); + expect(client._encHandlers[0x02]).to.have.been.calledOnce; + }); + + it('should fail on an unsupported encoding', function () { + client.set_onFBUReceive(sinon.spy()); + var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; + send_fbu_msg([rect_info], [[]], client); + expect(client._rfb_state).to.equal('failed'); + }); + + it('should be able to pause and resume receiving rects if not enought data', function () { + // seed some initial data to copy + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + var initial_data = client._display._drawCtx.createImageData(4, 2); + var initial_data_arr = target_data_check_arr.slice(0, 32); + for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; } + client._display._drawCtx.putImageData(initial_data, 0, 0); + + var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + var rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg([info[0]], [rects[0]], client, 2); + send_fbu_msg([info[1]], [rects[1]], client, -1); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('Message Encoding Handlers', 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'; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._fb_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); + }); + + it('should handle the COPYRECT encoding', function () { + // seed some initial data to copy + var initial_data = client._display._drawCtx.createImageData(4, 2); + var initial_data_arr = target_data_check_arr.slice(0, 32); + for (var i = 0; i < 32; i++) { initial_data.data[i] = initial_data_arr[i]; } + client._display._drawCtx.putImageData(initial_data, 0, 0); + + var info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, + { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + var rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + // TODO(directxman12): for encodings with subrects, test resuming on partial send? + // TODO(directxman12): test rre_chunk_sz (related to above about subrects)? + + it('should handle the RRE encoding', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }]; + 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); + 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); + rect.push(0xff); + rect.push16(2); // x: 2 + rect.push16(2); // y: 2 + rect.push16(2); // width: 2 + rect.push16(2); // height: 2 + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('the HEXTILE 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'; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._fb_Bpp = 4; + }); + + it('should handle a tile with fg, bg specified, normal subrects', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + 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); + rect.push(0xff); + rect.push(2); // 2 subrects + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle a raw tile', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + 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 + 3]); + } + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle a tile with only bg specified (solid bg)', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + rect.push(0x02); + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + send_fbu_msg(info, [rect], client); + + var expected = []; + for (var i = 0; i < 16; i++) { expected.push32(0xff00ff); } + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with bg and coloured subrects', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rect = []; + 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); + 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); + 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); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should carry over fg and bg colors from the previous tile if not specified', function () { + client._fb_width = 4; + client._fb_height = 17; + client._display.resize(4, 17); + + var info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}]; + 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); + rect.push(0xff); + rect.push(8); // 8 subrects + var i; + for (i = 0; i < 4; i++) { + rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + } + rect.push(0x08); // anysubrects + rect.push(1); // 1 subrect + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + + var expected = []; + for (i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); } + expected = expected.concat(target_data_check_arr.slice(0, 16)); + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should fail on an invalid subencoding', function () { + var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; + var rects = [[45]]; // an invalid subencoding + send_fbu_msg(info, rects, client); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + it.skip('should handle the TIGHT encoding', function () { + // TODO(directxman12): test this + }); + + it.skip('should handle the TIGHT_PNG encoding', function () { + // TODO(directxman12): test this + }); + + it('should handle the DesktopSize pseduo-encoding', function () { + client.set_onFBResize(sinon.spy()); + sinon.spy(client._display, 'resize'); + send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client); + + var spy = client.get_onFBResize(); + expect(spy).to.have.been.calledOnce; + expect(spy).to.have.been.calledWith(sinon.match.any, 20, 50); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + it.skip('should handle the Cursor pseudo-encoding', function () { + // TODO(directxman12): test + }); + + it('should handle the last_rect pseudo-encoding', function () { + client.set_onFBUReceive(sinon.spy()); + send_fbu_msg([{ x: 0, y: 0, width: 0, height: 0, encoding: -224}], [[]], client, 100); + expect(client._FBU.rects).to.equal(0); + expect(client.get_onFBUReceive()).to.have.been.calledOnce; + }); + }); + }); + + it('should set the colour map on the display on SetColourMapEntries', function () { + var expected_cm = []; + var data = [1, 0, 0, 1, 0, 4]; + var i; + for (i = 0; i < 4; i++) { + expected_cm[i + 1] = [i * 10, i * 10 + 1, i * 10 + 2]; + data.push16(expected_cm[i + 1][2] << 8); + data.push16(expected_cm[i + 1][1] << 8); + data.push16(expected_cm[i + 1][0] << 8); + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(client._display.get_colourMap()).to.deep.equal(expected_cm); + }); + + describe('XVP Message Handling', function () { + beforeEach(function () { + client = make_rfb(); + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client._fb_name = 'some device'; + client._fb_width = 27; + client._fb_height = 32; + }); + + it('should call updateState with a message on XVP_FAIL, but keep the same state', function () { + client._updateState = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 0])); + expect(client._updateState).to.have.been.calledOnce; + expect(client._updateState).to.have.been.calledWith('normal', 'Operation Failed'); + }); + + it('should set the XVP version and fire the callback with the version on XVP_INIT', function () { + client.set_onXvpInit(sinon.spy()); + client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 1])); + expect(client._rfb_xvp_ver).to.equal(10); + expect(client.get_onXvpInit()).to.have.been.calledOnce; + expect(client.get_onXvpInit()).to.have.been.calledWith(10); + }); + + it('should fail on unknown XVP message types', function () { + client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237])); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + it('should fire the clipboard callback with the retrieved text on ServerCutText', function () { + var expected_str = 'cheese!'; + var data = [3, 0, 0, 0]; + data.push32(expected_str.length); + for (var i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); } + client.set_onClipboard(sinon.spy()); + + client._sock._websocket._receive_data(new Uint8Array(data)); + var spy = client.get_onClipboard(); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.equal(expected_str); + }); + + it('should fire the bell callback on Bell', function () { + client.set_onBell(sinon.spy()); + client._sock._websocket._receive_data(new Uint8Array([2])); + expect(client.get_onBell()).to.have.been.calledOnce; + }); + + it('should fail on an unknown message type', function () { + client._sock._websocket._receive_data(new Uint8Array([87])); + expect(client._rfb_state).to.equal('failed'); + }); + }); + + describe('Asynchronous Events', function () { + describe('Mouse event handlers', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client._sock.send = sinon.spy(); + client._rfb_state = 'normal'; + }); + + it('should not send button messages in view-only mode', function () { + client._view_only = true; + client._mouse._onMouseButton(0, 0, 1, 0x001); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send movement messages in view-only mode', function () { + client._view_only = true; + client._mouse._onMouseMove(0, 0); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should send a pointer event on mouse button presses', function () { + client._mouse._onMouseButton(10, 12, 1, 0x001); + expect(client._sock.send).to.have.been.calledOnce; + var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001); + expect(client._sock.send).to.have.been.calledWith(pointer_msg); + }); + + it('should send a pointer event on mouse movement', function () { + client._mouse._onMouseMove(10, 12); + expect(client._sock.send).to.have.been.calledOnce; + var pointer_msg = RFB.messages.pointerEvent(10, 12, 0); + expect(client._sock.send).to.have.been.calledWith(pointer_msg); + }); + + it('should set the button mask so that future mouse movements use it', function () { + client._mouse._onMouseButton(10, 12, 1, 0x010); + client._sock.send = sinon.spy(); + client._mouse._onMouseMove(13, 9); + expect(client._sock.send).to.have.been.calledOnce; + var pointer_msg = RFB.messages.pointerEvent(13, 9, 0x010); + expect(client._sock.send).to.have.been.calledWith(pointer_msg); + }); + + // NB(directxman12): we don't need to test not sending messages in + // non-normal modes, since we haven't grabbed input + // yet (grabbing input should be checked in the lifecycle tests). + + it('should not send movement messages when viewport dragging', function () { + client._viewportDragging = true; + client._display.viewportChange = sinon.spy(); + client._mouse._onMouseMove(13, 9); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send button messages when initiating viewport dragging', function () { + client._viewportDrag = true; + client._mouse._onMouseButton(13, 9, 0x001); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should be initiate viewport dragging on a button down event, if enabled', function () { + client._viewportDrag = true; + client._mouse._onMouseButton(13, 9, 0x001); + expect(client._viewportDragging).to.be.true; + expect(client._viewportDragPos).to.deep.equal({ x: 13, y: 9 }); + }); + + it('should terminate viewport dragging on a button up event, if enabled', function () { + client._viewportDrag = true; + client._viewportDragging = true; + client._mouse._onMouseButton(13, 9, 0x000); + expect(client._viewportDragging).to.be.false; + }); + + it('if enabled, viewportDragging should occur on mouse movement while a button is down', function () { + client._viewportDrag = true; + client._viewportDragging = true; + client._viewportDragPos = { x: 13, y: 9 }; + client._display.viewportChange = sinon.spy(); + + client._mouse._onMouseMove(10, 4); + + expect(client._viewportDragging).to.be.true; + expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 }); + expect(client._display.viewportChange).to.have.been.calledOnce; + expect(client._display.viewportChange).to.have.been.calledWith(3, 5); + }); + }); + + describe('Keyboard Event Handlers', function () { + var client; + beforeEach(function () { + client = make_rfb(); + client._sock.send = sinon.spy(); + }); + + it('should send a key message on a key press', function () { + client._keyboard._onKeyPress(1234, 1); + expect(client._sock.send).to.have.been.calledOnce; + var key_msg = RFB.messages.keyEvent(1234, 1); + expect(client._sock.send).to.have.been.calledWith(key_msg); + }); + + it('should not send messages in view-only mode', function () { + client._view_only = true; + client._keyboard._onKeyPress(1234, 1); + expect(client._sock.send).to.not.have.been.called; + }); + }); + + describe('WebSocket event handlers', function () { + var client; + beforeEach(function () { + client = make_rfb(); + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function () { this.clock.restore(); }); + + // message events + it ('should do nothing if we receive an empty message and have nothing in the queue', function () { + client.connect('host', 8675); + client._rfb_state = 'normal'; + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(Base64.encode([])); + expect(client._normal_msg).to.not.have.been.called; + }); + + it('should handle a message in the normal state as a normal message', function () { + client.connect('host', 8675); + client._rfb_state = 'normal'; + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); + expect(client._normal_msg).to.have.been.calledOnce; + }); + + it('should handle a message in any non-disconnected/failed state like an init message', function () { + client.connect('host', 8675); + client._rfb_state = 'ProtocolVersion'; + client._init_msg = sinon.spy(); + client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); + expect(client._init_msg).to.have.been.calledOnce; + }); + + it('should split up the handling of muplitle normal messages across 10ms intervals', function () { + client.connect('host', 8675); + client._sock._websocket._open(); + client._rfb_state = 'normal'; + client.set_onBell(sinon.spy()); + client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); + expect(client.get_onBell()).to.have.been.calledOnce; + this.clock.tick(20); + expect(client.get_onBell()).to.have.been.calledTwice; + }); + + // open events + it('should update the state to ProtocolVersion on open (if the state is "connect")', function () { + client.connect('host', 8675); + client._sock._websocket._open(); + expect(client._rfb_state).to.equal('ProtocolVersion'); + }); + + it('should fail if we are not currently ready to connect and we get an "open" event', function () { + client.connect('host', 8675); + client._rfb_state = 'some_other_state'; + client._sock._websocket._open(); + expect(client._rfb_state).to.equal('failed'); + }); + + // close events + it('should transition to "disconnected" from "disconnect" on a close event', function () { + client.connect('host', 8675); + client._rfb_state = 'disconnect'; + client._sock._websocket.close(); + expect(client._rfb_state).to.equal('disconnected'); + }); + + it('should transition to failed if we get a close event from any non-"disconnection" state', function () { + client.connect('host', 8675); + client._rfb_state = 'normal'; + client._sock._websocket.close(); + expect(client._rfb_state).to.equal('failed'); + }); + + // error events do nothing + }); + }); +}); From bbbf42bb5a70cddd44d5d1f2ec1c6ae9156034bb Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 21 Jun 2014 23:26:28 -0400 Subject: [PATCH 013/527] Cleanup: UI code File: ui.js Tests Added: False Changes: - Fix JSHint errors - add some curly braces to improve clarity - move variable declarations to relevant locations instead of at the top of methods --- include/ui.js | 1864 ++++++++++++++++++++++++------------------------- 1 file changed, 920 insertions(+), 944 deletions(-) diff --git a/include/ui.js b/include/ui.js index f6afccc9..e869aa69 100644 --- a/include/ui.js +++ b/include/ui.js @@ -7,997 +7,973 @@ * See README.md for usage and integration instructions. */ -"use strict"; -/*jslint white: false, browser: true */ -/*global window, $D, Util, WebUtil, RFB, Display */ +/* jslint white: false, browser: true */ +/* global window, $D, Util, WebUtil, RFB, Display */ -// Load supporting scripts -window.onscriptsload = function () { UI.load(); }; -window.onload = function () { UI.keyboardinputReset(); }; -Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", - "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js", "keysym.js"]); +var UI; -var UI = { +(function () { + "use strict"; -rfb_state : 'loaded', -settingsOpen : false, -connSettingsOpen : false, -popupStatusOpen : false, -clipboardOpen: false, -keyboardVisible: false, -hideKeyboardTimeout: null, -lastKeyboardinput: null, -defaultKeyboardinputLen: 100, -extraKeysVisible: false, -ctrlOn: false, -altOn: false, -isTouchDevice: false, + // Load supporting scripts + window.onscriptsload = function () { UI.load(); }; + window.onload = function () { UI.keyboardinputReset(); }; + Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", + "keysymdef.js", "keyboard.js", "input.js", "display.js", + "jsunzip.js", "rfb.js", "keysym.js"]); -// Setup rfb object, load settings from browser storage, then call -// UI.init to setup the UI/menus -load: function (callback) { - WebUtil.initSettings(UI.start, callback); -}, + var UI = { -// Render default UI and initialize settings menu -start: function(callback) { - var html = '', i, sheet, sheets, llevels, port, autoconnect; + rfb_state : 'loaded', + settingsOpen : false, + connSettingsOpen : false, + popupStatusOpen : false, + clipboardOpen: false, + keyboardVisible: false, + hideKeyboardTimeout: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + extraKeysVisible: false, + ctrlOn: false, + altOn: false, + isTouchDevice: false, - UI.isTouchDevice = 'ontouchstart' in document.documentElement; + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load: function (callback) { + WebUtil.initSettings(UI.start, callback); + }, - // Stylesheet selection dropdown - sheet = WebUtil.selectStylesheet(); - sheets = WebUtil.getStylesheets(); - for (i = 0; i < sheets.length; i += 1) { - UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); - } + // Render default UI and initialize settings menu + start: function(callback) { + UI.isTouchDevice = 'ontouchstart' in document.documentElement; - // Logging selection dropdown - llevels = ['error', 'warn', 'info', 'debug']; - for (i = 0; i < llevels.length; i += 1) { - UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); - } - - // Settings with immediate effects - UI.initSetting('logging', 'warn'); - WebUtil.init_logging(UI.getSetting('logging')); - - UI.initSetting('stylesheet', 'default'); - WebUtil.selectStylesheet(null); - // call twice to get around webkit bug - WebUtil.selectStylesheet(UI.getSetting('stylesheet')); - - // if port == 80 (or 443) then it won't be present and should be - // set manually - port = window.location.port; - if (!port) { - if (window.location.protocol.substring(0,5) == 'https') { - port = 443; - } - else if (window.location.protocol.substring(0,4) == 'http') { - port = 80; - } - } - - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('host', window.location.hostname); - UI.initSetting('port', port); - UI.initSetting('password', ''); - UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('true_color', true); - UI.initSetting('cursor', !UI.isTouchDevice); - UI.initSetting('shared', true); - UI.initSetting('view_only', false); - UI.initSetting('path', 'websockify'); - UI.initSetting('repeaterID', ''); - - UI.rfb = RFB({'target': $D('noVNC_canvas'), - 'onUpdateState': UI.updateState, - 'onXvpInit': UI.updateXvpVisualState, - 'onClipboard': UI.clipReceive, - 'onDesktopName': UI.updateDocumentTitle}); - - autoconnect = WebUtil.getQueryVar('autoconnect', false); - if (autoconnect === 'true' || autoconnect == '1') { - autoconnect = true; - UI.connect(); - } else { - autoconnect = false; - } - - UI.updateVisualState(); - - // Unfocus clipboard when over the VNC area - //$D('VNC_screen').onmousemove = function () { - // var keyboard = UI.rfb.get_keyboard(); - // if ((! keyboard) || (! keyboard.get_focused())) { - // $D('VNC_clipboard_text').blur(); - // } - // }; - - // Show mouse selector buttons on touch screen devices - if (UI.isTouchDevice) { - // Show mobile buttons - $D('noVNC_mobile_buttons').style.display = "inline"; - UI.setMouseButton(); - // Remove the address bar - setTimeout(function() { window.scrollTo(0, 1); }, 100); - UI.forceSetting('clip', true); - $D('noVNC_clip').disabled = true; - } else { - UI.initSetting('clip', false); - } - - //iOS Safari does not support CSS position:fixed. - //This detects iOS devices and enables javascript workaround. - if ((navigator.userAgent.match(/iPhone/i)) || - (navigator.userAgent.match(/iPod/i)) || - (navigator.userAgent.match(/iPad/i))) { - //UI.setOnscroll(); - //UI.setResize(); - } - UI.setBarPosition(); - - $D('noVNC_host').focus(); - - UI.setViewClip(); - Util.addEvent(window, 'resize', UI.setViewClip); - - Util.addEvent(window, 'beforeunload', function () { - if (UI.rfb_state === 'normal') { - return "You are currently connected."; - } - } ); - - // Show description by default when hosted at for kanaka.github.com - if (location.host === "kanaka.github.io") { - // Open the description dialog - $D('noVNC_description').style.display = "block"; - } else { - // Show the connect panel on first load unless autoconnecting - if (autoconnect === UI.connSettingsOpen) { - UI.toggleConnectPanel(); - } - } - - // Add mouse event click/focus/blur event handlers to the UI - UI.addMouseHandlers(); - - if (typeof callback === "function") { - callback(UI.rfb); - } -}, - -addMouseHandlers: function() { - // Setup interface handlers that can't be inline - $D("noVNC_view_drag_button").onclick = UI.setViewDrag; - $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; - $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; - $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; - $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; - $D("showKeyboard").onclick = UI.showKeyboard; - - $D("keyboardinput").oninput = UI.keyInput; - $D("keyboardinput").onblur = UI.keyInputBlur; - - $D("showExtraKeysButton").onclick = UI.showExtraKeys; - $D("toggleCtrlButton").onclick = UI.toggleCtrl; - $D("toggleAltButton").onclick = UI.toggleAlt; - $D("sendTabButton").onclick = UI.sendTab; - $D("sendEscButton").onclick = UI.sendEsc; - - $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; - $D("xvpShutdownButton").onclick = UI.xvpShutdown; - $D("xvpRebootButton").onclick = UI.xvpReboot; - $D("xvpResetButton").onclick = UI.xvpReset; - $D("noVNC_status").onclick = UI.togglePopupStatusPanel; - $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel; - $D("xvpButton").onclick = UI.toggleXvpPanel; - $D("clipboardButton").onclick = UI.toggleClipboardPanel; - $D("settingsButton").onclick = UI.toggleSettingsPanel; - $D("connectButton").onclick = UI.toggleConnectPanel; - $D("disconnectButton").onclick = UI.disconnect; - $D("descriptionButton").onclick = UI.toggleConnectPanel; - - $D("noVNC_clipboard_text").onfocus = UI.displayBlur; - $D("noVNC_clipboard_text").onblur = UI.displayFocus; - $D("noVNC_clipboard_text").onchange = UI.clipSend; - $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; - - $D("noVNC_settings_menu").onmouseover = UI.displayBlur; - $D("noVNC_settings_menu").onmouseover = UI.displayFocus; - $D("noVNC_apply").onclick = UI.settingsApply; - - $D("noVNC_connect_button").onclick = UI.connect; -}, - -// Read form control compatible setting from cookie -getSetting: function(name) { - var val, ctrl = $D('noVNC_' + name); - val = WebUtil.readSetting(name); - if (val !== null && ctrl.type === 'checkbox') { - if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { - val = false; - } else { - val = true; - } - } - return val; -}, - -// Update cookie and form control setting. If value is not set, then -// updates from control to current cookie setting. -updateSetting: function(name, value) { - - var i, ctrl = $D('noVNC_' + name); - // Save the cookie for this session - if (typeof value !== 'undefined') { - WebUtil.writeSetting(name, value); - } - - // Update the settings control - value = UI.getSetting(name); - - if (ctrl.type === 'checkbox') { - ctrl.checked = value; - - } else if (typeof ctrl.options !== 'undefined') { - for (i = 0; i < ctrl.options.length; i += 1) { - if (ctrl.options[i].value === value) { - ctrl.selectedIndex = i; - break; + // Stylesheet selection dropdown + var sheet = WebUtil.selectStylesheet(); + var sheets = WebUtil.getStylesheets(); + var i; + for (i = 0; i < sheets.length; i += 1) { + UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); } - } - } else { - /*Weird IE9 error leads to 'null' appearring - in textboxes instead of ''.*/ - if (value === null) { - value = ""; - } - ctrl.value = value; - } -}, -// Save control setting to cookie -saveSetting: function(name) { - var val, ctrl = $D('noVNC_' + name); - if (ctrl.type === 'checkbox') { - val = ctrl.checked; - } else if (typeof ctrl.options !== 'undefined') { - val = ctrl.options[ctrl.selectedIndex].value; - } else { - val = ctrl.value; - } - WebUtil.writeSetting(name, val); - //Util.Debug("Setting saved '" + name + "=" + val + "'"); - return val; -}, + // Logging selection dropdown + var llevels = ['error', 'warn', 'info', 'debug']; + for (i = 0; i < llevels.length; i += 1) { + UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); + } -// Initial page load read/initialization of settings -initSetting: function(name, defVal) { - var val; + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + WebUtil.init_logging(UI.getSetting('logging')); - // Check Query string followed by cookie - val = WebUtil.getQueryVar(name); - if (val === null) { - val = WebUtil.readSetting(name, defVal); - } - UI.updateSetting(name, val); - //Util.Debug("Setting '" + name + "' initialized to '" + val + "'"); - return val; -}, + UI.initSetting('stylesheet', 'default'); + WebUtil.selectStylesheet(null); + // call twice to get around webkit bug + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); -// Force a setting to be a certain value -forceSetting: function(name, val) { - UI.updateSetting(name, val); - return val; -}, + // if port == 80 (or 443) then it won't be present and should be + // set manually + var port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0,5) == 'https') { + port = 443; + } + else if (window.location.protocol.substring(0,4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('password', ''); + UI.initSetting('encrypt', (window.location.protocol === "https:")); + UI.initSetting('true_color', true); + UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpVisualState, + 'onClipboard': UI.clipReceive, + 'onDesktopName': UI.updateDocumentTitle}); + + var autoconnect = WebUtil.getQueryVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + } + + UI.updateVisualState(); + + // Show mouse selector buttons on touch screen devices + if (UI.isTouchDevice) { + // Show mobile buttons + $D('noVNC_mobile_buttons').style.display = "inline"; + UI.setMouseButton(); + // Remove the address bar + setTimeout(function() { window.scrollTo(0, 1); }, 100); + UI.forceSetting('clip', true); + $D('noVNC_clip').disabled = true; + } else { + UI.initSetting('clip', false); + } + + //iOS Safari does not support CSS position:fixed. + //This detects iOS devices and enables javascript workaround. + if ((navigator.userAgent.match(/iPhone/i)) || + (navigator.userAgent.match(/iPod/i)) || + (navigator.userAgent.match(/iPad/i))) { + //UI.setOnscroll(); + //UI.setResize(); + } + UI.setBarPosition(); + + $D('noVNC_host').focus(); + + UI.setViewClip(); + Util.addEvent(window, 'resize', UI.setViewClip); + + Util.addEvent(window, 'beforeunload', function () { + if (UI.rfb_state === 'normal') { + return "You are currently connected."; + } + } ); + + // Show description by default when hosted at for kanaka.github.com + if (location.host === "kanaka.github.io") { + // Open the description dialog + $D('noVNC_description').style.display = "block"; + } else { + // Show the connect panel on first load unless autoconnecting + if (autoconnect === UI.connSettingsOpen) { + UI.toggleConnectPanel(); + } + } + + // Add mouse event click/focus/blur event handlers to the UI + UI.addMouseHandlers(); + + if (typeof callback === "function") { + callback(UI.rfb); + } + }, + + addMouseHandlers: function() { + // Setup interface handlers that can't be inline + $D("noVNC_view_drag_button").onclick = UI.setViewDrag; + $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; + $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; + $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; + $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; + $D("showKeyboard").onclick = UI.showKeyboard; + + $D("keyboardinput").oninput = UI.keyInput; + $D("keyboardinput").onblur = UI.keyInputBlur; + + $D("showExtraKeysButton").onclick = UI.showExtraKeys; + $D("toggleCtrlButton").onclick = UI.toggleCtrl; + $D("toggleAltButton").onclick = UI.toggleAlt; + $D("sendTabButton").onclick = UI.sendTab; + $D("sendEscButton").onclick = UI.sendEsc; + + $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; + $D("xvpShutdownButton").onclick = UI.xvpShutdown; + $D("xvpRebootButton").onclick = UI.xvpReboot; + $D("xvpResetButton").onclick = UI.xvpReset; + $D("noVNC_status").onclick = UI.togglePopupStatusPanel; + $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel; + $D("xvpButton").onclick = UI.toggleXvpPanel; + $D("clipboardButton").onclick = UI.toggleClipboardPanel; + $D("settingsButton").onclick = UI.toggleSettingsPanel; + $D("connectButton").onclick = UI.toggleConnectPanel; + $D("disconnectButton").onclick = UI.disconnect; + $D("descriptionButton").onclick = UI.toggleConnectPanel; + + $D("noVNC_clipboard_text").onfocus = UI.displayBlur; + $D("noVNC_clipboard_text").onblur = UI.displayFocus; + $D("noVNC_clipboard_text").onchange = UI.clipSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; + + $D("noVNC_settings_menu").onmouseover = UI.displayBlur; + $D("noVNC_settings_menu").onmouseover = UI.displayFocus; + $D("noVNC_apply").onclick = UI.settingsApply; + + $D("noVNC_connect_button").onclick = UI.connect; + }, + + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = $D('noVNC_' + name); + var val = WebUtil.readSetting(name); + if (val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting: function(name, value) { + + // Save the cookie for this session + if (typeof value !== 'undefined') { + WebUtil.writeSetting(name, value); + } + + // Update the settings control + value = UI.getSetting(name); + + var ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + + } else if (typeof ctrl.options !== 'undefined') { + for (var i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; + } + } + } else { + /*Weird IE9 error leads to 'null' appearring + in textboxes instead of ''.*/ + if (value === null) { + value = ""; + } + ctrl.value = value; + } + }, + + // Save control setting to cookie + saveSetting: function(name) { + var val, ctrl = $D('noVNC_' + name); + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + //Util.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, + + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getQueryVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + UI.updateSetting(name, val); + return val; + }, + + // Force a setting to be a certain value + forceSetting: function(name, val) { + UI.updateSetting(name, val); + return val; + }, -// Show the popup status panel -togglePopupStatusPanel: function() { - var psp = $D('noVNC_popup_status_panel'); - if (UI.popupStatusOpen === true) { - psp.style.display = "none"; - UI.popupStatusOpen = false; - } else { - psp.innerHTML = $D('noVNC_status').innerHTML; - psp.style.display = "block"; - psp.style.left = window.innerWidth/2 - - parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; - UI.popupStatusOpen = true; - } -}, + // Show the popup status panel + togglePopupStatusPanel: function() { + var psp = $D('noVNC_popup_status_panel'); + if (UI.popupStatusOpen === true) { + psp.style.display = "none"; + UI.popupStatusOpen = false; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; + UI.popupStatusOpen = true; + } + }, -// Show the XVP panel -toggleXvpPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } - // Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } - // Close clipboard panel if open - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - // Toggle XVP panel - if (UI.xvpOpen === true) { - $D('noVNC_xvp').style.display = "none"; - $D('xvpButton').className = "noVNC_status_button"; - UI.xvpOpen = false; - } else { - $D('noVNC_xvp').style.display = "block"; - $D('xvpButton').className = "noVNC_status_button_selected"; - UI.xvpOpen = true; - } -}, + // Show the XVP panel + toggleXvpPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Toggle XVP panel + if (UI.xvpOpen === true) { + $D('noVNC_xvp').style.display = "none"; + $D('xvpButton').className = "noVNC_status_button"; + UI.xvpOpen = false; + } else { + $D('noVNC_xvp').style.display = "block"; + $D('xvpButton').className = "noVNC_status_button_selected"; + UI.xvpOpen = true; + } + }, -// Show the clipboard panel -toggleClipboardPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } - // Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } - // Toggle Clipboard Panel - if (UI.clipboardOpen === true) { - $D('noVNC_clipboard').style.display = "none"; - $D('clipboardButton').className = "noVNC_status_button"; - UI.clipboardOpen = false; - } else { - $D('noVNC_clipboard').style.display = "block"; - $D('clipboardButton').className = "noVNC_status_button_selected"; - UI.clipboardOpen = true; - } -}, + // Show the clipboard panel + toggleClipboardPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + // Toggle Clipboard Panel + if (UI.clipboardOpen === true) { + $D('noVNC_clipboard').style.display = "none"; + $D('clipboardButton').className = "noVNC_status_button"; + UI.clipboardOpen = false; + } else { + $D('noVNC_clipboard').style.display = "block"; + $D('clipboardButton').className = "noVNC_status_button_selected"; + UI.clipboardOpen = true; + } + }, -// Show the connection settings panel/menu -toggleConnectPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close connection settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - $D('connectButton').className = "noVNC_status_button"; - } - // Close clipboard panel if open - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } + // Show the connection settings panel/menu + toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + $D('connectButton').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } - // Toggle Connection Panel - if (UI.connSettingsOpen === true) { - $D('noVNC_controls').style.display = "none"; - $D('connectButton').className = "noVNC_status_button"; - UI.connSettingsOpen = false; - UI.saveSetting('host'); - UI.saveSetting('port'); - //UI.saveSetting('password'); - } else { - $D('noVNC_controls').style.display = "block"; - $D('connectButton').className = "noVNC_status_button_selected"; - UI.connSettingsOpen = true; - $D('noVNC_host').focus(); - } -}, + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('connectButton').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('connectButton').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_host').focus(); + } + }, -// Toggle the settings menu: -// On open, settings are refreshed from saved cookies. -// On close, settings are applied -toggleSettingsPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - if (UI.settingsOpen) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } else { - UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { - UI.updateSetting('cursor'); - } else { - UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; - } - UI.updateSetting('clip'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('stylesheet'); - UI.updateSetting('logging'); + // Toggle the settings menu: + // On open, settings are refreshed from saved cookies. + // On close, settings are applied + toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); - UI.openSettingsMenu(); - } -}, + UI.openSettingsMenu(); + } + }, -// Open menu -openSettingsMenu: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close clipboard panel if open - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - // Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } - $D('noVNC_settings').style.display = "block"; - $D('settingsButton').className = "noVNC_status_button_selected"; - UI.settingsOpen = true; -}, + // Open menu + openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close popup status panel if open + if (UI.popupStatusOpen === true) { + UI.togglePopupStatusPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('settingsButton').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; + }, -// Close menu (without applying settings) -closeSettingsMenu: function() { - $D('noVNC_settings').style.display = "none"; - $D('settingsButton').className = "noVNC_status_button"; - UI.settingsOpen = false; -}, + // Close menu (without applying settings) + closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('settingsButton').className = "noVNC_status_button"; + UI.settingsOpen = false; + }, -// Save/apply settings when 'Apply' button is pressed -settingsApply: function() { - //Util.Debug(">> settingsApply"); - UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { - UI.saveSetting('cursor'); - } - UI.saveSetting('clip'); - UI.saveSetting('shared'); - UI.saveSetting('view_only'); - UI.saveSetting('path'); - UI.saveSetting('repeaterID'); - UI.saveSetting('stylesheet'); - UI.saveSetting('logging'); + // Save/apply settings when 'Apply' button is pressed + settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (UI.rfb.get_display().get_cursor_uri()) { + UI.saveSetting('cursor'); + } + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); - // Settings with immediate (non-connected related) effect - WebUtil.selectStylesheet(UI.getSetting('stylesheet')); - WebUtil.init_logging(UI.getSetting('logging')); - UI.setViewClip(); - UI.setViewDrag(UI.rfb.get_viewportDrag()); - //Util.Debug("<< settingsApply"); -}, + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.setViewClip(); + UI.setViewDrag(UI.rfb.get_viewportDrag()); + //Util.Debug("<< settingsApply"); + }, -setPassword: function() { - UI.rfb.sendPassword($D('noVNC_password').value); - //Reset connect button. - $D('noVNC_connect_button').value = "Connect"; - $D('noVNC_connect_button').onclick = UI.Connect; - //Hide connection panel. - UI.toggleConnectPanel(); - return false; -}, + setPassword: function() { + UI.rfb.sendPassword($D('noVNC_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.Connect; + //Hide connection panel. + UI.toggleConnectPanel(); + return false; + }, -sendCtrlAltDel: function() { - UI.rfb.sendCtrlAltDel(); -}, + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, -xvpShutdown: function() { - UI.rfb.xvpShutdown(); -}, + xvpShutdown: function() { + UI.rfb.xvpShutdown(); + }, -xvpReboot: function() { - UI.rfb.xvpReboot(); -}, + xvpReboot: function() { + UI.rfb.xvpReboot(); + }, -xvpReset: function() { - UI.rfb.xvpReset(); -}, + xvpReset: function() { + UI.rfb.xvpReset(); + }, -setMouseButton: function(num) { - var b, blist = [0, 1,2,4], button; + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } - if (typeof num === 'undefined') { - // Disable mouse buttons - num = -1; - } - if (UI.rfb) { - UI.rfb.get_mouse().set_touchButton(num); - } + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, - for (b = 0; b < blist.length; b++) { - button = $D('noVNC_mouse_button' + blist[b]); - if (blist[b] === num) { - button.style.display = ""; - } else { - button.style.display = "none"; - /* - button.style.backgroundColor = "black"; - button.style.color = "lightgray"; - button.style.backgroundColor = ""; - button.style.color = ""; - */ - } - } -}, + updateState: function(rfb, state, oldstate, msg) { + UI.rfb_state = state; + var klass; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + /* falls through */ + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); -updateState: function(rfb, state, oldstate, msg) { - var s, sb, c, d, cad, vd, klass; - UI.rfb_state = state; - switch (state) { - case 'failed': - case 'fatal': - klass = "noVNC_status_error"; - break; - case 'normal': - klass = "noVNC_status_normal"; - break; - case 'disconnected': - $D('noVNC_logo').style.display = "block"; - // Fall through - case 'loaded': - klass = "noVNC_status_normal"; - break; - case 'password': + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + + if (typeof(msg) !== 'undefined') { + $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_status').innerHTML = msg; + } + + UI.updateVisualState(); + }, + + // Disable/enable controls depending on connection state + updateVisualState: function() { + var connected = UI.rfb_state === 'normal' ? true : false; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_encrypt').disabled = connected; + $D('noVNC_true_color').disabled = connected; + if (UI.rfb && UI.rfb.get_display() && + UI.rfb.get_display().get_cursor_uri()) { + $D('noVNC_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + $D('noVNC_shared').disabled = connected; + $D('noVNC_view_only').disabled = connected; + $D('noVNC_path').disabled = connected; + $D('noVNC_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('clipboardButton').style.display = "inline"; + $D('showKeyboard').style.display = "inline"; + $D('noVNC_extra_keys').style.display = ""; + $D('sendCtrlAltDelButton').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('clipboardButton').style.display = "none"; + $D('showKeyboard').style.display = "none"; + $D('noVNC_extra_keys').style.display = "none"; + $D('sendCtrlAltDelButton').style.display = "none"; + UI.updateXvpVisualState(0); + } + + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.setViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'loaded': + case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + break; + default: + $D('connectButton').style.display = "none"; + $D('disconnectButton').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); + }, + + // Disable/enable XVP button + updateXvpVisualState: function(ver) { + if (ver >= 1) { + $D('xvpButton').style.display = 'inline'; + } else { + $D('xvpButton').style.display = 'none'; + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + } + }, + + // Display the desktop name in the document title + updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; + }, + + clipReceive: function(rfb, text) { + Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipReceive"); + }, + + connect: function() { + UI.closeSettingsMenu(); UI.toggleConnectPanel(); - $D('noVNC_connect_button').value = "Send Password"; - $D('noVNC_connect_button').onclick = UI.setPassword; - $D('noVNC_password').focus(); + var host = $D('noVNC_host').value; + var port = $D('noVNC_port').value; + var password = $D('noVNC_password').value; + var path = $D('noVNC_path').value; + if ((!host) || (!port)) { + throw new Error("Must set host and port"); + } - klass = "noVNC_status_warn"; - break; - default: - klass = "noVNC_status_warn"; - break; - } + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - if (typeof(msg) !== 'undefined') { - $D('noVNC-control-bar').setAttribute("class", klass); - $D('noVNC_status').innerHTML = msg; - } + UI.rfb.connect(host, port, password, path); - UI.updateVisualState(); -}, + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + }, -// Disable/enable controls depending on connection state -updateVisualState: function() { - var connected = UI.rfb_state === 'normal' ? true : false; + disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); - //Util.Debug(">> updateVisualState"); - $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; - if (UI.rfb && UI.rfb.get_display() && - UI.rfb.get_display().get_cursor_uri()) { - $D('noVNC_cursor').disabled = connected; - } else { - UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; - } - $D('noVNC_shared').disabled = connected; - $D('noVNC_view_only').disabled = connected; - $D('noVNC_path').disabled = connected; - $D('noVNC_repeaterID').disabled = connected; + $D('noVNC_logo').style.display = "block"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + }, - if (connected) { - UI.setViewClip(); - UI.setMouseButton(1); - $D('clipboardButton').style.display = "inline"; - $D('showKeyboard').style.display = "inline"; - $D('noVNC_extra_keys').style.display = ""; - $D('sendCtrlAltDelButton').style.display = "inline"; - } else { - UI.setMouseButton(); - $D('clipboardButton').style.display = "none"; - $D('showKeyboard').style.display = "none"; - $D('noVNC_extra_keys').style.display = "none"; - $D('sendCtrlAltDelButton').style.display = "none"; - UI.updateXvpVisualState(0); - } - - // State change disables viewport dragging. - // It is enabled (toggled) by direct click on the button - UI.setViewDrag(false); + displayBlur: function() { + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + }, - switch (UI.rfb_state) { - case 'fatal': - case 'failed': - case 'loaded': - case 'disconnected': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; - break; - default: - $D('connectButton').style.display = "none"; - $D('disconnectButton').style.display = ""; - break; - } + displayFocus: function() { + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + }, - //Util.Debug("<< updateVisualState"); -}, + clipClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, -// Disable/enable XVP button -updateXvpVisualState: function(ver) { - if (ver >= 1) { - $D('xvpButton').style.display = 'inline'; - } else { - $D('xvpButton').style.display = 'none'; - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); + clipSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipSend"); + }, + + // Enable/disable and configure viewport clipping + setViewClip: function(clip) { + var display; + if (UI.rfb) { + display = UI.rfb.get_display(); + } else { + return; + } + + var cur_clip = display.get_viewport(); + + if (typeof(clip) !== 'boolean') { + // Use current setting + clip = UI.getSetting('clip'); + } + + if (clip && !cur_clip) { + // Turn clipping on + UI.updateSetting('clip', true); + } else if (!clip && cur_clip) { + // Turn clipping off + UI.updateSetting('clip', false); + display.set_viewport(false); + $D('noVNC_canvas').style.position = 'static'; + display.viewportChange(); + } + if (UI.getSetting('clip')) { + // If clipping, update clipping settings + $D('noVNC_canvas').style.position = 'absolute'; + var pos = Util.getPosition($D('noVNC_canvas')); + var new_w = window.innerWidth - pos.x; + var new_h = window.innerHeight - pos.y; + display.set_viewport(true); + display.viewportChange(0, 0, new_w, new_h); + } + }, + + // Toggle/set/unset the viewport drag/move button + setViewDrag: function(drag) { + var vmb = $D('noVNC_view_drag_button'); + if (!UI.rfb) { return; } + + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport()) { + vmb.style.display = "inline"; + } else { + vmb.style.display = "none"; + } + + if (typeof(drag) === "undefined" || + typeof(drag) === "object") { + // If not specified, then toggle + drag = !UI.rfb.get_viewportDrag(); + } + if (drag) { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + }, + + // On touch devices, show the OS keyboard + showKeyboard: function() { + var kbi = $D('keyboardinput'); + var skb = $D('showKeyboard'); + var l = kbi.value.length; + if(UI.keyboardVisible === false) { + kbi.focus(); + try { kbi.setSelectionRange(l, l); } // Move the caret to the end + catch (err) {} // setSelectionRange is undefined in Google Chrome + UI.keyboardVisible = true; + skb.className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === true) { + kbi.blur(); + skb.className = "noVNC_status_button"; + UI.keyboardVisible = false; + } + }, + + keepKeyboard: function() { + clearTimeout(UI.hideKeyboardTimeout); + if(UI.keyboardVisible === true) { + $D('keyboardinput').focus(); + $D('showKeyboard').className = "noVNC_status_button_selected"; + } else if(UI.keyboardVisible === false) { + $D('keyboardinput').blur(); + $D('showKeyboard').className = "noVNC_status_button"; + } + }, + + keyboardinputReset: function() { + var kbi = $D('keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); + UI.lastKeyboardinput = kbi.value; + }, + + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput: function(event) { + var newValue = event.target.value; + var oldValue = UI.lastKeyboardinput; + + var newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + var oldLen = oldValue.length; + + var backspaces; + var inputs = newLen - oldLen; + if (inputs < 0) { + backspaces = -inputs; + } else { + backspaces = 0; + } + + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + var i; + for (i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } + + // Send the key events + for (i = 0; i < backspaces; i++) { + UI.rfb.sendKey(XK_BackSpace); + } + for (i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(newValue.charCodeAt(i)); + } + + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(function() { UI.keepKeyboard(); }, 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, + + keyInputBlur: function() { + $D('showKeyboard').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); + }, + + showExtraKeys: function() { + UI.keepKeyboard(); + if(UI.extraKeysVisible === false) { + $D('toggleCtrlButton').style.display = "inline"; + $D('toggleAltButton').style.display = "inline"; + $D('sendTabButton').style.display = "inline"; + $D('sendEscButton').style.display = "inline"; + $D('showExtraKeysButton').className = "noVNC_status_button_selected"; + UI.extraKeysVisible = true; + } else if(UI.extraKeysVisible === true) { + $D('toggleCtrlButton').style.display = ""; + $D('toggleAltButton').style.display = ""; + $D('sendTabButton').style.display = ""; + $D('sendEscButton').style.display = ""; + $D('showExtraKeysButton').className = "noVNC_status_button"; + UI.extraKeysVisible = false; + } + }, + + toggleCtrl: function() { + UI.keepKeyboard(); + if(UI.ctrlOn === false) { + UI.rfb.sendKey(XK_Control_L, true); + $D('toggleCtrlButton').className = "noVNC_status_button_selected"; + UI.ctrlOn = true; + } else if(UI.ctrlOn === true) { + UI.rfb.sendKey(XK_Control_L, false); + $D('toggleCtrlButton').className = "noVNC_status_button"; + UI.ctrlOn = false; + } + }, + + toggleAlt: function() { + UI.keepKeyboard(); + if(UI.altOn === false) { + UI.rfb.sendKey(XK_Alt_L, true); + $D('toggleAltButton').className = "noVNC_status_button_selected"; + UI.altOn = true; + } else if(UI.altOn === true) { + UI.rfb.sendKey(XK_Alt_L, false); + $D('toggleAltButton').className = "noVNC_status_button"; + UI.altOn = false; + } + }, + + sendTab: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Tab); + }, + + sendEsc: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Escape); + }, + + setKeyboard: function() { + UI.keyboardVisible = false; + }, + + // iOS < Version 5 does not support position fixed. Javascript workaround: + setOnscroll: function() { + window.onscroll = function() { + UI.setBarPosition(); + }; + }, + + setResize: function () { + window.onResize = function() { + UI.setBarPosition(); + }; + }, + + //Helper to add options to dropdown. + addOption: function(selectbox, text, value) { + var optn = document.createElement("OPTION"); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, + + setBarPosition: function() { + $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + + var vncwidth = $D('noVNC_screen').style.offsetWidth; + $D('noVNC-control-bar').style.width = vncwidth + 'px'; } - } -}, - -// Display the desktop name in the document title -updateDocumentTitle: function(rfb, name) { - document.title = name + " - noVNC"; -}, - - -clipReceive: function(rfb, text) { - Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); - $D('noVNC_clipboard_text').value = text; - Util.Debug("<< UI.clipReceive"); -}, - - -connect: function() { - var host, port, password, path; - - UI.closeSettingsMenu(); - UI.toggleConnectPanel(); - - host = $D('noVNC_host').value; - port = $D('noVNC_port').value; - password = $D('noVNC_password').value; - path = $D('noVNC_path').value; - if ((!host) || (!port)) { - throw("Must set host and port"); - } - - UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); - UI.rfb.set_local_cursor(UI.getSetting('cursor')); - UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_view_only(UI.getSetting('view_only')); - UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - - UI.rfb.connect(host, port, password, path); - - //Close dialog. - setTimeout(UI.setBarPosition, 100); - $D('noVNC_logo').style.display = "none"; -}, - -disconnect: function() { - UI.closeSettingsMenu(); - UI.rfb.disconnect(); - - $D('noVNC_logo').style.display = "block"; - UI.connSettingsOpen = false; - UI.toggleConnectPanel(); -}, - -displayBlur: function() { - UI.rfb.get_keyboard().set_focused(false); - UI.rfb.get_mouse().set_focused(false); -}, - -displayFocus: function() { - UI.rfb.get_keyboard().set_focused(true); - UI.rfb.get_mouse().set_focused(true); -}, - -clipClear: function() { - $D('noVNC_clipboard_text').value = ""; - UI.rfb.clipboardPasteFrom(""); -}, - -clipSend: function() { - var text = $D('noVNC_clipboard_text').value; - Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Util.Debug("<< UI.clipSend"); -}, - - -// Enable/disable and configure viewport clipping -setViewClip: function(clip) { - var display, cur_clip, pos, new_w, new_h; - - if (UI.rfb) { - display = UI.rfb.get_display(); - } else { - return; - } - - cur_clip = display.get_viewport(); - - if (typeof(clip) !== 'boolean') { - // Use current setting - clip = UI.getSetting('clip'); - } - - if (clip && !cur_clip) { - // Turn clipping on - UI.updateSetting('clip', true); - } else if (!clip && cur_clip) { - // Turn clipping off - UI.updateSetting('clip', false); - display.set_viewport(false); - $D('noVNC_canvas').style.position = 'static'; - display.viewportChange(); - } - if (UI.getSetting('clip')) { - // If clipping, update clipping settings - $D('noVNC_canvas').style.position = 'absolute'; - pos = Util.getPosition($D('noVNC_canvas')); - new_w = window.innerWidth - pos.x; - new_h = window.innerHeight - pos.y; - display.set_viewport(true); - display.viewportChange(0, 0, new_w, new_h); - } -}, - -// Toggle/set/unset the viewport drag/move button -setViewDrag: function(drag) { - var vmb = $D('noVNC_view_drag_button'); - if (!UI.rfb) { return; } - - if (UI.rfb_state === 'normal' && - UI.rfb.get_display().get_viewport()) { - vmb.style.display = "inline"; - } else { - vmb.style.display = "none"; - } - - if (typeof(drag) === "undefined" || - typeof(drag) === "object") { - // If not specified, then toggle - drag = !UI.rfb.get_viewportDrag(); - } - if (drag) { - vmb.className = "noVNC_status_button_selected"; - UI.rfb.set_viewportDrag(true); - } else { - vmb.className = "noVNC_status_button"; - UI.rfb.set_viewportDrag(false); - } -}, - -// On touch devices, show the OS keyboard -showKeyboard: function() { - var kbi, skb, l; - kbi = $D('keyboardinput'); - skb = $D('showKeyboard'); - l = kbi.value.length; - if(UI.keyboardVisible === false) { - kbi.focus(); - try { kbi.setSelectionRange(l, l); } // Move the caret to the end - catch (err) {} // setSelectionRange is undefined in Google Chrome - UI.keyboardVisible = true; - skb.className = "noVNC_status_button_selected"; - } else if(UI.keyboardVisible === true) { - kbi.blur(); - skb.className = "noVNC_status_button"; - UI.keyboardVisible = false; - } -}, - -keepKeyboard: function() { - clearTimeout(UI.hideKeyboardTimeout); - if(UI.keyboardVisible === true) { - $D('keyboardinput').focus(); - $D('showKeyboard').className = "noVNC_status_button_selected"; - } else if(UI.keyboardVisible === false) { - $D('keyboardinput').blur(); - $D('showKeyboard').className = "noVNC_status_button"; - } -}, - -keyboardinputReset: function() { - var kbi = $D('keyboardinput'); - kbi.value = Array(UI.defaultKeyboardinputLen).join("_"); - UI.lastKeyboardinput = kbi.value; -}, - -// When normal keyboard events are left uncought, use the input events from -// the keyboardinput element instead and generate the corresponding key events. -// This code is required since some browsers on Android are inconsistent in -// sending keyCodes in the normal keyboard events when using on screen keyboards. -keyInput: function(event) { - var newValue, oldValue, newLen, oldLen; - newValue = event.target.value; - oldValue = UI.lastKeyboardinput; - - try { - // Try to check caret position since whitespace at the end - // will not be considered by value.length in some browsers - newLen = Math.max(event.target.selectionStart, newValue.length); - } catch (err) { - // selectionStart is undefined in Google Chrome - newLen = newValue.length; - } - oldLen = oldValue.length; - - var backspaces; - var inputs = newLen - oldLen; - if (inputs < 0) - backspaces = -inputs; - else - backspaces = 0; - - // Compare the old string with the new to account for - // text-corrections or other input that modify existing text - for (var i = 0; i < Math.min(oldLen, newLen); i++) { - if (newValue.charAt(i) != oldValue.charAt(i)) { - inputs = newLen - i; - backspaces = oldLen - i; - break; - } - } - - // Send the key events - for (var i = 0; i < backspaces; i++) - UI.rfb.sendKey(XK_BackSpace); - for (var i = newLen - inputs; i < newLen; i++) - UI.rfb.sendKey(newValue.charCodeAt(i)); - - // Control the text content length in the keyboardinput element - if (newLen > 2 * UI.defaultKeyboardinputLen) { - UI.keyboardinputReset(); - } else if (newLen < 1) { - // There always have to be some text in the keyboardinput - // element with which backspace can interact. - UI.keyboardinputReset(); - // This sometimes causes the keyboard to disappear for a second - // but it is required for the android keyboard to recognize that - // text has been added to the field - event.target.blur(); - // This has to be ran outside of the input handler in order to work - setTimeout(function() { UI.keepKeyboard(); }, 0); - - } else { - UI.lastKeyboardinput = newValue; - } -}, - -keyInputBlur: function() { - $D('showKeyboard').className = "noVNC_status_button"; - //Weird bug in iOS if you change keyboardVisible - //here it does not actually occur so next time - //you click keyboard icon it doesnt work. - UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); -}, - -showExtraKeys: function() { - UI.keepKeyboard(); - if(UI.extraKeysVisible === false) { - $D('toggleCtrlButton').style.display = "inline"; - $D('toggleAltButton').style.display = "inline"; - $D('sendTabButton').style.display = "inline"; - $D('sendEscButton').style.display = "inline"; - $D('showExtraKeysButton').className = "noVNC_status_button_selected"; - UI.extraKeysVisible = true; - } else if(UI.extraKeysVisible === true) { - $D('toggleCtrlButton').style.display = ""; - $D('toggleAltButton').style.display = ""; - $D('sendTabButton').style.display = ""; - $D('sendEscButton').style.display = ""; - $D('showExtraKeysButton').className = "noVNC_status_button"; - UI.extraKeysVisible = false; - } -}, - -toggleCtrl: function() { - UI.keepKeyboard(); - if(UI.ctrlOn === false) { - UI.rfb.sendKey(XK_Control_L, true); - $D('toggleCtrlButton').className = "noVNC_status_button_selected"; - UI.ctrlOn = true; - } else if(UI.ctrlOn === true) { - UI.rfb.sendKey(XK_Control_L, false); - $D('toggleCtrlButton').className = "noVNC_status_button"; - UI.ctrlOn = false; - } -}, - -toggleAlt: function() { - UI.keepKeyboard(); - if(UI.altOn === false) { - UI.rfb.sendKey(XK_Alt_L, true); - $D('toggleAltButton').className = "noVNC_status_button_selected"; - UI.altOn = true; - } else if(UI.altOn === true) { - UI.rfb.sendKey(XK_Alt_L, false); - $D('toggleAltButton').className = "noVNC_status_button"; - UI.altOn = false; - } -}, - -sendTab: function() { - UI.keepKeyboard(); - UI.rfb.sendKey(XK_Tab); -}, - -sendEsc: function() { - UI.keepKeyboard(); - UI.rfb.sendKey(XK_Escape); -}, - -setKeyboard: function() { - UI.keyboardVisible = false; -}, - -// iOS < Version 5 does not support position fixed. Javascript workaround: -setOnscroll: function() { - window.onscroll = function() { - UI.setBarPosition(); }; -}, - -setResize: function () { - window.onResize = function() { - UI.setBarPosition(); - }; -}, - -//Helper to add options to dropdown. -addOption: function(selectbox,text,value ) -{ - var optn = document.createElement("OPTION"); - optn.text = text; - optn.value = value; - selectbox.options.add(optn); -}, - -setBarPosition: function() { - $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; - $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; - - var vncwidth = $D('noVNC_screen').style.offsetWidth; - $D('noVNC-control-bar').style.width = vncwidth + 'px'; -} - -}; - - - - +})(); From e6af0f60b061933dee1a8637aeb7bba7dd65b133 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 23 Jun 2014 19:39:50 -0400 Subject: [PATCH 014/527] Add support for Travis CI and SauceLabs Testing This adds support for Travis CI and SauceLabs testing. Testing on SauceLabs in done via the Karma test runner. Note that encrypted Sauce username and access key values need to be inserted into .travis.yml as global environment variables. Additionally, the local test runner (which is still useful for debugging tests and code) was updated to reflect that the 'node_modules' folder now gets placed in the root directory. --- .travis.yml | 13 +++ README.md | 1 + karma.conf.js | 191 +++++++++++++++++++++++++++++++ package.json | 50 ++++++++ tests/run_from_console.casper.js | 2 +- tests/run_from_console.js | 12 +- 6 files changed, 262 insertions(+), 7 deletions(-) create mode 100644 .travis.yml create mode 100644 karma.conf.js create mode 100644 package.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..6c594a85 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: +- '0.11' +env: + matrix: + - TEST_BROWSER_NAME=PhantomJS + - TEST_BROWSER_NAME=chrome TEST_BROWSER_OS='Windows 7,Linux' + - TEST_BROWSER_NAME=firefox TEST_BROWSER_OS='Windows 7,Linux' TEST_BROWSER_VERSION='30,26' + - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 7' TEST_BROWSER_VERSION=10 + - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 8.1' TEST_BROWSER_VERSION=11 + - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.8' TEST_BROWSER_VERSION=6 + - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.9' TEST_BROWSER_VERSION=7 +before_script: npm install -g karma-cli diff --git a/README.md b/README.md index d547267d..c0aedd8a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ## noVNC: HTML5 VNC Client +[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=refactor%2Fcleanup)](https://travis-ci.org/kanaka/noVNC) ### Description diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 00000000..fca5970d --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,191 @@ +// Karma configuration + +module.exports = function(config) { + /*var customLaunchers = { + sl_chrome_win7: { + base: 'SauceLabs', + browserName: 'chrome', + platform: 'Windows 7' + }, + + sl_firefox30_linux: { + base: 'SauceLabs', + browserName: 'firefox', + version: '30', + platform: 'Linux' + }, + + sl_firefox26_linux: { + base: 'SauceLabs', + browserName: 'firefox', + version: 26, + platform: 'Linux' + }, + + sl_windows7_ie10: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 7', + version: '10' + }, + + sl_windows81_ie11: { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8.1', + version: '11' + }, + + sl_osxmavericks_safari7: { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.9', + version: '7' + }, + + sl_osxmtnlion_safari6: { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.8', + version: '6' + } + };*/ + + var customLaunchers = {}; + var browsers = []; + var useSauce = false; + + if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) { + useSauce = true; + } + + if (useSauce && process.env.TEST_BROWSER_NAME && process.env.TEST_BROWSER_NAME != 'PhantomJS') { + var names = process.env.TEST_BROWSER_NAME.split(','); + var platforms = process.env.TEST_BROWSER_OS.split(','); + var versions = []; + if (process.env.TEST_BROWSER_VERSION) { + versions = process.env.TEST_BROWSER_VERSION.split(','); + } else { + versions = [null]; + } + + for (var i = 0; i < names.length; i++) { + for (var j = 0; j < platforms.length; j++) { + for (var k = 0; k < versions.length; k++) { + var launcher_name = 'sl_' + platforms[j].replace(/[^a-zA-Z0-9]/g, '') + '_' + names[i]; + if (versions[k]) { + launcher_name += '_' + versions[k]; + } + + customLaunchers[launcher_name] = { + base: 'SauceLabs', + browserName: names[i], + platform: platforms[j], + }; + + if (versions[i]) { + customLaunchers[launcher_name].version = versions[k]; + } + } + } + } + + browsers = Object.keys(customLaunchers); + } else { + useSauce = false; + browsers = ['PhantomJS']; + } + + var my_conf = { + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha', 'sinon', 'chai', 'sinon-chai'], + + + // list of files / patterns to load in the browser (loaded in order) + files: [ + 'tests/fake.*.js', + 'include/util.js', // load first to avoid issues, since methods are called immediately + //'../include/*.js', + 'include/base64.js', + 'include/keysym.js', + 'include/keysymdef.js', + 'include/keyboard.js', + 'include/input.js', + 'include/websock.js', + 'include/rfb.js', + 'include/jsunzip.js', + 'include/des.js', + 'include/display.js', + 'tests/test.*.js' + ], + + client: { + mocha: { + 'ui': 'bdd' + } + }, + + // list of files to exclude + exclude: [ + '../include/playback.js', + '../include/ui.js' + ], + + customLaunchers: customLaunchers, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: browsers, + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['mocha', 'saucelabs'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Increase timeout in case connection is slow/we run more browsers than possible + // (we currently get 3 for free, and we try to run 7, so it can take a while) + captureTimeout: 240000 + }; + + if (useSauce) { + my_conf.sauceLabs = { + testName: 'noVNC Tests (all)', + startConnect: true, + }; + } + + config.set(my_conf); +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..3f1b29bc --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "noVNC", + "version": "0.5.0", + "description": "An HTML5 VNC client", + "main": "karma.conf.js", + "directories": { + "doc": "docs", + "test": "tests" + }, + "scripts": { + "test": "karma start karma.conf.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/kanaka/noVNC.git" + }, + "author": "Joel Martin (https://github.com/kanaka)", + "contributors": [ + "Solly Ross (https://github.com/directxman12)", + "Peter Åstrand (https://github.com/astrand)", + "Samuel Mannehed (https://github.com/samhed)" + ], + "license": "MPL 2.0", + "bugs": { + "url": "https://github.com/kanaka/noVNC/issues" + }, + "homepage": "https://github.com/kanaka/noVNC", + "devDependencies": { + "ansi": "^0.3.0", + "casperjs": "^1.1.0-beta3", + "chai": "^1.9.1", + "commander": "^2.2.0", + "karma": "^0.12.16", + "karma-chai": "^0.1.0", + "karma-mocha": "^0.1.4", + "karma-mocha-reporter": "^0.2.5", + "karma-phantomjs-launcher": "^0.1.4", + "karma-sauce-launcher": "^0.2.8", + "karma-sinon": "^1.0.3", + "karma-sinon-chai": "^0.1.6", + "mocha": "^1.20.1", + "open": "0.0.5", + "phantom": "^0.6.3", + "phantomjs": "^1.9.7-9", + "sinon": "^1.10.2", + "sinon-chai": "^2.5.0", + "spooky": "^0.2.4", + "temp": "^0.8.0" + } +} diff --git a/tests/run_from_console.casper.js b/tests/run_from_console.casper.js index 7cb4b7c5..57ed2be2 100644 --- a/tests/run_from_console.casper.js +++ b/tests/run_from_console.casper.js @@ -2,7 +2,7 @@ var Spooky = require('spooky'); var path = require('path'); var phantom_path = require('phantomjs').path; -var casper_path = path.resolve(__dirname, 'node_modules/casperjs/bin/casperjs'); +var casper_path = path.resolve(__dirname, '../node_modules/casperjs/bin/casperjs'); process.env.PHANTOMJS_EXECUTABLE = phantom_path; var casper_opts = { child: { diff --git a/tests/run_from_console.js b/tests/run_from_console.js index 0d4cc8f5..bfdd1b69 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -67,16 +67,16 @@ if (program.autoInject) { temp.track(); var template = { - header: "\n\n\n\n\n
", + header: "\n\n\n\n\n
", script_tag: function(p) { return ""; }, footer: "\n\n" }; - template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/chai/chai.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/mocha/mocha.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon/pkg/sinon.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, 'node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/chai/chai.js')); + template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/mocha/mocha.js')); + template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon/pkg/sinon.js')); + template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n"; From 7caa9c20c250eae31eb9761dfe845911429154d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Wed, 17 Sep 2014 16:31:12 +0900 Subject: [PATCH 015/527] Prevent noVNC loading error when invalid property is set on object Util.set_defaults should accommodate with missing properties to prevent 'Uncaught TypeError: Cannot read property 'call' of undefined' error. --- include/util.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/util.js b/include/util.js index c145d5a6..a6045dac 100644 --- a/include/util.js +++ b/include/util.js @@ -341,6 +341,10 @@ Util.set_defaults = function (obj, conf, defaults) { for (i = 0; i < keys.length; i++) { var setter = obj['_raw_set_' + keys[i]]; + if (!setter) { + Util.Warn('Invalid property ' + keys[i]); + continue; + } if (conf[keys[i]]) { setter.call(obj, conf[keys[i]]); From 77bd04f8338b5ccaf78e2f8e3df0400ad9908ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Wed, 17 Sep 2014 16:44:21 +0900 Subject: [PATCH 016/527] Fix invalid updateState property of RFB It was changed to `onUpdateState` in b1dee9478815b22bf5fee3ee9e44321d4bb46c91. --- tests/vnc_perf.html | 2 +- tests/vnc_playback.html | 2 +- vnc_auto.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/vnc_perf.html b/tests/vnc_perf.html index 18aba355..c439e955 100644 --- a/tests/vnc_perf.html +++ b/tests/vnc_perf.html @@ -202,7 +202,7 @@ dbgmsg(" " + enc + ": " + VNC_frame_data_multi[enc].length); } rfb = new RFB({'target': $D('VNC_canvas'), - 'updateState': updateState}); + 'onUpdateState': updateState}); rfb.testMode(send_array, VNC_frame_encoding); } diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index 9d7f31f5..b5faf93c 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -131,7 +131,7 @@ if (fname) { message("VNC_frame_data.length: " + VNC_frame_data.length); rfb = new RFB({'target': $D('VNC_canvas'), - 'updateState': updateState}); + 'onUpdateState': updateState}); } } diff --git a/vnc_auto.html b/vnc_auto.html index 53b8220c..ff376fec 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -198,7 +198,7 @@ 'local_cursor': WebUtil.getQueryVar('cursor', true), 'shared': WebUtil.getQueryVar('shared', true), 'view_only': WebUtil.getQueryVar('view_only', false), - 'updateState': updateState, + 'onUpdateState': updateState, 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired}); rfb.connect(host, port, password, path); From f8f95d6023c21c050d37c4e0b1313b70655209c0 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 11:43:58 -0400 Subject: [PATCH 017/527] Added Sauce Auth Details for Travis CI This will enable Travis CI to run tests on Sauce Labs (thanks to OpenSauce, their free program for Open Source Software) --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6c594a85..dfcec2fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,7 @@ env: - TEST_BROWSER_NAME='internet explorer' TEST_BROWSER_OS='Windows 8.1' TEST_BROWSER_VERSION=11 - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.8' TEST_BROWSER_VERSION=6 - TEST_BROWSER_NAME=safari TEST_BROWSER_OS='OS X 10.9' TEST_BROWSER_VERSION=7 + global: + - secure: QE5GqGd2hrpQsIgd8dlv3oRUUHqZayomzzQjNXOB81VQi241uz/ru+3GtBZLB5WLZCq/Gj89vbLnR0LN4ixlmPaWv3/WJQGyDGuRD/vMnccVl+rBUP/Hh2zdYwiISIGcrywNAE+KLus/lyt/ahVgzbaRaDSzrM1HaZFT/rndGck= + - secure: g75sdctEwj0hoLW0Y08Tdv8s5scNzplB6a9EtaJ2vJD9S/bK+AsPqbWesGv1UlrFPCWdbV7Vg61vkmoUjcmb5xhqFIjcM9TlYJoKWeOTsOmnQoSIkIq6gMF1k02+LmKInbPgIzrp3m3jluS1qaOs/EzFpDnJp9hWBiAfXa12Jxk= before_script: npm install -g karma-cli From 2c9623b5a73e5ab9b54d462596d1fef0063c9a73 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 12:16:06 -0400 Subject: [PATCH 018/527] Fixed assertion collision issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When run via karma, all the tests are loaded into the same page. This was causing a collision in the 'displayed' assertion dealing with using viewportLoc. The assertions are now in their own file, pulled in by tests that need them. Additionally, several tests which only set fb_width and fb_height were correct to set viewportLoc as well. Closes #392 Also-Authored-By: Martin André (github: mandre) --- karma.conf.js | 1 + tests/assertions.js | 24 ++++++++++++++++++++++++ tests/test.display.js | 15 +-------------- tests/test.rfb.js | 31 +++++-------------------------- 4 files changed, 31 insertions(+), 40 deletions(-) create mode 100644 tests/assertions.js diff --git a/karma.conf.js b/karma.conf.js index fca5970d..40e21c81 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -109,6 +109,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser (loaded in order) files: [ 'tests/fake.*.js', + 'tests/assertions.js', 'include/util.js', // load first to avoid issues, since methods are called immediately //'../include/*.js', 'include/base64.js', diff --git a/tests/assertions.js b/tests/assertions.js new file mode 100644 index 00000000..92b11d1f --- /dev/null +++ b/tests/assertions.js @@ -0,0 +1,24 @@ +// some useful assertions for noVNC +chai.use(function (_chai, utils) { + _chai.Assertion.addMethod('displayed', function (target_data) { + var obj = this._obj; + var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + var data = new Uint8Array(data_cl); + this.assert(utils.eql(data, target_data), + "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", + "expected #{this} not to have displayed the image #{act}", + target_data, + data); + }); + + _chai.Assertion.addMethod('sent', function (target_data) { + var obj = this._obj; + var data = obj._websocket._get_sent_data(); + this.assert(utils.eql(data, target_data), + "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", + "expected #{this} not to have sent the data #{act}", + target_data, + data); + }); +}); diff --git a/tests/test.display.js b/tests/test.display.js index c4535a03..832970d5 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -1,21 +1,8 @@ // requires local modules: util, base64, display +// requires test modules: assertions /* jshint expr: true */ var expect = chai.expect; -chai.use(function (_chai, utils) { - _chai.Assertion.addMethod('displayed', function (target_data) { - var obj = this._obj; - var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; - // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that - var data = new Uint8Array(data_cl); - this.assert(utils.eql(data, target_data), - "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", - "expected #{this} not to have displayed the image #{act}", - target_data, - data); - }); -}); - describe('Display/Canvas Helper', function () { var checked_data = [ 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 595548e1..56ec2562 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1,5 +1,5 @@ // requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, jsunzip, des, display -// requires test modules: fake.websocket +// requires test modules: fake.websocket, assertions /* jshint expr: true */ var assert = chai.assert; var expect = chai.expect; @@ -13,31 +13,6 @@ function make_rfb (extra_opts) { return new RFB(extra_opts); } -// some useful assertions for noVNC -chai.use(function (_chai, utils) { - _chai.Assertion.addMethod('displayed', function (target_data) { - var obj = this._obj; - var data_cl = obj._drawCtx.getImageData(0, 0, obj._fb_width, obj._fb_height).data; - // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that - var data = new Uint8Array(data_cl); - this.assert(utils.eql(data, target_data), - "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", - "expected #{this} not to have displayed the image #{act}", - target_data, - data); - }); - - _chai.Assertion.addMethod('sent', function (target_data) { - var obj = this._obj; - var data = obj._websocket._get_sent_data(); - this.assert(utils.eql(data, target_data), - "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", - "expected #{this} not to have sent the data #{act}", - target_data, - data); - }); -}); - describe('Remote Frame Buffer Protocol Client', function() { "use strict"; before(FakeWebSocket.replace); @@ -1211,6 +1186,8 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_height = 4; client._display._fb_width = 4; client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; client._fb_Bpp = 4; }); @@ -1286,6 +1263,8 @@ describe('Remote Frame Buffer Protocol Client', function() { client._fb_height = 4; client._display._fb_width = 4; client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; client._fb_Bpp = 4; }); From f0e4548b16704384f8876c7252babef8b5554501 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 12:49:32 -0400 Subject: [PATCH 019/527] Fix Travis Sauce Tunnel Issues Travis is failing because Sauce can't find the appropriate tunnelid. This should fix that but setting the tunnel id to be the Travis job number. --- karma.conf.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/karma.conf.js b/karma.conf.js index 40e21c81..94b69868 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -182,9 +182,11 @@ module.exports = function(config) { }; if (useSauce) { + my_conf.captureTimeout = 0; // use SL timeout my_conf.sauceLabs = { testName: 'noVNC Tests (all)', startConnect: true, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER }; } From d02a99f0c85557115caa99ff729bc1e2209a0a4a Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 14:17:15 -0400 Subject: [PATCH 020/527] Fixed Typo Causing MouseUp to not Register There was a typo in one of the instances of the _buttonMask field (it was written as _buttonMaks), causing MouseUp to never be sent. This has been rectified, and the unit tests for the mouse handler have been changed to check for explicitly sending mouseup and mousedown. Fixes #393 --- include/rfb.js | 2 +- tests/test.rfb.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index 9e96f715..ffa66020 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -550,7 +550,7 @@ var RFB; if (down) { this._mouse_buttonMask |= bmask; } else { - this._mouse_buttonMaks ^= bmask; + this._mouse_buttonMask ^= bmask; } if (this._viewportDrag) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 56ec2562..64625870 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1512,6 +1512,20 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._sock.send).to.have.been.calledWith(pointer_msg); }); + it('should send a mask of 1 on mousedown', function () { + client._mouse._onMouseButton(10, 12, 1, 0x001); + expect(client._sock.send).to.have.been.calledOnce; + var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001); + expect(client._sock.send).to.have.been.calledWith(pointer_msg); + }); + + it('should send a mask of 0 on mouseup', function () { + client._mouse._onMouseButton(10, 12, 0, 0x001); + expect(client._sock.send).to.have.been.calledOnce; + var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x000); + expect(client._sock.send).to.have.been.calledWith(pointer_msg); + }); + it('should send a pointer event on mouse movement', function () { client._mouse._onMouseMove(10, 12); expect(client._sock.send).to.have.been.calledOnce; From cfc02e5e2bccfe2c1a80e4097372b8423ce9d87a Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 14:48:00 -0400 Subject: [PATCH 021/527] Fixed presence detection bug in utils.set_defaults Previously, Utils.set_defaults was using `if(conf[keys[i]])` to check for the presence of a configuration key. This would fail if `conf[keys[i]]` happened to be false. Instead, we now use `if(keys[i] in conf)`, which simply checks for the presence of the key in the conf object. --- include/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/util.js b/include/util.js index a6045dac..9e5f98c9 100644 --- a/include/util.js +++ b/include/util.js @@ -346,7 +346,7 @@ Util.set_defaults = function (obj, conf, defaults) { continue; } - if (conf[keys[i]]) { + if (keys[i] in conf) { setter.call(obj, conf[keys[i]]); } else { setter.call(obj, defaults[keys[i]]); From 53762c31fe21a7ac39f124cd3dfb0d3f894ebe7c Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 14:51:15 -0400 Subject: [PATCH 022/527] Fixed Cursor URI Support Detection There was a bug in cursor URI support detection due to the way set_defaults now works -- the code was checking for `null`, whereas when not set, options default to `undefined` unless otherwise specified. The code now checks for either `null` or `undefined`. Tests have been added to ensure that this works properly. --- include/display.js | 4 ++-- tests/test.display.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/include/display.js b/include/display.js index e8ac63d8..d2876a76 100644 --- a/include/display.js +++ b/include/display.js @@ -89,12 +89,12 @@ var Display; var curSave = this._target.style.cursor; Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8); if (this._target.style.cursor) { - if (this._cursor_uri === null) { + if (this._cursor_uri === null || this._cursor_uri === undefined) { this._cursor_uri = true; } Util.Info("Data URI scheme cursor supported"); } else { - if (this._cursor_uri === null) { + if (this._cursor_uri === null || this._cursor_uri === undefined) { this._cursor_uri = false; } Util.Warn("Data URI scheme cursor not supported"); diff --git a/tests/test.display.js b/tests/test.display.js index 832970d5..25adfbea 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -26,6 +26,40 @@ describe('Display/Canvas Helper', function () { return canvas; } + describe('checking for cursor uri support', function () { + beforeEach(function () { + this._old_change_cursor = Display.changeCursor; + }); + + it('should disable cursor URIs if there is no support', function () { + Display.changeCursor = function(target) { + target.style.cursor = undefined; + }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); + expect(display._cursor_uri).to.be.false; + }); + + it('should enable cursor URIs if there is support', function () { + Display.changeCursor = function(target) { + target.style.cursor = 'pointer'; + }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); + expect(display._cursor_uri).to.be.true; + }); + + it('respect the cursor_uri option if there is support', function () { + Display.changeCursor = function(target) { + target.style.cursor = 'pointer'; + }; + var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false, cursor_uri: false }); + expect(display._cursor_uri).to.be.false; + }); + + afterEach(function () { + Display.changeCursor = this._old_change_cursor; + }); + }); + describe('viewport handling', function () { var display; beforeEach(function () { From 3b4fd003c2ba6e0294282ea203e6ead6e9275317 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 19 Sep 2014 15:14:34 -0400 Subject: [PATCH 023/527] Fixed broken mouse test in test.rfb.js Because we use the XOR (`^`) operator, the button mask must be set before a MouseUp event happens, otherwise we'll send a pointer event like it was a MouseDown event. The button mask was not set in one of the tests, so the test was failing. --- tests/test.rfb.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 64625870..fb32af0f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1520,6 +1520,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should send a mask of 0 on mouseup', function () { + client._mouse_buttonMask = 0x001; client._mouse._onMouseButton(10, 12, 0, 0x001); expect(client._sock.send).to.have.been.calledOnce; var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x000); From 0b0b0433b55e97107574a82ff5b0f755d17ec4dd Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 22 Sep 2014 21:37:04 -0400 Subject: [PATCH 024/527] Fix Race Condition in Display#clear on IE10 There was a race condition in Display#clear on IE10 because we resize and then clear that causes the canvas to not actually end up cleared. Clearing the current viewport first solves the issue. It doesn't appear to affect other platforms, so it's inside a engine check (`Util.Engine.trident === 6`). Once we stop supporting IE10, we should just remove this, because it's not the best to have Engine-specific code. --- include/display.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/display.js b/include/display.js index d2876a76..8763fa4a 100644 --- a/include/display.js +++ b/include/display.js @@ -312,6 +312,12 @@ var Display; this.resize(this._logo.width, this._logo.height); this.blitStringImage(this._logo.data, 0, 0); } else { + if (Util.Engine.trident === 6) { + // NB(directxman12): there's a bug in IE10 where we can fail to actually + // clear the canvas here because of the resize. + // Clearing the current viewport first fixes the issue + this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); + } this.resize(640, 20); this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); } From b11bb5c38595d1cb578c500a74c093ef8d8aa865 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 22 Sep 2014 21:42:32 -0400 Subject: [PATCH 025/527] Add support for Relative Paths in the Test Runner This patch adds support for using relative paths with the '-r' or '--relative' methods. This can be useful if you want to output HTML (with the '--output-html' option) and use it in a webpage. Additionally, the '-o' was removed from the documentation of '--output-html', since it hasn't worked for that in a while ('-o' means open in browser instead). --- tests/run_from_console.js | 62 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/tests/run_from_console.js b/tests/run_from_console.js index bfdd1b69..2a5bb70b 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -17,8 +17,9 @@ program .option('-p, --provider ', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper') .option('-g, --generate-html', 'Instead of running the tests, just return the path to the generated HTML file, then wait for user interaction to exit (should be used with .js tests).') .option('-o, --open-in-browser', 'Open the generated HTML files in a web browser using the "open" module (must be used with the "-g"/"--generate-html" option).') - .option('-o, --output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') + .option('--output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') .option('-d, --debug', 'Show debug output (the "console" event) from the provider') + .option('-r, --relative', 'Use relative paths in the generated HTML file') .parse(process.argv); if (program.tests.length === 0) { @@ -31,6 +32,27 @@ var file_paths = []; var all_js = program.tests.reduce(function(a,e) { return a && e.slice(-3) == '.js'; }, true); +var get_path = function (/* arguments */) { + if (program.relative) { + return path.join.apply(null, arguments); + } else { + var args = Array.prototype.slice.call(arguments); + args.unshift(__dirname, '..'); + return path.resolve.apply(null, args); + } +}; + +var get_path_cwd = function (/* arguments */) { + if (program.relative) { + var part_path = path.join.apply(null, arguments); + return path.relative(path.join(__dirname, '..'), path.resolve(process.cwd(), part_path)); + } else { + var args = Array.prototype.slice.call(arguments); + args.unshift(process.cwd()); + return path.resolve.apply(null, args); + } +}; + if (all_js && !program.autoInject) { var all_modules = {}; @@ -44,7 +66,7 @@ if (all_js && !program.autoInject) { var eol = content.indexOf('\n', ind); var modules = content.slice(ind, eol).split(/,\s*/); modules.forEach(function (mod) { - all_modules[path.resolve(__dirname, '../include/', mod)+'.js'] = 1; + all_modules[get_path('include/', mod) + '.js'] = 1; }); } @@ -54,7 +76,7 @@ if (all_js && !program.autoInject) { var fakes_eol = content.indexOf('\n', fakes_ind); var fakes_modules = content.slice(fakes_ind, fakes_eol).split(/,\s*/); fakes_modules.forEach(function (mod) { - all_modules[path.resolve(__dirname, mod) + '.js'] = 1; + all_modules[get_path('tests/', mod) + '.js'] = 1; }); } }); @@ -67,27 +89,27 @@ if (program.autoInject) { temp.track(); var template = { - header: "\n\n\n\n\n
", + header: "\n\n\n\n\n
", script_tag: function(p) { return ""; }, footer: "\n\n" }; - template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/chai/chai.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/mocha/mocha.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon/pkg/sinon.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js')); - template.header += "\n" + template.script_tag(path.resolve(__dirname, '../node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/chai/chai.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/mocha/mocha.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon/pkg/sinon.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); + template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js')); template.header += "\n"; template.header = program.autoInject.reduce(function(acc, sn) { - return acc + "\n" + template.script_tag(path.resolve(process.cwd(), sn)); + return acc + "\n" + template.script_tag(get_path_cwd(sn)); }, template.header); file_paths = program.tests.map(function(jsn, ind) { var templ = template.header; templ += "\n"; - templ += template.script_tag(path.resolve(process.cwd(), jsn)); + templ += template.script_tag(get_path_cwd(jsn)); templ += template.footer; var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' }); @@ -118,15 +140,19 @@ if (program.outputHtml) { return; } + if (use_ansi) { + cursor + .bold() + .write(program.tests[path_ind]) + .reset() + .write("\n") + .write(Array(program.tests[path_ind].length+1).join('=')) + .write("\n\n"); + } + cursor - .bold() - .write(program.tests[path_ind]) - .reset() - .write("\n") - .write(Array(program.tests[path_ind].length+1).join('=')) - .write("\n\n") .write(data) - .write("\n"); + .write("\n\n"); }); }); } From 58529d347b1d19b05539118d2e447915df9900df Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 22 Sep 2014 21:49:17 -0400 Subject: [PATCH 026/527] Correct Travis CI Badge URL The readme was using the URL for the 'refactor/cleanup' branch, which doesn't exist anymore. It now uses the badge for the master branch as it should. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0aedd8a..b5679cdd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## noVNC: HTML5 VNC Client -[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=refactor%2Fcleanup)](https://travis-ci.org/kanaka/noVNC) +[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=master)](https://travis-ci.org/kanaka/noVNC) ### Description From f9fd0313b8cf72bfe449a25956ec35a6822eb957 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 22 Sep 2014 22:30:19 -0400 Subject: [PATCH 027/527] Make Utils.js use Object.define to modify Array Previously, we were modifying Array's prototype using simple assignment. This can mess with enumeration/iteration. Thus, we now use Object.defineProperty with enumerable set to false. See #366 --- include/util.js | 123 ++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/include/util.js b/include/util.js index 9e5f98c9..909d04b7 100644 --- a/include/util.js +++ b/include/util.js @@ -17,72 +17,75 @@ var Util = {}; * Make arrays quack */ -Array.prototype.push8 = function (num) { - "use strict"; - this.push(num & 0xFF); +var addFunc = function (cl, name, func) { + if (!cl.prototype[name]) { + Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); + } }; -Array.prototype.push16 = function (num) { +addFunc(Array, 'push8', function (num) { + "use strict"; + this.push(num & 0xFF); +}); + +addFunc(Array, 'push16', function (num) { "use strict"; this.push((num >> 8) & 0xFF, num & 0xFF); -}; -Array.prototype.push32 = function (num) { +}); + +addFunc(Array, 'push32', function (num) { "use strict"; this.push((num >> 24) & 0xFF, (num >> 16) & 0xFF, (num >> 8) & 0xFF, num & 0xFF); -}; +}); // IE does not support map (even in IE9) //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Array.prototype.map) { - Array.prototype.map = function (fun /*, thisp*/) { - "use strict"; - var len = this.length; - if (typeof fun != "function") { - throw new TypeError(); - } +addFunc(Array, 'map', function (fun /*, thisp*/) { + "use strict"; + var len = this.length; + if (typeof fun != "function") { + throw new TypeError(); + } - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in this) { - res[i] = fun.call(thisp, this[i], i, this); - } + var res = new Array(len); + var thisp = arguments[1]; + for (var i = 0; i < len; i++) { + if (i in this) { + res[i] = fun.call(thisp, this[i], i, this); } + } - return res; - }; -} + return res; +}); // IE <9 does not support indexOf //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (elt /*, from*/) { - "use strict"; - var len = this.length >>> 0; +addFunc(Array, 'indexOf', function (elt /*, from*/) { + "use strict"; + var len = this.length >>> 0; - var from = Number(arguments[1]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - if (from < 0) { - from += len; - } + var from = Number(arguments[1]) || 0; + from = (from < 0) ? Math.ceil(from) : Math.floor(from); + if (from < 0) { + from += len; + } - for (; from < len; from++) { - if (from in this && - this[from] === elt) { - return from; - } + for (; from < len; from++) { + if (from in this && + this[from] === elt) { + return from; } - return -1; - }; -} + } + return -1; +}); // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { @@ -131,30 +134,28 @@ if (!Object.keys) { //This prototype is provided by the Mozilla foundation and //is distributed under the MIT license. //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 - // internal IsCallable function - throw new TypeError("Function.prototype.bind - " + - "what is trying to be bound is not callable"); - } +addFunc(Function, 'bind', function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError("Function.prototype.bind - " + + "what is trying to be bound is not callable"); + } - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); - return fBound; - }; -} + return fBound; +}); // // requestAnimationFrame shim with setTimeout fallback From 3257d9f26535dee47cf21c8811e2c627c9dc72c3 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 30 Sep 2014 12:17:33 -0400 Subject: [PATCH 028/527] Force Travis to use Node 0.11.13 The latest version of Node.js has a bug that affects the Karma test runner. A patch has been merged to Karma, but has not landed in a version yet. Until a new version of Karma is released, we should keep node at 0.11.13. See karma-runner/karma#1182 (cherry picked from commit 9af2346a0cead634f3af5f390770ea65929c1f4a) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index dfcec2fe..371b974a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- '0.11' +- '0.11.13' env: matrix: - TEST_BROWSER_NAME=PhantomJS From 58ca1978eacdb030233b5f7351cc1b063da289ee Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Wed, 1 Oct 2014 16:30:39 +0200 Subject: [PATCH 029/527] Fix subprotocols Broken by b1dee9478815b22bf5fee3ee9e44321d4bb46c91 --- include/rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index ffa66020..0afe656d 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -316,7 +316,7 @@ var RFB; uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; Util.Info("connecting to " + uri); - this._sock.open(uri, this._sockProtocols); + this._sock.open(uri, this._wsProtocols); Util.Debug("<< RFB.connect"); }, From ec31f82eda92bf94e96072d9dc4ad07b5eeed772 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 29 Oct 2014 19:12:33 -0400 Subject: [PATCH 030/527] Fix Cursor Issue When Using True Color This fixes an issue where, when using true color, the changeCursor function would not actually write the cursor to the target array. Fixes #407 --- include/display.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/display.js b/include/display.js index 8763fa4a..1006c366 100644 --- a/include/display.js +++ b/include/display.js @@ -713,6 +713,12 @@ var Display; cur.push(rgb[1]); // green cur.push(rgb[0]); // red cur.push(alpha); // alpha + } else { + idx = ((w0 * y) + x) * 4; + cur.push(pixels[idx + 2]); // blue + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx]); // red + cur.push(alpha); // alpha } } } From dbaf49f5708d266ec0218661039f981fda0195cb Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sun, 2 Nov 2014 13:27:06 -0500 Subject: [PATCH 031/527] Added in guidelines for contributing to noVNC In CONTRIBUTING.md, you can now find a set of guidelines for contributing to the noVNC project. They detail coding style information, requirements for pull requests, and how to run the unit tests. --- CONTRIBUTING.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4bcea104 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,54 @@ +How to contribute to noVNC +========================== + +We accept code via pull requests on GitHub. There are several guidelines that +we expect contributors submitting code requests to follow. If you have issues +following any of these guidelines, feel free to drop us a line by leaving a +comment in the code request or sending us an email. + +Contributing Guidelines +----------------------- + +* While we don't have an official coding style guide, please try to follow + the general coding style of the existing code. +** Use four spaces instead of tabs +** prefix private variables and functions with an `_` + +* Please try to include unit tests for your code. For instance, if you + introduce a new encoding, add a test to `tests/test.rfb.js` under the + "Encoding Handlers" section (basically, input a small pattern in your + encoding and make sure the pattern gets displayed correctly). If you + fix a bug, try to add a unit test that would have caught that bug + (if possible -- some bugs, especially visual ones, are hard to test for). + +* Squash your commits down in to a clean commit history. For instance, there + should not be "cleanup" commits where you fix issues in previous commits in + the same pull request. Before you go to commit, use `git rebase -i` to + squash these changes into the relevant commits. For instance, a good commit + history might look like "Added support for FOO encoding, Added support for + BAR message, Placed Button in UI to Trigger BAR" (where each comma denotes + a separate commit). + +* Add both a title and description to your commit, if possible. Place more + detail on what you did in the description. + +Running the unit tests +---------------------- + +There are two ways to run the unit tests. For both ways, you should first run +`npm install` (not as root). + +The first way to run the tests is to run `npm test`. This will run all the +tests in the headless PhantomJS browser (which uses WebKit). + +The second way to run the tests is using the `tests/run_from_console.js` file. +This way is a bit more flexible, and can provide more information about what +went wrong. To run all the tests, simply run `tests/run_from_console.js`. +To run a specific test file, you can use the `-t path/to/test/file.js` option. +If you wish to simply generate the HTML for the test, use the `-g` option, and +the path to the temporary HTML file will be written to standard out. To open +this file in your default browser automatically, pass the `-o` option as well. +More information can be found by passing the `--help` or `-h` option. + + +Thanks, and happy coding! From 795fca23dc6cf124b2e6969ee42bd8e7c836d605 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 11 Nov 2014 16:29:06 +0100 Subject: [PATCH 032/527] Modify minimum width of clear to accomodate smaller screens --- include/display.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/display.js b/include/display.js index 1006c366..a42b854a 100644 --- a/include/display.js +++ b/include/display.js @@ -318,7 +318,7 @@ var Display; // Clearing the current viewport first fixes the issue this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); } - this.resize(640, 20); + this.resize(240, 20); this._drawCtx.clearRect(0, 0, this._viewportLoc.w, this._viewportLoc.h); } From bd6874e087a2fdfa068d41d089e7f357798f766a Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 13 Nov 2014 17:22:48 +0100 Subject: [PATCH 033/527] We should use the globally declared UI variable --- include/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index e869aa69..4748ff00 100644 --- a/include/ui.js +++ b/include/ui.js @@ -22,7 +22,7 @@ var UI; "keysymdef.js", "keyboard.js", "input.js", "display.js", "jsunzip.js", "rfb.js", "keysym.js"]); - var UI = { + UI = { rfb_state : 'loaded', settingsOpen : false, From fda40d8927fda5fedbb6c9ca60532b5213071c36 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 17 Nov 2014 14:19:19 -0500 Subject: [PATCH 034/527] Fix broken tests from changing default screen size Commit 795fca23dc6cf124b2e6969ee42bd8e7c836d605 changed the default size from 640 to 240. This broke a couple tests which depended on the default size being 640. Those tests have now been fixed. --- tests/test.rfb.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index fb32af0f..6233fa8f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1083,8 +1083,8 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send an update request if there is sufficient data', function () { var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, - dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] }; - var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20); + dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; + var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 240, 20); client._framebufferUpdate = function () { return true; }; client._sock._websocket._receive_data(new Uint8Array([0])); @@ -1099,8 +1099,8 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should resume receiving an update if we previously did not have enough data', function () { var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, - dirtyBoxes: [ { x: 0, y: 0, w: 640, h: 20 } ] }; - var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 640, 20); + dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; + var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 240, 20); // just enough to set FBU.rects client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); From c42ea22525ffdc7332a96cea96a5b8e7bf40513d Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 17 Nov 2014 17:52:33 -0500 Subject: [PATCH 035/527] Fix Karma sinon-chai version issues Previously, we were using the karma-sinon-chai package to provide sinon-chai to karma. This used an older version of sinon-chai, and looks to be no longer maintained (it's been a month since sinon-chai was updated). A new package, karma-sinon-chai-latest, is now used. This package uses the latest version of sinon-chai and sinon, just like karma-chai uses the latest version of chai. --- package.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 3f1b29bc..63c26662 100644 --- a/package.json +++ b/package.json @@ -28,23 +28,23 @@ "devDependencies": { "ansi": "^0.3.0", "casperjs": "^1.1.0-beta3", - "chai": "^1.9.1", - "commander": "^2.2.0", - "karma": "^0.12.16", + "chai": "^1.10.0", + "commander": "^2.5.0", + "karma": "^0.12.25", "karma-chai": "^0.1.0", - "karma-mocha": "^0.1.4", - "karma-mocha-reporter": "^0.2.5", + "karma-mocha": "^0.1.9", + "karma-mocha-reporter": "^0.3.1", "karma-phantomjs-launcher": "^0.1.4", - "karma-sauce-launcher": "^0.2.8", + "karma-sauce-launcher": "^0.2.10", "karma-sinon": "^1.0.3", - "karma-sinon-chai": "^0.1.6", - "mocha": "^1.20.1", - "open": "0.0.5", - "phantom": "^0.6.3", - "phantomjs": "^1.9.7-9", - "sinon": "^1.10.2", - "sinon-chai": "^2.5.0", - "spooky": "^0.2.4", - "temp": "^0.8.0" + "karma-sinon-chai-latest": "^0.1.0", + "mocha": "^2.0.1", + "open": "^0.0.5", + "phantom": "^0.7.0", + "phantomjs": "^1.9.12", + "sinon": "^1.12.1", + "sinon-chai": "^2.6.0", + "spooky": "^0.2.5", + "temp": "^0.8.1" } } From 40ac6f0ab605d4adf8c05bc3dcbc2fce2442d5c4 Mon Sep 17 00:00:00 2001 From: Ramon de Klein Date: Thu, 6 Nov 2014 13:11:46 +0100 Subject: [PATCH 036/527] Don't draw "blank" HEXTILE tiles with random data Previously, if a HEXTILE tiles was received with a subencoding of 0x00, it would draw a rectangle using data from the render queue, which would result in random colored blocks when using the HEXTILE encoding. This is the result of a miscopy during the refactoring. It now has the correct functionality according to the RFB protocol specification, which is to draw a rectangle with the last set background color. Closes #411 --- include/rfb.js | 3 +-- tests/test.rfb.js | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 0afe656d..f2f70795 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1496,8 +1496,7 @@ var RFB; // Weird: ignore blanks are RAW Util.Debug(" Ignoring blank after RAW"); } else { - this._display.fillRect(x, y, w, h, rQ, rQi); - rQi += this._FBU.bytes - 1; + this._display.fillRect(x, y, w, h, FBU.background); } } else if (this._FBU.subencoding & 0x01) { // Raw this._display.blitImage(x, y, w, h, rQ, rQi); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 6233fa8f..1510f9e5 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1312,6 +1312,32 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display).to.have.displayed(new Uint8Array(expected)); }); + it('should handle a tile with only bg specified and an empty frame afterwards', function () { + // set the width so we can have two tiles + client._fb_width = 8; + client._display._fb_width = 8; + client._display._viewportLoc.w = 8; + + var info = [{ x: 0, y: 0, width: 8, height: 4, encoding: 0x05 }]; + + var rect = []; + + // send a bg frame + rect.push(0x02); + rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + + // send an empty frame + rect.push(0x00); + + send_fbu_msg(info, [rect], client); + + var expected = []; + var i; + for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 1: solid + for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 2: same bkground color + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + it('should handle a tile with bg and coloured subrects', function () { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rect = []; From df89129ff0bcaa5c4c3f02d06b6ed3ebbe4cba40 Mon Sep 17 00:00:00 2001 From: Joe Huss Date: Sun, 23 Nov 2014 07:22:39 -0500 Subject: [PATCH 037/527] Fix for typo from a recent pull There is a minor typo in the recent pull Simple fix. --- include/rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index f2f70795..74777d53 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1496,7 +1496,7 @@ var RFB; // Weird: ignore blanks are RAW Util.Debug(" Ignoring blank after RAW"); } else { - this._display.fillRect(x, y, w, h, FBU.background); + 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); From 4865278deead91fa58b0779b10f8e6b81d169e0e Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 24 Nov 2014 15:16:12 -0500 Subject: [PATCH 038/527] Fixed Erroneous HEXTILE test The HEXTILE test which tested for a background tile followed by an empty tile was only wide enough to actually test for one tile, thus not actually testing the functionality. It now actually uses two tiles, thus actually testing the functionality. --- tests/test.rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 1510f9e5..d80e3d52 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1318,7 +1318,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._display._fb_width = 8; client._display._viewportLoc.w = 8; - var info = [{ x: 0, y: 0, width: 8, height: 4, encoding: 0x05 }]; + var info = [{ x: 0, y: 0, width: 32, height: 4, encoding: 0x05 }]; var rect = []; From ed7f8c38860b8ebc9b0b02887ebaaaad42bd46ca Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 26 Nov 2014 09:22:12 +0100 Subject: [PATCH 039/527] Add ISO Level 3 Shift (AltGr) to keysym.js --- include/keysym.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/keysym.js b/include/keysym.js index a00d595e..2b971984 100644 --- a/include/keysym.js +++ b/include/keysym.js @@ -170,6 +170,8 @@ XK_Super_R = 0xffec, /* Right super */ XK_Hyper_L = 0xffed, /* Left hyper */ XK_Hyper_R = 0xffee, /* Right hyper */ +XK_ISO_Level3_Shift = 0xfe03, /* AltGr */ + /* * Latin 1 * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) From 282834caf14a0673587fae1599d2fa09a6370c1b Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 26 Nov 2014 09:24:13 +0100 Subject: [PATCH 040/527] Fixes #309, make use of keysym.js Bonus 3 bug fixes: * Meta is 0xFFE7 not 0xFE07 * Super_L is 0xFFEB not 0xFFEC * Super_R is 0xFFEC not 0xFFED --- include/keyboard.js | 134 ++++++++++++++++++++++---------------------- include/rfb.js | 12 ++-- 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/include/keyboard.js b/include/keyboard.js index 60443214..86670312 100644 --- a/include/keyboard.js +++ b/include/keyboard.js @@ -31,7 +31,7 @@ var kbdUtil = (function() { function hasShortcutModifier(charModifier, currentModifiers) { var mods = {}; for (var key in currentModifiers) { - if (parseInt(key) !== 0xffe1) { + if (parseInt(key) !== XK_Shift_L) { mods[key] = currentModifiers[key]; } } @@ -65,24 +65,18 @@ var kbdUtil = (function() { // Helper object tracking modifier key state // and generates fake key events to compensate if it gets out of sync function ModifierSync(charModifier) { - var ctrl = 0xffe3; - var alt = 0xffe9; - var altGr = 0xfe03; - var shift = 0xffe1; - var meta = 0xffe7; - if (!charModifier) { if (isMac()) { // on Mac, Option (AKA Alt) is used as a char modifier - charModifier = [alt]; + charModifier = [XK_Alt_L]; } else if (isWindows()) { // on Windows, Ctrl+Alt is used as a char modifier - charModifier = [alt, ctrl]; + charModifier = [XK_Alt_L, XK_Control_L]; } else if (isLinux()) { - // on Linux, AltGr is used as a char modifier - charModifier = [altGr]; + // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier + charModifier = [XK_ISO_Level3_Shift]; } else { charModifier = []; @@ -90,11 +84,11 @@ var kbdUtil = (function() { } var state = {}; - state[ctrl] = false; - state[alt] = false; - state[altGr] = false; - state[shift] = false; - state[meta] = false; + state[XK_Control_L] = false; + state[XK_Alt_L] = false; + state[XK_ISO_Level3_Shift] = false; + state[XK_Shift_L] = false; + state[XK_Meta_L] = false; function sync(evt, keysym) { var result = []; @@ -102,25 +96,30 @@ var kbdUtil = (function() { return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; } - if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { - state[ctrl] = evt.ctrlKey; - result.push(syncKey(ctrl)); + if (evt.ctrlKey !== undefined && + evt.ctrlKey !== state[XK_Control_L] && keysym !== XK_Control_L) { + state[XK_Control_L] = evt.ctrlKey; + result.push(syncKey(XK_Control_L)); } - if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { - state[alt] = evt.altKey; - result.push(syncKey(alt)); + if (evt.altKey !== undefined && + evt.altKey !== state[XK_Alt_L] && keysym !== XK_Alt_L) { + state[XK_Alt_L] = evt.altKey; + result.push(syncKey(XK_Alt_L)); } - if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { - state[altGr] = evt.altGraphKey; - result.push(syncKey(altGr)); + if (evt.altGraphKey !== undefined && + evt.altGraphKey !== state[XK_ISO_Level3_Shift] && keysym !== XK_ISO_Level3_Shift) { + state[XK_ISO_Level3_Shift] = evt.altGraphKey; + result.push(syncKey(XK_ISO_Level3_Shift)); } - if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { - state[shift] = evt.shiftKey; - result.push(syncKey(shift)); + if (evt.shiftKey !== undefined && + evt.shiftKey !== state[XK_Shift_L] && keysym !== XK_Shift_L) { + state[XK_Shift_L] = evt.shiftKey; + result.push(syncKey(XK_Shift_L)); } - if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { - state[meta] = evt.metaKey; - result.push(syncKey(meta)); + if (evt.metaKey !== undefined && + evt.metaKey !== state[XK_Meta_L] && keysym !== XK_Meta_L) { + state[XK_Meta_L] = evt.metaKey; + result.push(syncKey(XK_Meta_L)); } return result; } @@ -211,21 +210,21 @@ var kbdUtil = (function() { return shiftPressed ? keycode : keycode + 32; // A-Z } if (keycode >= 0x60 && keycode <= 0x69) { - return 0xffb0 + (keycode - 0x60); // numpad 0-9 + return XK_KP_0 + (keycode - 0x60); // numpad 0-9 } switch(keycode) { - case 0x20: return 0x20; // space - case 0x6a: return 0xffaa; // multiply - case 0x6b: return 0xffab; // add - case 0x6c: return 0xffac; // separator - case 0x6d: return 0xffad; // subtract - case 0x6e: return 0xffae; // decimal - case 0x6f: return 0xffaf; // divide - case 0xbb: return 0x2b; // + - case 0xbc: return 0x2c; // , - case 0xbd: return 0x2d; // - - case 0xbe: return 0x2e; // . + case 0x20: return XK_space; + case 0x6a: return XK_KP_Multiply; + case 0x6b: return XK_KP_Add; + case 0x6c: return XK_KP_Separator; + case 0x6d: return XK_KP_Subtract; + case 0x6e: return XK_KP_Decimal; + case 0x6f: return XK_KP_Divide; + case 0xbb: return XK_plus; + case 0xbc: return XK_comma; + case 0xbd: return XK_minus; + case 0xbe: return XK_period; } return nonCharacterKey({keyCode: keycode}); @@ -239,43 +238,44 @@ var kbdUtil = (function() { var keycode = evt.keyCode; if (keycode >= 0x70 && keycode <= 0x87) { - return 0xffbe + keycode - 0x70; // F1-F24 + return XK_F1 + keycode - 0x70; // F1-F24 } switch (keycode) { - case 8 : return 0xFF08; // BACKSPACE - case 13 : return 0xFF0D; // ENTER + case 8 : return XK_BackSpace; + case 13 : return XK_Return; - case 9 : return 0xFF09; // TAB + case 9 : return XK_Tab; - case 27 : return 0xFF1B; // ESCAPE - case 46 : return 0xFFFF; // DELETE + case 27 : return XK_Escape; + case 46 : return XK_Delete; - case 36 : return 0xFF50; // HOME - case 35 : return 0xFF57; // END - case 33 : return 0xFF55; // PAGE_UP - case 34 : return 0xFF56; // PAGE_DOWN - case 45 : return 0xFF63; // INSERT + case 36 : return XK_Home; + case 35 : return XK_End; + case 33 : return XK_Page_Up; + case 34 : return XK_Page_Down; + case 45 : return XK_Insert; - case 37 : return 0xFF51; // LEFT - case 38 : return 0xFF52; // UP - case 39 : return 0xFF53; // RIGHT - case 40 : return 0xFF54; // DOWN - case 16 : return 0xFFE1; // SHIFT - case 17 : return 0xFFE3; // CONTROL - case 18 : return 0xFFE9; // Left ALT (Mac Option) + case 37 : return XK_Left; + case 38 : return XK_Up; + case 39 : return XK_Right; + case 40 : return XK_Down; - case 224 : return 0xFE07; // Meta - case 225 : return 0xFE03; // AltGr - case 91 : return 0xFFEC; // Super_L (Win Key) - case 92 : return 0xFFED; // Super_R (Win Key) - case 93 : return 0xFF67; // Menu (Win Menu), Mac Command + case 16 : return XK_Shift_L; + case 17 : return XK_Control_L; + case 18 : return XK_Alt_L; // also: Option-key on Mac + + case 224 : return XK_Meta_L; + case 225 : return XK_ISO_Level3_Shift; // AltGr + case 91 : return XK_Super_L; // also: Windows-key + case 92 : return XK_Super_R; // also: Windows-key + case 93 : return XK_Menu; // also: Windows-Menu, Command on Mac default: return null; } } return { hasShortcutModifier : hasShortcutModifier, - hasCharModifier : hasCharModifier, + hasCharModifier : hasCharModifier, ModifierSync : ModifierSync, getKey : getKey, getKeysym : getKeysym, diff --git a/include/rfb.js b/include/rfb.js index 74777d53..59fd785d 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -252,12 +252,12 @@ var RFB; Util.Info("Sending Ctrl-Alt-Del"); var arr = []; - arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 1)); // Control - arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 1)); // Alt - arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 1)); // Delete - arr = arr.concat(RFB.messages.keyEvent(0xFFFF, 0)); // Delete - arr = arr.concat(RFB.messages.keyEvent(0xFFE9, 0)); // Alt - arr = arr.concat(RFB.messages.keyEvent(0xFFE3, 0)); // Control + arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 1)); + arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1)); + arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1)); + arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0)); + arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0)); + arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0)); this._sock.send(arr); }, From 0331495382f7572da13968520d890f02d18028b4 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Fri, 28 Nov 2014 16:35:55 +0100 Subject: [PATCH 041/527] Loads keysym.js Without it, `XK_ISO_Level3_Shift` (at least) is undefined. --- vnc_auto.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnc_auto.html b/vnc_auto.html index ff376fec..b05024e3 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -77,7 +77,7 @@ // Load supporting scripts Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js"]); + "jsunzip.js", "rfb.js", "keysym.js"]); var rfb; From dc4b6301c810bd82f4d00c2032ac6775973a4edb Mon Sep 17 00:00:00 2001 From: Alexander Clouter Date: Sat, 22 Nov 2014 10:20:51 +0000 Subject: [PATCH 042/527] 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 155d78b39933a396d2555376f4e70f976c30b86d Mon Sep 17 00:00:00 2001 From: Jacob Swanner Date: Thu, 8 Jan 2015 15:25:55 -0500 Subject: [PATCH 043/527] Unregister event listeners from websock. Prevents possible memory and event notification leaks when tearing down connection and reestablishing a new one. --- include/rfb.js | 4 ++++ include/websock.js | 4 ++++ tests/test.rfb.js | 26 ++++++++++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/include/rfb.js b/include/rfb.js index 59fd785d..f461aff9 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -197,6 +197,7 @@ var RFB; } else { this._fail("Server disconnected" + msg); } + this._sock.off('close'); }.bind(this)); this._sock.on('error', function (e) { Util.Warn("WebSocket on-error event"); @@ -239,6 +240,9 @@ var RFB; disconnect: function () { this._updateState('disconnect', 'Disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); }, sendPassword: function (passwd) { diff --git a/include/websock.js b/include/websock.js index 1b89a91f..cc82e5a2 100644 --- a/include/websock.js +++ b/include/websock.js @@ -200,6 +200,10 @@ function Websock() { }, // Event Handlers + off: function (evt) { + this._eventHandlers[evt] = function () {}; + }, + on: function (evt, handler) { this._eventHandlers[evt] = handler; }, diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d80e3d52..d777a860 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -62,6 +62,24 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._updateState).to.have.been.calledOnce; expect(client._updateState).to.have.been.calledWith('disconnect'); }); + + it('should unregister error event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('error'); + }); + + it('should unregister message event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('message'); + }); + + it('should unregister open event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('open'); + }); }); describe('#sendPassword', function () { @@ -1710,6 +1728,14 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_state).to.equal('failed'); }); + it('should unregister close event handler', function () { + sinon.spy(client._sock, 'off'); + client.connect('host', 8675); + client._rfb_state = 'disconnect'; + client._sock._websocket.close(); + expect(client._sock.off).to.have.been.calledWith('close'); + }); + // error events do nothing }); }); From b804b3e45810b54bd8f93c7df73598a1b19a19b7 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 15 Jan 2015 16:27:18 +0100 Subject: [PATCH 044/527] Fixes #428 - hides the local cursor when using the server-side cursor. --- include/display.js | 7 ++++++- include/rfb.js | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/include/display.js b/include/display.js index a42b854a..db45d7b1 100644 --- a/include/display.js +++ b/include/display.js @@ -93,13 +93,14 @@ var Display; this._cursor_uri = true; } Util.Info("Data URI scheme cursor supported"); + this._target.style.cursor = curSave; } else { if (this._cursor_uri === null || this._cursor_uri === undefined) { this._cursor_uri = false; } Util.Warn("Data URI scheme cursor not supported"); + this._target.style.cursor = "none"; } - this._target.style.cursor = curSave; } catch (exc) { Util.Error("Data URI scheme cursor test exception: " + exc); this._cursor_uri = false; @@ -470,6 +471,10 @@ var Display; this._target.style.cursor = "default"; }, + disableLocalCursor: function () { + this._target.style.cursor = "none"; + }, + // Overridden getters/setters get_context: function () { return this._drawCtx; diff --git a/include/rfb.js b/include/rfb.js index 59fd785d..980e61c9 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -411,7 +411,9 @@ var RFB; if (this._display && this._display.get_context()) { this._keyboard.ungrab(); this._mouse.ungrab(); - this._display.defaultCursor(); + if (state !== 'connect' && state !== 'loaded') { + this._display.defaultCursor(); + } if (Util.get_logging() !== 'debug' || state === 'loaded') { // Show noVNC logo on load and when disconnected, unless in // debug mode @@ -1215,6 +1217,7 @@ var RFB; RFB.prototype.set_local_cursor = function (cursor) { if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { this._local_cursor = false; + this._display.disableLocalCursor(); //Only show server-side cursor } else { if (this._display.get_cursor_uri()) { this._local_cursor = true; From 455f8f3fd0d3296cd3590fe354dd811ed755565c Mon Sep 17 00:00:00 2001 From: Nathaniel Bibler Date: Mon, 26 Jan 2015 11:46:40 -0500 Subject: [PATCH 045/527] Replace custom getPosition algorithms with getBoundingClientRect. --- include/util.js | 69 +++---------------------------------------------- 1 file changed, 4 insertions(+), 65 deletions(-) diff --git a/include/util.js b/include/util.js index 909d04b7..482b10c9 100644 --- a/include/util.js +++ b/include/util.js @@ -433,72 +433,11 @@ Util.load_scripts = function (files) { }; -// Get DOM element position on page -// This solution is based based on http://www.greywyvern.com/?post=331 -// Thanks to Brian Huisman AKA GreyWyvern! -Util.getPosition = (function () { +Util.getPosition = function(obj) { "use strict"; - function getStyle(obj, styleProp) { - var y; - if (obj.currentStyle) { - y = obj.currentStyle[styleProp]; - } else if (window.getComputedStyle) - y = window.getComputedStyle(obj, null)[styleProp]; - return y; - } - - function scrollDist() { - var myScrollTop = 0, myScrollLeft = 0; - var html = document.getElementsByTagName('html')[0]; - - // get the scrollTop part - if (html.scrollTop && document.documentElement.scrollTop) { - myScrollTop = html.scrollTop; - } else if (html.scrollTop || document.documentElement.scrollTop) { - myScrollTop = html.scrollTop + document.documentElement.scrollTop; - } else if (document.body.scrollTop) { - myScrollTop = document.body.scrollTop; - } else { - myScrollTop = 0; - } - - // get the scrollLeft part - if (html.scrollLeft && document.documentElement.scrollLeft) { - myScrollLeft = html.scrollLeft; - } else if (html.scrollLeft || document.documentElement.scrollLeft) { - myScrollLeft = html.scrollLeft + document.documentElement.scrollLeft; - } else if (document.body.scrollLeft) { - myScrollLeft = document.body.scrollLeft; - } else { - myScrollLeft = 0; - } - - return [myScrollLeft, myScrollTop]; - } - - return function (obj) { - var curleft = 0, curtop = 0, scr = obj, fixed = false; - while ((scr = scr.parentNode) && scr != document.body) { - curleft -= scr.scrollLeft || 0; - curtop -= scr.scrollTop || 0; - if (getStyle(scr, "position") == "fixed") { - fixed = true; - } - } - if (fixed && !window.opera) { - var scrDist = scrollDist(); - curleft += scrDist[0]; - curtop += scrDist[1]; - } - - do { - curleft += obj.offsetLeft; - curtop += obj.offsetTop; - } while ((obj = obj.offsetParent)); - - return {'x': curleft, 'y': curtop}; - }; -})(); + var objPosition = obj.getBoundingClientRect(); + return {'x': objPosition.left, 'y': objPosition.top}; +}; // Get mouse event position in DOM element From e9f55ea0f8482f306ba862a7270083a691f52f2a Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 3 Feb 2015 13:04:16 +0100 Subject: [PATCH 046/527] Fixes issue #435 - added missing comma in keysym.js --- include/keysym.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/keysym.js b/include/keysym.js index 2b971984..58b107c0 100644 --- a/include/keysym.js +++ b/include/keysym.js @@ -58,7 +58,7 @@ XK_KP_Up = 0xff97, XK_KP_Right = 0xff98, XK_KP_Down = 0xff99, XK_KP_Prior = 0xff9a, -XK_KP_Page_Up = 0xff9a +XK_KP_Page_Up = 0xff9a, XK_KP_Next = 0xff9b, XK_KP_Page_Down = 0xff9b, XK_KP_End = 0xff9c, From 5b7598ac6bdf24ee6fb0dc6616b08551d12f815f Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 6 Feb 2015 15:53:10 +0100 Subject: [PATCH 047/527] Use our own event registration function for keyboardinputReset. --- include/ui.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index 4748ff00..4132d050 100644 --- a/include/ui.js +++ b/include/ui.js @@ -17,7 +17,6 @@ var UI; // Load supporting scripts window.onscriptsload = function () { UI.load(); }; - window.onload = function () { UI.keyboardinputReset(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", "jsunzip.js", "rfb.js", "keysym.js"]); @@ -139,6 +138,8 @@ var UI; UI.setViewClip(); Util.addEvent(window, 'resize', UI.setViewClip); + Util.addEvent(window, 'load', UI.keyboardinputReset); + Util.addEvent(window, 'beforeunload', function () { if (UI.rfb_state === 'normal') { return "You are currently connected."; From 636be753b2c2769e06ec27665fbeb652a060481a Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 6 Feb 2015 16:43:45 +0100 Subject: [PATCH 048/527] Support automatic resize [Part 1/4]: display.js * Split viewportChange into two functions, one for changing size and the other for changing position. * Modified viewport code to be capable of changing to a bigger size in the context of a client-initiated resize. * Made clearer distinctions between when viewport-clipping or not. * Added public function for telling when viewport-clipping. * Updated tests that were using viewportChange. --- include/display.js | 112 +++++++++++++++++++++++++----------------- tests/test.display.js | 22 +++++---- tests/test.rfb.js | 8 +-- tests/viewport.html | 9 ++-- 4 files changed, 86 insertions(+), 65 deletions(-) diff --git a/include/display.js b/include/display.js index db45d7b1..d1278681 100644 --- a/include/display.js +++ b/include/display.js @@ -111,54 +111,12 @@ var Display; Display.prototype = { // Public methods - viewportChange: function (deltaX, deltaY, width, height) { + viewportChangePos: function (deltaX, deltaY) { var vp = this._viewportLoc; - var cr = this._cleanRect; - var canvas = this._target; if (!this._viewport) { - Util.Debug("Setting viewport to full display region"); deltaX = -vp.w; // clamped later of out of bounds deltaY = -vp.h; - width = this._fb_width; - height = this._fb_height; - } - - if (typeof(deltaX) === "undefined") { deltaX = 0; } - if (typeof(deltaY) === "undefined") { deltaY = 0; } - if (typeof(width) === "undefined") { width = vp.w; } - if (typeof(height) === "undefined") { height = vp.h; } - - // Size change - if (width > this._fb_width) { width = this._fb_width; } - if (height > this._fb_height) { height = this._fb_height; } - - if (vp.w !== width || vp.h !== height) { - // Change width - if (width < vp.w && cr.x2 > vp.x + width - 1) { - cr.x2 = vp.x + width - 1; - } - vp.w = width; - - // Change height - if (height < vp.h && cr.y2 > vp.y + height - 1) { - cr.y2 = vp.y + height - 1; - } - vp.h = height; - - var saveImg = null; - if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { - var img_width = canvas.width < vp.w ? canvas.width : vp.w; - var img_height = canvas.height < vp.h ? canvas.height : vp.h; - saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); - } - - canvas.width = vp.w; - canvas.height = vp.h; - - if (saveImg) { - this._drawCtx.putImageData(saveImg, 0, 0); - } } var vx2 = vp.x + vp.w - 1; @@ -191,6 +149,7 @@ var Display; vy2 += deltaY; // Update the clean rectangle + var cr = this._cleanRect; if (vp.x > cr.x1) { cr.x1 = vp.x; } @@ -228,6 +187,7 @@ var Display; // Copy the valid part of the viewport to the shifted location var saveStyle = this._drawCtx.fillStyle; + var canvas = this._target; this._drawCtx.fillStyle = "rgb(255,255,255)"; if (deltaX !== 0) { this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); @@ -240,6 +200,58 @@ var Display; this._drawCtx.fillStyle = saveStyle; }, + viewportChangeSize: function(width, height) { + + if (!this._viewport || + typeof(width) === "undefined" || typeof(height) === "undefined") { + + Util.Debug("Setting viewport to full display region"); + width = this._fb_width; + height = this._fb_height; + } + + var vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + + var cr = this._cleanRect; + + if (width < vp.w && cr.x2 > vp.x + width - 1) { + cr.x2 = vp.x + width - 1; + } + + if (height < vp.h && cr.y2 > vp.y + height - 1) { + cr.y2 = vp.y + height - 1; + } + + if (this.fbuClip()) { + // clipping + vp.w = window.innerWidth; + var cb = document.getElementById('noVNC-control-bar'); + var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; + vp.h = window.innerHeight - controlbar_h - 5; + } else { + // scrollbars + vp.w = width; + vp.h = height; + } + + var saveImg = null; + var canvas = this._target; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } + + canvas.width = vp.w; + canvas.height = vp.h; + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } + } + }, + // Return a map of clean and dirty areas of the viewport and reset the // tracking of clean and dirty areas // @@ -305,7 +317,7 @@ var Display; this._rescale(this._scale); - this.viewportChange(); + this.viewportChangeSize(); }, clear: function () { @@ -475,6 +487,14 @@ var Display; this._target.style.cursor = "none"; }, + fbuClip: function () { + var cb = document.getElementById('noVNC-control-bar'); + var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; + return (this._viewport && + (this._fb_width > window.innerWidth + || this._fb_height > window.innerHeight - controlbar_h - 5)); + }, + // Overridden getters/setters get_context: function () { return this._drawCtx; @@ -485,14 +505,14 @@ var Display; }, set_width: function (w) { - this.resize(w, this._fb_height); + this._fb_width = w; }, get_width: function () { return this._fb_width; }, set_height: function (h) { - this.resize(this._fb_width, h); + this._fb_height = h; }, get_height: function () { return this._fb_height; diff --git a/tests/test.display.js b/tests/test.display.js index 25adfbea..949aca1e 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -65,13 +65,15 @@ describe('Display/Canvas Helper', function () { beforeEach(function () { display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); display.resize(5, 5); - display.viewportChange(1, 1, 3, 3); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); display.getCleanDirtyReset(); }); it('should take viewport location into consideration when drawing images', function () { - display.resize(4, 4); - display.viewportChange(0, 0, 2, 2); + display.set_width(4); + display.set_height(4); + display.viewportChangeSize(2, 2); display.drawImage(make_image_canvas(basic_data), 1, 1); var expected = new Uint8Array(16); @@ -82,7 +84,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the left side when shifted left', function () { - display.viewportChange(-1, 0, 3, 3); + display.viewportChangePos(-1, 0); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 2, h: 3 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -90,7 +92,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the right side when shifted right', function () { - display.viewportChange(1, 0, 3, 3); + display.viewportChangePos(1, 0); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 2, y: 1, w: 2, h: 3 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -98,7 +100,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the top part when shifted up', function () { - display.viewportChange(0, -1, 3, 3); + display.viewportChangePos(0, -1); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 1, w: 3, h: 2 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -106,7 +108,7 @@ describe('Display/Canvas Helper', function () { }); it('should redraw the bottom part when shifted down', function () { - display.viewportChange(0, 1, 3, 3); + display.viewportChangePos(0, 1); var cdr = display.getCleanDirtyReset(); expect(cdr.cleanBox).to.deep.equal({ x: 1, y: 2, w: 3, h: 2 }); expect(cdr.dirtyBoxes).to.have.length(1); @@ -114,7 +116,7 @@ describe('Display/Canvas Helper', function () { }); it('should reset the entire viewport to being clean after calculating the clean/dirty boxes', function () { - display.viewportChange(0, 1, 3, 3); + display.viewportChangePos(0, 1); var cdr1 = display.getCleanDirtyReset(); var cdr2 = display.getCleanDirtyReset(); expect(cdr1).to.not.deep.equal(cdr2); @@ -146,9 +148,9 @@ describe('Display/Canvas Helper', function () { }); it('should update the viewport dimensions', function () { - sinon.spy(display, 'viewportChange'); + sinon.spy(display, 'viewportChangeSize'); display.resize(2, 2); - expect(display.viewportChange).to.have.been.calledOnce; + expect(display.viewportChangeSize).to.have.been.calledOnce; }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index d777a860..2ac8a129 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1593,7 +1593,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should not send movement messages when viewport dragging', function () { client._viewportDragging = true; - client._display.viewportChange = sinon.spy(); + client._display.viewportChangePos = sinon.spy(); client._mouse._onMouseMove(13, 9); expect(client._sock.send).to.not.have.been.called; }); @@ -1622,14 +1622,14 @@ describe('Remote Frame Buffer Protocol Client', function() { client._viewportDrag = true; client._viewportDragging = true; client._viewportDragPos = { x: 13, y: 9 }; - client._display.viewportChange = sinon.spy(); + client._display.viewportChangePos = sinon.spy(); client._mouse._onMouseMove(10, 4); expect(client._viewportDragging).to.be.true; expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 }); - expect(client._display.viewportChange).to.have.been.calledOnce; - expect(client._display.viewportChange).to.have.been.calledWith(3, 5); + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(3, 5); }); }); diff --git a/tests/viewport.html b/tests/viewport.html index cb13ecf3..374d8b15 100644 --- a/tests/viewport.html +++ b/tests/viewport.html @@ -97,7 +97,7 @@ deltaY = lastPos.y - y; // drag frame buffer lastPos = {'x': x, 'y': y}; - display.viewportChange(deltaX, deltaY); + display.viewportChangePos(deltaX, deltaY); return; } @@ -166,14 +166,13 @@ var p = $D('canvas').parentNode; message("doResize1: [" + (p.offsetWidth - padW) + "," + (p.offsetHeight - padH) + "]"); - display.viewportChange(0, 0, - p.offsetWidth - padW, p.offsetHeight - padH); + display.viewportChangeSize(p.offsetWidth - padW, p.offsetHeight - padH); /* var pos, new_w, new_h;pos pos = Util.getPosition($D('canvas').parentNode); new_w = window.innerWidth - pos.x; new_h = window.innerHeight - pos.y; - display.viewportChange(0, 0, new_w, new_h); + display.viewportChangeSize(new_w, new_h); */ } @@ -194,7 +193,7 @@ Util.addEvent(window, 'resize', doResize); // Shrink viewport for first resize call so that the // scrollbars are disabled - display.viewportChange(0, 0, 10, 10); + display.viewportChangeSize(10, 10); setTimeout(doResize, 1); setInterval(dirtyRedraw, 50); From b0ec6509f11469569c438e5f5e4300e6a1ce4dad Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 6 Feb 2015 17:06:48 +0100 Subject: [PATCH 049/527] Support automatic resize [Part 2/4]: rfb.js * Support sending the setDesktopSize encoding (client -> server) * Support recieving the ExtendedDesktopSize encoding (server <- client) --- include/rfb.js | 79 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 559eccbc..03d4b816 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -53,7 +53,8 @@ var RFB; //['compress_lo', -255 ], ['compress_hi', -247 ], ['last_rect', -224 ], - ['xvp', -309 ] + ['xvp', -309 ], + ['ext_desktop_size', -308 ] ]; this._encHandlers = {}; @@ -106,6 +107,10 @@ var RFB; pixels: 0 }; + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; + // Mouse state this._mouse_buttonMask = 0; this._mouse_arr = []; @@ -305,6 +310,32 @@ var RFB; this._sock.send(RFB.messages.clientCutText(text)); }, + setDesktopSize: function (width, height) { + if (this._rfb_state !== "normal") { return; } + + if (this._supportsSetDesktopSize) { + + var arr = [251]; // msg-type + arr.push8(0); // padding + arr.push16(width); // width + arr.push16(height); // height + + arr.push8(1); // number-of-screens + arr.push8(0); // padding + + // screen array + arr.push32(this._screen_id); // id + arr.push16(0); // x-position + arr.push16(0); // y-position + arr.push16(width); // width + arr.push16(height); // height + arr.push32(this._screen_flags); // flags + + this._sock.send(arr); + } + }, + + // Private methods _connect: function () { @@ -585,7 +616,7 @@ var RFB; var deltaY = this._viewportDragPos.y - y; this._viewportDragPos = {'x': x, 'y': y}; - this._display.viewportChange(deltaX, deltaY); + this._display.viewportChangePos(deltaX, deltaY); // Skip sending mouse events return; @@ -944,8 +975,8 @@ var RFB; } this._display.set_true_color(this._true_color); - this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); this._keyboard.grab(); this._mouse.grab(); @@ -1839,12 +1870,52 @@ var RFB; return true; }, + ext_desktop_size: function () { + this._FBU.bytes = 1; + if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + + this._supportsSetDesktopSize = true; + var number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (var i=0; i> set_desktopsize"); this._fb_width = this._FBU.width; this._fb_height = this._FBU.height; - this._onFBResize(this, this._fb_width, this._fb_height); this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); this._FBU.bytes = 0; From a2158362d9326e1c2b1c073b9a752634c7d23d15 Mon Sep 17 00:00:00 2001 From: Jeremy Hanmer Date: Fri, 6 Feb 2015 14:24:47 -0800 Subject: [PATCH 050/527] keysym.js needs inclusion in debian's install list --- debian/novnc.install | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/novnc.install b/debian/novnc.install index ac51e3c6..54cb49ec 100644 --- a/debian/novnc.install +++ b/debian/novnc.install @@ -14,6 +14,7 @@ include/base64.js /usr/share/novnc/include include/des.js /usr/share/novnc/include include/display.js /usr/share/novnc/include include/keysymdef.js /usr/share/novnc/include +include/keysym.js /usr/share/novnc/include include/keyboard.js /usr/share/novnc/include include/input.js /usr/share/novnc/include include/logo.js /usr/share/novnc/include From f8b399d7dfb180f2e173b3cc567a4ad543dda655 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 10 Feb 2015 17:05:58 +0100 Subject: [PATCH 051/527] Support automatic resize [Part 3/4]: ui.js * Added a resize request (setDesktopSize) triggered when connecting and by changes to the browser window's size. * Hid the view-drag-hand when the display area is the same or smaller than the remote session size. * Added a setting for the automatic resize feature. * Updated vnc.html and vnc_auto.html to reflect the changes to the UI. --- include/ui.js | 74 ++++++++++++++++++++++++++++++++++++++++++--------- vnc.html | 1 + vnc_auto.html | 29 +++++++++++++++++++- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/include/ui.js b/include/ui.js index 4132d050..8e13fee0 100644 --- a/include/ui.js +++ b/include/ui.js @@ -15,6 +15,8 @@ var UI; (function () { "use strict"; + var resizeTimeout; + // Load supporting scripts window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", @@ -43,6 +45,19 @@ var UI; WebUtil.initSettings(UI.start, callback); }, + onresize: function (callback) { + if (UI.getSetting('resize')) { + var innerW = window.innerWidth; + var innerH = window.innerHeight; + var controlbarH = $D('noVNC-control-bar').offsetHeight; + // For some unknown reason the container is higher than the canvas, + // 5px higher in Firefox and 4px higher in Chrome + var padding = 5; + if (innerW !== undefined && innerH !== undefined) + UI.rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + } + }, + // Render default UI and initialize settings menu start: function(callback) { UI.isTouchDevice = 'ontouchstart' in document.documentElement; @@ -89,6 +104,7 @@ var UI; UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('true_color', true); UI.initSetting('cursor', !UI.isTouchDevice); + UI.initSetting('resize', false); UI.initSetting('shared', true); UI.initSetting('view_only', false); UI.initSetting('path', 'websockify'); @@ -98,6 +114,8 @@ var UI; 'onUpdateState': UI.updateState, 'onXvpInit': UI.updateXvpVisualState, 'onClipboard': UI.clipReceive, + 'onFBUComplete': UI.FBUComplete, + 'onFBResize': UI.updateViewDragButton, 'onDesktopName': UI.updateDocumentTitle}); var autoconnect = WebUtil.getQueryVar('autoconnect', false); @@ -118,7 +136,6 @@ var UI; // Remove the address bar setTimeout(function() { window.scrollTo(0, 1); }, 100); UI.forceSetting('clip', true); - $D('noVNC_clip').disabled = true; } else { UI.initSetting('clip', false); } @@ -136,7 +153,17 @@ var UI; $D('noVNC_host').focus(); UI.setViewClip(); - Util.addEvent(window, 'resize', UI.setViewClip); + + Util.addEvent(window, 'resize', function () { + UI.setViewClip(); + // When the window has been resized, wait until the size remains + // the same for 0.5 seconds before sending the request for changing + // the resolution of the session + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function(){ + UI.onresize(); + }, 500); + } ); Util.addEvent(window, 'load', UI.keyboardinputReset); @@ -212,7 +239,7 @@ var UI; getSetting: function(name) { var ctrl = $D('noVNC_' + name); var val = WebUtil.readSetting(name); - if (val !== null && ctrl.type === 'checkbox') { + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { val = false; } else { @@ -427,6 +454,7 @@ var UI; $D('noVNC_cursor').disabled = true; } UI.updateSetting('clip'); + UI.updateSetting('resize'); UI.updateSetting('shared'); UI.updateSetting('view_only'); UI.updateSetting('path'); @@ -479,6 +507,7 @@ var UI; UI.saveSetting('cursor'); } UI.saveSetting('clip'); + UI.saveSetting('resize'); UI.saveSetting('shared'); UI.saveSetting('view_only'); UI.saveSetting('path'); @@ -595,6 +624,8 @@ var UI; UI.updateSetting('cursor', !UI.isTouchDevice); $D('noVNC_cursor').disabled = true; } + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + $D('noVNC_resize').disabled = connected; $D('noVNC_shared').disabled = connected; $D('noVNC_view_only').disabled = connected; $D('noVNC_path').disabled = connected; @@ -650,6 +681,16 @@ var UI; } }, + // This resize can not be done until we know from the first Frame Buffer Update + // if it is supported or not. + // The resize is needed to make sure the server desktop size is updated to the + // corresponding size of the current local window when reconnecting to an + // existing session. + FBUComplete: function(rfb, fbu) { + UI.onresize(); + UI.rfb.set_onFBUComplete(function() { }); + }, + // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; @@ -691,6 +732,9 @@ var UI; UI.closeSettingsMenu(); UI.rfb.disconnect(); + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.FBUComplete); + $D('noVNC_logo').style.display = "block"; UI.connSettingsOpen = false; UI.toggleConnectPanel(); @@ -742,7 +786,7 @@ var UI; UI.updateSetting('clip', false); display.set_viewport(false); $D('noVNC_canvas').style.position = 'static'; - display.viewportChange(); + display.viewportChangeSize(); } if (UI.getSetting('clip')) { // If clipping, update clipping settings @@ -751,27 +795,22 @@ var UI; var new_w = window.innerWidth - pos.x; var new_h = window.innerHeight - pos.y; display.set_viewport(true); - display.viewportChange(0, 0, new_w, new_h); + display.viewportChangeSize(new_w, new_h); } }, // Toggle/set/unset the viewport drag/move button setViewDrag: function(drag) { - var vmb = $D('noVNC_view_drag_button'); if (!UI.rfb) { return; } - if (UI.rfb_state === 'normal' && - UI.rfb.get_display().get_viewport()) { - vmb.style.display = "inline"; - } else { - vmb.style.display = "none"; - } + UI.updateViewDragButton(); if (typeof(drag) === "undefined" || typeof(drag) === "object") { // If not specified, then toggle drag = !UI.rfb.get_viewportDrag(); } + var vmb = $D('noVNC_view_drag_button'); if (drag) { vmb.className = "noVNC_status_button_selected"; UI.rfb.set_viewportDrag(true); @@ -781,6 +820,17 @@ var UI; } }, + updateViewDragButton: function() { + var vmb = $D('noVNC_view_drag_button'); + if (UI.rfb_state === 'normal' && + UI.rfb.get_display().get_viewport() && + UI.rfb.get_display().fbuClip()) { + vmb.style.display = "inline"; + } else { + vmb.style.display = "none"; + } + }, + // On touch devices, show the OS keyboard showKeyboard: function() { var kbi = $D('keyboardinput'); diff --git a/vnc.html b/vnc.html index adb01576..7cc07cf9 100644 --- a/vnc.html +++ b/vnc.html @@ -157,6 +157,7 @@
  • True Color
  • Local Cursor
  • Clip to Window
  • +
  • Resize Remote to Window
  • Shared Mode
  • View Only
  • Path
  • diff --git a/vnc_auto.html b/vnc_auto.html index b05024e3..9fd2272a 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -80,7 +80,23 @@ "jsunzip.js", "rfb.js", "keysym.js"]); var rfb; + var resizeTimeout; + + function UIresize() { + if (WebUtil.getQueryVar('resize', false)) { + var innerW = window.innerWidth; + var innerH = window.innerHeight; + var controlbarH = $D('noVNC_status_bar').offsetHeight; + var padding = 5; + if (innerW !== undefined && innerH !== undefined) + rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + } + } + function FBUComplete(rfb, fbu) { + UIresize(); + rfb.set_onFBUComplete(function() { }); + } function passwordRequired(rfb) { var msg; msg = '
    From 4dec490aae2a8ab82f287ee2f7adc4ff6dec3447 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 10 Feb 2015 17:06:55 +0100 Subject: [PATCH 052/527] Support automatic resize [Part 4/4]: unit tests * Added new tests for the setDesktopSize encoding * Added new tests for the ExtendedDesktopSize encoding --- tests/test.rfb.js | 158 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 2ac8a129..444e42c4 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -195,6 +195,48 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); + describe("#setDesktopSize", function () { + beforeEach(function() { + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'send'); + client._rfb_state = "normal"; + client._view_only = false; + client._supportsSetDesktopSize = true; + }); + + it('should send the request with the given width and height', function () { + var expected = [251]; + expected.push8(0); // padding + expected.push16(1); // width + expected.push16(2); // height + expected.push8(1); // number-of-screens + expected.push8(0); // padding before screen array + expected.push32(0); // id + expected.push16(0); // x-position + expected.push16(0); // y-position + expected.push16(1); // width + expected.push16(2); // height + expected.push32(0); // flags + + client.setDesktopSize(1, 2); + expect(client._sock).to.have.sent(expected); + }); + + it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { + client._supportsSetDesktopSize = false; + client.setDesktopSize(1,2); + expect(client._sock.send).to.not.have.been.called; + }); + + it('should not send the request if we are not in a normal state', function () { + client._rfb_state = "broken"; + client.setDesktopSize(1,2); + expect(client._sock.send).to.not.have.been.called; + }); + }); + describe("XVP operations", function () { beforeEach(function () { client._sock = new Websock(); @@ -1443,6 +1485,122 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._display.resize).to.have.been.calledWith(20, 50); }); + describe('the ExtendedDesktopSize pseudo-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._supportsSetDesktopSize = false; + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display._fb_width = 4; + client._display._fb_height = 4; + client._display._viewportLoc.w = 4; + client._display._viewportLoc.h = 4; + client._fb_Bpp = 4; + sinon.spy(client._display, 'resize'); + client.set_onFBResize(sinon.spy()); + }); + + function make_screen_data (nr_of_screens) { + var data = []; + data.push8(nr_of_screens); // number-of-screens + data.push8(0); // padding + data.push16(0); // padding + for (var i=0; i Date: Mon, 16 Feb 2015 17:03:17 -0500 Subject: [PATCH 053/527] Remove local copies of websockify This commit removes local copies of websockify. Instead `utils/launch.sh` performs the following logic: If `utils/websockify` exists, use `utils/websockify/run` (if the latter does not exist, or is not executable, fail, since this is probably a mistake). Otherwise, check to see if websockify is installed somewhere (try `which websockify`). If it is, use that. Otherwise, clone websockify from github, and tell git to ignore that directory. Packaged versions of noVNC should simply list websockify as a requirement. The debian packaging has been updated to reflect this. Closes #433 --- debian/control | 2 +- utils/launch.sh | 36 +- utils/nova-novncproxy | 152 ------ utils/rebind | 18 - utils/rebind.c | 94 ---- utils/websocket.py | 1030 ----------------------------------------- utils/websockify.py | 1 - utils/wsproxy.py | 1 - 8 files changed, 35 insertions(+), 1299 deletions(-) delete mode 100755 utils/nova-novncproxy delete mode 100755 utils/rebind delete mode 100644 utils/rebind.c delete mode 100644 utils/websocket.py delete mode 120000 utils/websockify.py delete mode 120000 utils/wsproxy.py diff --git a/debian/control b/debian/control index f7b92d54..0f79f889 100644 --- a/debian/control +++ b/debian/control @@ -8,6 +8,6 @@ Homepage: https://github.com/kanaka/noVNC/ Package: novnc Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, python (>= 2.4) +Depends: ${shlibs:Depends}, ${misc:Depends}, python (>= 2.4), websockify Description: HTML5 VNC client VNC client using HTML5 (WebSockets, Canvas) with encryption (wss://) support. diff --git a/utils/launch.sh b/utils/launch.sh index 1581f17a..1492bf99 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -9,7 +9,7 @@ usage() { echo echo "Starts the WebSockets proxy and a mini-webserver and " echo "provides a cut-and-paste URL to go to." - echo + echo echo " --listen PORT Port for proxy/webserver to listen on" echo " Default: 6080" echo " --vnc VNC_HOST:PORT VNC server host:port proxy target" @@ -101,8 +101,40 @@ else echo "Warning: could not find self.pem" fi +# try to find websockify (prefer local, try global, then download local) +if [[ -e ${HERE}/websockify ]]; then + WEBSOCKIFY=${HERE}/websockify/run + + if [[ ! -x $WEBSOCKIFY ]]; then + echo "The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable." + echo "If you inteded to use an installed websockify package, please remove ${HERE}/websockify." + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" +else + WEBSOCKIFY=$(which websockify 2>/dev/null) + + if [[ $? -ne 0 ]]; then + echo "No installed websockify, attempting to clone websockify..." + WEBSOCKIFY=${HERE}/websockify/run + git clone https://github.com/kanaka/websockify + git update-index --assume-unchanged websockify + + if [[ ! -e $WEBSOCKIFY ]]; then + echo "Unable to locate ${HERE}/websockify/run after downloading" + exit 1 + fi + + echo "Using local websockify at $WEBSOCKIFY" + else + echo "Using installed websockify at $WEBSOCKIFY" + fi +fi + echo "Starting webserver and WebSockets proxy on port ${PORT}" -${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +#${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +${WEBSOCKIFY} --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & proxy_pid="$!" sleep 1 if ! ps -p ${proxy_pid} >/dev/null; then diff --git a/utils/nova-novncproxy b/utils/nova-novncproxy deleted file mode 100755 index 7e5afbd0..00000000 --- a/utils/nova-novncproxy +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 Openstack, LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -#!/usr/bin/env python - -''' -Websocket proxy that is compatible with Openstack Nova. -Leverages websockify by Joel Martin -''' - -import Cookie -from oslo.config import cfg -import socket -import sys - -import websockify - -from nova import config -from nova import context -from nova import utils -from nova.openstack.common import rpc - - -opts = [ - cfg.BoolOpt('record', - default=False, - help='Record sessions to FILE.[session_number]'), - cfg.BoolOpt('daemon', - default=False, - help='Become a daemon (background process)'), - cfg.BoolOpt('ssl_only', - default=False, - help='Disallow non-encrypted connections'), - cfg.BoolOpt('source_is_ipv6', - default=False, - help='Source is ipv6'), - cfg.StrOpt('cert', - default='self.pem', - help='SSL certificate file'), - cfg.StrOpt('key', - default=None, - help='SSL key file (if separate from cert)'), - cfg.StrOpt('web', - default='.', - help='Run webserver on same port. Serve files from DIR.'), - cfg.StrOpt('novncproxy_host', - default='0.0.0.0', - help='Host on which to listen for incoming requests'), - cfg.IntOpt('novncproxy_port', - default=6080, - help='Port on which to listen for incoming requests'), - ] -CONF = cfg.CONF -CONF.register_cli_opts(opts) - -# As of nova commit 0b11668e64450039dc071a4a123abd02206f865f we must -# manually register the rpc library -if hasattr(rpc, 'register_opts'): - rpc.register_opts(CONF) - - -class NovaWebSocketProxy(websockify.WebSocketProxy): - def __init__(self, *args, **kwargs): - websockify.WebSocketProxy.__init__(self, *args, **kwargs) - - def new_client(self): - """ - Called after a new WebSocket connection has been established. - """ - cookie = Cookie.SimpleCookie() - cookie.load(self.headers.getheader('cookie')) - token = cookie['token'].value - ctxt = context.get_admin_context() - connect_info = rpc.call(ctxt, 'consoleauth', - {'method': 'check_token', - 'args': {'token': token}}) - - if not connect_info: - raise Exception("Invalid Token") - - host = connect_info['host'] - port = int(connect_info['port']) - - # Connect to the target - self.msg("connecting to: %s:%s" % ( - host, port)) - tsock = self.socket(host, port, - connect=True) - - # Handshake as necessary - if connect_info.get('internal_access_path'): - tsock.send("CONNECT %s HTTP/1.1\r\n\r\n" % - connect_info['internal_access_path']) - while True: - data = tsock.recv(4096, socket.MSG_PEEK) - if data.find("\r\n\r\n") != -1: - if not data.split("\r\n")[0].find("200"): - raise Exception("Invalid Connection Info") - tsock.recv(len(data)) - break - - if self.verbose and not self.daemon: - print(self.traffic_legend) - - # Start proxying - try: - self.do_proxy(tsock) - except: - if tsock: - tsock.shutdown(socket.SHUT_RDWR) - tsock.close() - self.vmsg("%s:%s: Target closed" % (host, port)) - raise - - -if __name__ == '__main__': - if CONF.ssl_only and not os.path.exists(CONF.cert): - parser.error("SSL only and %s not found" % CONF.cert) - - # Setup flags - config.parse_args(sys.argv) - - # Create and start the NovaWebSockets proxy - server = NovaWebSocketProxy(listen_host=CONF.novncproxy_host, - listen_port=CONF.novncproxy_port, - source_is_ipv6=CONF.source_is_ipv6, - verbose=CONF.verbose, - cert=CONF.cert, - key=CONF.key, - ssl_only=CONF.ssl_only, - daemon=CONF.daemon, - record=CONF.record, - web=CONF.web, - target_host='ignore', - target_port='ignore', - wrap_mode='exit', - wrap_cmd=None) - server.start_server() diff --git a/utils/rebind b/utils/rebind deleted file mode 100755 index 2289aaa4..00000000 --- a/utils/rebind +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -usage() { - echo "Usage: $(basename $0) OLD_PORT NEW_PORT COMMAND_LINE" - echo - echo "Launch COMMAND_LINE, but intercept system calls to bind" - echo "to OLD_PORT and instead bind them to localhost:NEW_PORT" - exit 2 -} - -# Parameter defaults -mydir=$(readlink -f $(dirname ${0})) - -export REBIND_PORT_OLD="${1}"; shift -export REBIND_PORT_NEW="${1}"; shift - -LD_PRELOAD=${mydir}/rebind.so "${@}" - diff --git a/utils/rebind.c b/utils/rebind.c deleted file mode 100644 index 69b9ff9e..00000000 --- a/utils/rebind.c +++ /dev/null @@ -1,94 +0,0 @@ -/* - * rebind: Intercept bind calls and bind to a different port - * Copyright 2010 Joel Martin - * Licensed under MPL-2.0 (see docs/LICENSE.MPL-2.0) - * - * Overload (LD_PRELOAD) bind system call. If REBIND_PORT_OLD and - * REBIND_PORT_NEW environment variables are set then bind on the new - * port (of localhost) instead of the old port. - * - * This allows a bridge/proxy (such as websockify) to run on the old port and - * translate traffic to/from the new port. - * - * Usage: - * LD_PRELOAD=./rebind.so \ - * REBIND_PORT_OLD=23 \ - * REBIND_PORT_NEW=2023 \ - * program - */ - -//#define DO_DEBUG 1 - -#include -#include - -#define __USE_GNU 1 // Pull in RTLD_NEXT -#include - -#include -#include - - -#if defined(DO_DEBUG) -#define DEBUG(...) \ - fprintf(stderr, "wswrapper: "); \ - fprintf(stderr, __VA_ARGS__); -#else -#define DEBUG(...) -#endif - - -int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) -{ - static void * (*func)(); - int do_move = 0; - struct sockaddr_in * addr_in = (struct sockaddr_in *)addr; - struct sockaddr_in addr_tmp; - socklen_t addrlen_tmp; - char * PORT_OLD, * PORT_NEW, * end1, * end2; - int ret, oldport, newport, askport = htons(addr_in->sin_port); - uint32_t askaddr = htons(addr_in->sin_addr.s_addr); - if (!func) func = (void *(*)()) dlsym(RTLD_NEXT, "bind"); - - DEBUG(">> bind(%d, _, %d), askaddr %d, askport %d\n", - sockfd, addrlen, askaddr, askport); - - /* Determine if we should move this socket */ - if (addr_in->sin_family == AF_INET) { - // TODO: support IPv6 - PORT_OLD = getenv("REBIND_OLD_PORT"); - PORT_NEW = getenv("REBIND_NEW_PORT"); - if (PORT_OLD && (*PORT_OLD != '\0') && - PORT_NEW && (*PORT_NEW != '\0')) { - oldport = strtol(PORT_OLD, &end1, 10); - newport = strtol(PORT_NEW, &end2, 10); - if (oldport && (*end1 == '\0') && - newport && (*end2 == '\0') && - (oldport == askport)) { - do_move = 1; - } - } - } - - if (! do_move) { - /* Just pass everything right through to the real bind */ - ret = (int) func(sockfd, addr, addrlen); - DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); - return ret; - } - - DEBUG("binding fd %d on localhost:%d instead of 0x%x:%d\n", - sockfd, newport, ntohl(addr_in->sin_addr.s_addr), oldport); - - /* Use a temporary location for the new address information */ - addrlen_tmp = sizeof(addr_tmp); - memcpy(&addr_tmp, addr, addrlen_tmp); - - /* Bind to other port on the loopback instead */ - addr_tmp.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - addr_tmp.sin_port = htons(newport); - ret = (int) func(sockfd, &addr_tmp, addrlen_tmp); - - DEBUG("<< bind(%d, _, %d) ret %d\n", sockfd, addrlen, ret); - return ret; -} diff --git a/utils/websocket.py b/utils/websocket.py deleted file mode 100644 index 67f5aef6..00000000 --- a/utils/websocket.py +++ /dev/null @@ -1,1030 +0,0 @@ -#!/usr/bin/env python - -''' -Python WebSocket library with support for "wss://" encryption. -Copyright 2011 Joel Martin -Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) - -Supports following protocol versions: - - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 - - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 - - http://tools.ietf.org/html/rfc6455 - -You can make a cert/key with openssl using: -openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem -as taken from http://docs.python.org/dev/library/ssl.html#certificates - -''' - -import os, sys, time, errno, signal, socket, select, logging -import array, struct -from base64 import b64encode, b64decode - -# Imports that vary by python version - -# python 3.0 differences -if sys.hexversion > 0x3000000: - b2s = lambda buf: buf.decode('latin_1') - s2b = lambda s: s.encode('latin_1') - s2a = lambda s: s -else: - b2s = lambda buf: buf # No-op - s2b = lambda s: s # No-op - s2a = lambda s: [ord(c) for c in s] -try: from io import StringIO -except: from cStringIO import StringIO -try: from http.server import SimpleHTTPRequestHandler -except: from SimpleHTTPServer import SimpleHTTPRequestHandler - -# python 2.6 differences -try: from hashlib import sha1 -except: from sha import sha as sha1 - -# python 2.5 differences -try: - from struct import pack, unpack_from -except: - from struct import pack - def unpack_from(fmt, buf, offset=0): - slice = buffer(buf, offset, struct.calcsize(fmt)) - return struct.unpack(fmt, slice) - -# Degraded functionality if these imports are missing -for mod, msg in [('numpy', 'HyBi protocol will be slower'), - ('ssl', 'TLS/SSL/wss is disabled'), - ('multiprocessing', 'Multi-Processing is disabled'), - ('resource', 'daemonizing is disabled')]: - try: - globals()[mod] = __import__(mod) - except ImportError: - globals()[mod] = None - print("WARNING: no '%s' module, %s" % (mod, msg)) - -if multiprocessing and sys.platform == 'win32': - # make sockets pickle-able/inheritable - import multiprocessing.reduction - - -# HTTP handler with WebSocket upgrade support -class WebSocketRequestHandler(SimpleHTTPRequestHandler): - """ - WebSocket Request Handler Class, derived from SimpleHTTPRequestHandler. - Must be sub-classed with new_websocket_client method definition. - The request handler can be configured by setting optional - attributes on the server object: - - * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled, - only websocket is allowed. - * verbose: If true, verbose logging is activated. - * daemon: Running as daemon, do not write to console etc - * record: Record raw frame data as JavaScript array into specified filename - * run_once: Handle a single request - * handler_id: A sequence number for this connection, appended to record filename - """ - buffer_size = 65536 - - GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - server_version = "WebSockify" - - protocol_version = "HTTP/1.1" - - # An exception while the WebSocket client was connected - class CClose(Exception): - pass - - def __init__(self, req, addr, server): - # Retrieve a few configuration variables from the server - self.only_upgrade = getattr(server, "only_upgrade", False) - self.verbose = getattr(server, "verbose", False) - self.daemon = getattr(server, "daemon", False) - self.record = getattr(server, "record", False) - self.run_once = getattr(server, "run_once", False) - self.rec = None - self.handler_id = getattr(server, "handler_id", False) - self.file_only = getattr(server, "file_only", False) - self.traffic = getattr(server, "traffic", False) - - self.logger = getattr(server, "logger", None) - if self.logger is None: - self.logger = WebSocketServer.get_logger() - - SimpleHTTPRequestHandler.__init__(self, req, addr, server) - - @staticmethod - def unmask(buf, hlen, plen): - pstart = hlen + 4 - pend = pstart + plen - if numpy: - b = c = s2b('') - if plen >= 4: - mask = numpy.frombuffer(buf, dtype=numpy.dtype('BB', b1, payload_len) - elif payload_len > 125 and payload_len < 65536: - header = pack('>BBH', b1, 126, payload_len) - elif payload_len >= 65536: - header = pack('>BBQ', b1, 127, payload_len) - - #self.msg("Encoded: %s", repr(header + buf)) - - return header + buf, len(header), 0 - - @staticmethod - def decode_hybi(buf, base64=False, logger=None): - """ Decode HyBi style WebSocket packets. - Returns: - {'fin' : 0_or_1, - 'opcode' : number, - 'masked' : boolean, - 'hlen' : header_bytes_number, - 'length' : payload_bytes_number, - 'payload' : decoded_buffer, - 'left' : bytes_left_number, - 'close_code' : number, - 'close_reason' : string} - """ - - f = {'fin' : 0, - 'opcode' : 0, - 'masked' : False, - 'hlen' : 2, - 'length' : 0, - 'payload' : None, - 'left' : 0, - 'close_code' : 1000, - 'close_reason' : ''} - - if logger is None: - logger = WebSocketServer.get_logger() - - blen = len(buf) - f['left'] = blen - - if blen < f['hlen']: - return f # Incomplete frame header - - b1, b2 = unpack_from(">BB", buf) - f['opcode'] = b1 & 0x0f - f['fin'] = (b1 & 0x80) >> 7 - f['masked'] = (b2 & 0x80) >> 7 - - f['length'] = b2 & 0x7f - - if f['length'] == 126: - f['hlen'] = 4 - if blen < f['hlen']: - return f # Incomplete frame header - (f['length'],) = unpack_from('>xxH', buf) - elif f['length'] == 127: - f['hlen'] = 10 - if blen < f['hlen']: - return f # Incomplete frame header - (f['length'],) = unpack_from('>xxQ', buf) - - full_len = f['hlen'] + f['masked'] * 4 + f['length'] - - if blen < full_len: # Incomplete frame - return f # Incomplete frame header - - # Number of bytes that are part of the next frame(s) - f['left'] = blen - full_len - - # Process 1 frame - if f['masked']: - # unmask payload - f['payload'] = WebSocketRequestHandler.unmask(buf, f['hlen'], - f['length']) - else: - logger.debug("Unmasked frame: %s" % repr(buf)) - f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len] - - if base64 and f['opcode'] in [1, 2]: - try: - f['payload'] = b64decode(f['payload']) - except: - logger.exception("Exception while b64decoding buffer: %s" % - (repr(buf))) - raise - - if f['opcode'] == 0x08: - if f['length'] >= 2: - f['close_code'] = unpack_from(">H", f['payload'])[0] - if f['length'] > 3: - f['close_reason'] = f['payload'][2:] - - return f - - - # - # WebSocketRequestHandler logging/output functions - # - - def print_traffic(self, token="."): - """ Show traffic flow mode. """ - if self.traffic: - sys.stdout.write(token) - sys.stdout.flush() - - def msg(self, msg, *args, **kwargs): - """ Output message with handler_id prefix. """ - prefix = "% 3d: " % self.handler_id - self.logger.log(logging.INFO, "%s%s" % (prefix, msg), *args, **kwargs) - - def vmsg(self, msg, *args, **kwargs): - """ Same as msg() but as debug. """ - prefix = "% 3d: " % self.handler_id - self.logger.log(logging.DEBUG, "%s%s" % (prefix, msg), *args, **kwargs) - - def warn(self, msg, *args, **kwargs): - """ Same as msg() but as warning. """ - prefix = "% 3d: " % self.handler_id - self.logger.log(logging.WARN, "%s%s" % (prefix, msg), *args, **kwargs) - - # - # Main WebSocketRequestHandler methods - # - def send_frames(self, bufs=None): - """ Encode and send WebSocket frames. Any frames already - queued will be sent first. If buf is not set then only queued - frames will be sent. Returns the number of pending frames that - could not be fully sent. If returned pending frames is greater - than 0, then the caller should call again when the socket is - ready. """ - - tdelta = int(time.time()*1000) - self.start_time - - if bufs: - for buf in bufs: - if self.base64: - encbuf, lenhead, lentail = self.encode_hybi(buf, opcode=1, base64=True) - else: - encbuf, lenhead, lentail = self.encode_hybi(buf, opcode=2, base64=False) - - if self.rec: - self.rec.write("%s,\n" % - repr("{%s{" % tdelta - + encbuf[lenhead:len(encbuf)-lentail])) - - self.send_parts.append(encbuf) - - while self.send_parts: - # Send pending frames - buf = self.send_parts.pop(0) - sent = self.request.send(buf) - - if sent == len(buf): - self.print_traffic("<") - else: - self.print_traffic("<.") - self.send_parts.insert(0, buf[sent:]) - break - - return len(self.send_parts) - - def recv_frames(self): - """ Receive and decode WebSocket frames. - - Returns: - (bufs_list, closed_string) - """ - - closed = False - bufs = [] - tdelta = int(time.time()*1000) - self.start_time - - buf = self.request.recv(self.buffer_size) - if len(buf) == 0: - closed = {'code': 1000, 'reason': "Client closed abruptly"} - return bufs, closed - - if self.recv_part: - # Add partially received frames to current read buffer - buf = self.recv_part + buf - self.recv_part = None - - while buf: - frame = self.decode_hybi(buf, base64=self.base64, - logger=self.logger) - #self.msg("Received buf: %s, frame: %s", repr(buf), frame) - - if frame['payload'] == None: - # Incomplete/partial frame - self.print_traffic("}.") - if frame['left'] > 0: - self.recv_part = buf[-frame['left']:] - break - else: - if frame['opcode'] == 0x8: # connection close - closed = {'code': frame['close_code'], - 'reason': frame['close_reason']} - break - - self.print_traffic("}") - - if self.rec: - start = frame['hlen'] - end = frame['hlen'] + frame['length'] - if frame['masked']: - recbuf = WebSocketRequestHandler.unmask(buf, frame['hlen'], - frame['length']) - else: - recbuf = buf[frame['hlen']:frame['hlen'] + - frame['length']] - self.rec.write("%s,\n" % - repr("}%s}" % tdelta + recbuf)) - - - bufs.append(frame['payload']) - - if frame['left']: - buf = buf[-frame['left']:] - else: - buf = '' - - return bufs, closed - - def send_close(self, code=1000, reason=''): - """ Send a WebSocket orderly close frame. """ - - msg = pack(">H%ds" % len(reason), code, reason) - buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False) - self.request.send(buf) - - def do_websocket_handshake(self): - h = self.headers - - prot = 'WebSocket-Protocol' - protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',') - - ver = h.get('Sec-WebSocket-Version') - if ver: - # HyBi/IETF version of the protocol - - # HyBi-07 report version 7 - # HyBi-08 - HyBi-12 report version 8 - # HyBi-13 reports version 13 - if ver in ['7', '8', '13']: - self.version = "hybi-%02d" % int(ver) - else: - self.send_error(400, "Unsupported protocol version %s" % ver) - return False - - key = h['Sec-WebSocket-Key'] - - # Choose binary if client supports it - if 'binary' in protocols: - self.base64 = False - elif 'base64' in protocols: - self.base64 = True - else: - self.send_error(400, "Client must support 'binary' or 'base64' protocol") - return False - - # Generate the hash value for the accept header - accept = b64encode(sha1(s2b(key + self.GUID)).digest()) - - self.send_response(101, "Switching Protocols") - self.send_header("Upgrade", "websocket") - self.send_header("Connection", "Upgrade") - self.send_header("Sec-WebSocket-Accept", b2s(accept)) - if self.base64: - self.send_header("Sec-WebSocket-Protocol", "base64") - else: - self.send_header("Sec-WebSocket-Protocol", "binary") - self.end_headers() - return True - else: - self.send_error(400, "Missing Sec-WebSocket-Version header. Hixie protocols not supported.") - - return False - - def handle_websocket(self): - """Upgrade a connection to Websocket, if requested. If this succeeds, - new_websocket_client() will be called. Otherwise, False is returned. - """ - if (self.headers.get('upgrade') and - self.headers.get('upgrade').lower() == 'websocket'): - - if not self.do_websocket_handshake(): - return False - - # Indicate to server that a Websocket upgrade was done - self.server.ws_connection = True - # Initialize per client settings - self.send_parts = [] - self.recv_part = None - self.start_time = int(time.time()*1000) - - # client_address is empty with, say, UNIX domain sockets - client_addr = "" - is_ssl = False - try: - client_addr = self.client_address[0] - is_ssl = self.client_address[2] - except IndexError: - pass - - if is_ssl: - self.stype = "SSL/TLS (wss://)" - else: - self.stype = "Plain non-SSL (ws://)" - - self.log_message("%s: %s WebSocket connection", client_addr, - self.stype) - self.log_message("%s: Version %s, base64: '%s'", client_addr, - self.version, self.base64) - if self.path != '/': - self.log_message("%s: Path: '%s'", client_addr, self.path) - - if self.record: - # Record raw frame data as JavaScript array - fname = "%s.%s" % (self.record, - self.handler_id) - self.log_message("opening record file: %s", fname) - self.rec = open(fname, 'w+') - encoding = "binary" - if self.base64: encoding = "base64" - self.rec.write("var VNC_frame_encoding = '%s';\n" - % encoding) - self.rec.write("var VNC_frame_data = [\n") - - try: - self.new_websocket_client() - except self.CClose: - # Close the client - _, exc, _ = sys.exc_info() - self.send_close(exc.args[0], exc.args[1]) - return True - else: - return False - - def do_GET(self): - """Handle GET request. Calls handle_websocket(). If unsuccessful, - and web server is enabled, SimpleHTTPRequestHandler.do_GET will be called.""" - if not self.handle_websocket(): - if self.only_upgrade: - self.send_error(405, "Method Not Allowed") - else: - SimpleHTTPRequestHandler.do_GET(self) - - def list_directory(self, path): - if self.file_only: - self.send_error(404, "No such file") - else: - return SimpleHTTPRequestHandler.list_directory(self, path) - - def new_websocket_client(self): - """ Do something with a WebSockets client connection. """ - raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") - - def do_HEAD(self): - if self.only_upgrade: - self.send_error(405, "Method Not Allowed") - else: - SimpleHTTPRequestHandler.do_HEAD(self) - - def finish(self): - if self.rec: - self.rec.write("'EOF'];\n") - self.rec.close() - - def handle(self): - # When using run_once, we have a single process, so - # we cannot loop in BaseHTTPRequestHandler.handle; we - # must return and handle new connections - if self.run_once: - self.handle_one_request() - else: - SimpleHTTPRequestHandler.handle(self) - - def log_request(self, code='-', size='-'): - if self.verbose: - SimpleHTTPRequestHandler.log_request(self, code, size) - - -class WebSocketServer(object): - """ - WebSockets server class. - As an alternative, the standard library SocketServer can be used - """ - - policy_response = """\n""" - log_prefix = "websocket" - - # An exception before the WebSocket connection was established - class EClose(Exception): - pass - - class Terminate(Exception): - pass - - def __init__(self, RequestHandlerClass, listen_host='', - listen_port=None, source_is_ipv6=False, - verbose=False, cert='', key='', ssl_only=None, - daemon=False, record='', web='', - file_only=False, - run_once=False, timeout=0, idle_timeout=0, traffic=False, - tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None, - tcp_keepintvl=None): - - # settings - self.RequestHandlerClass = RequestHandlerClass - self.verbose = verbose - self.listen_host = listen_host - self.listen_port = listen_port - self.prefer_ipv6 = source_is_ipv6 - self.ssl_only = ssl_only - self.daemon = daemon - self.run_once = run_once - self.timeout = timeout - self.idle_timeout = idle_timeout - self.traffic = traffic - - self.launch_time = time.time() - self.ws_connection = False - self.handler_id = 1 - - self.logger = self.get_logger() - self.tcp_keepalive = tcp_keepalive - self.tcp_keepcnt = tcp_keepcnt - self.tcp_keepidle = tcp_keepidle - self.tcp_keepintvl = tcp_keepintvl - - # Make paths settings absolute - self.cert = os.path.abspath(cert) - self.key = self.web = self.record = '' - if key: - self.key = os.path.abspath(key) - if web: - self.web = os.path.abspath(web) - if record: - self.record = os.path.abspath(record) - - if self.web: - os.chdir(self.web) - self.only_upgrade = not self.web - - # Sanity checks - if not ssl and self.ssl_only: - raise Exception("No 'ssl' module and SSL-only specified") - if self.daemon and not resource: - raise Exception("Module 'resource' required to daemonize") - - # Show configuration - self.msg("WebSocket server settings:") - self.msg(" - Listen on %s:%s", - self.listen_host, self.listen_port) - self.msg(" - Flash security policy server") - if self.web: - self.msg(" - Web server. Web root: %s", self.web) - if ssl: - if os.path.exists(self.cert): - self.msg(" - SSL/TLS support") - if self.ssl_only: - self.msg(" - Deny non-SSL/TLS connections") - else: - self.msg(" - No SSL/TLS support (no cert file)") - else: - self.msg(" - No SSL/TLS support (no 'ssl' module)") - if self.daemon: - self.msg(" - Backgrounding (daemon)") - if self.record: - self.msg(" - Recording to '%s.*'", self.record) - - # - # WebSocketServer static methods - # - - @staticmethod - def get_logger(): - return logging.getLogger("%s.%s" % ( - WebSocketServer.log_prefix, - WebSocketServer.__class__.__name__)) - - @staticmethod - def socket(host, port=None, connect=False, prefer_ipv6=False, - unix_socket=None, use_ssl=False, tcp_keepalive=True, - tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None): - """ Resolve a host (and optional port) to an IPv4 or IPv6 - address. Create a socket. Bind to it if listen is set, - otherwise connect to it. Return the socket. - """ - flags = 0 - if host == '': - host = None - if connect and not (port or unix_socket): - raise Exception("Connect mode requires a port") - if use_ssl and not ssl: - raise Exception("SSL socket requested but Python SSL module not loaded."); - if not connect and use_ssl: - raise Exception("SSL only supported in connect mode (for now)") - if not connect: - flags = flags | socket.AI_PASSIVE - - if not unix_socket: - addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM, - socket.IPPROTO_TCP, flags) - if not addrs: - raise Exception("Could not resolve host '%s'" % host) - addrs.sort(key=lambda x: x[0]) - if prefer_ipv6: - addrs.reverse() - sock = socket.socket(addrs[0][0], addrs[0][1]) - - if tcp_keepalive: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - if tcp_keepcnt: - sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, - tcp_keepcnt) - if tcp_keepidle: - sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, - tcp_keepidle) - if tcp_keepintvl: - sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, - tcp_keepintvl) - - if connect: - sock.connect(addrs[0][4]) - if use_ssl: - sock = ssl.wrap_socket(sock) - else: - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(addrs[0][4]) - sock.listen(100) - else: - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(unix_socket) - - return sock - - @staticmethod - def daemonize(keepfd=None, chdir='/'): - os.umask(0) - if chdir: - os.chdir(chdir) - else: - os.chdir('/') - os.setgid(os.getgid()) # relinquish elevations - os.setuid(os.getuid()) # relinquish elevations - - # Double fork to daemonize - if os.fork() > 0: os._exit(0) # Parent exits - os.setsid() # Obtain new process group - if os.fork() > 0: os._exit(0) # Parent exits - - # Signal handling - signal.signal(signal.SIGTERM, signal.SIG_IGN) - signal.signal(signal.SIGINT, signal.SIG_IGN) - - # Close open files - maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if maxfd == resource.RLIM_INFINITY: maxfd = 256 - for fd in reversed(range(maxfd)): - try: - if fd != keepfd: - os.close(fd) - except OSError: - _, exc, _ = sys.exc_info() - if exc.errno != errno.EBADF: raise - - # Redirect I/O to /dev/null - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) - - def do_handshake(self, sock, address): - """ - do_handshake does the following: - - Peek at the first few bytes from the socket. - - If the connection is Flash policy request then answer it, - close the socket and return. - - If the connection is an HTTPS/SSL/TLS connection then SSL - wrap the socket. - - Read from the (possibly wrapped) socket. - - If we have received a HTTP GET request and the webserver - functionality is enabled, answer it, close the socket and - return. - - Assume we have a WebSockets connection, parse the client - handshake data. - - Send a WebSockets handshake server response. - - Return the socket for this WebSocket client. - """ - ready = select.select([sock], [], [], 3)[0] - - - if not ready: - raise self.EClose("ignoring socket not ready") - # Peek, but do not read the data so that we have a opportunity - # to SSL wrap the socket first - handshake = sock.recv(1024, socket.MSG_PEEK) - #self.msg("Handshake [%s]" % handshake) - - if handshake == "": - raise self.EClose("ignoring empty handshake") - - elif handshake.startswith(s2b("")): - # Answer Flash policy request - handshake = sock.recv(1024) - sock.send(s2b(self.policy_response)) - raise self.EClose("Sending flash policy response") - - elif handshake[0] in ("\x16", "\x80", 22, 128): - # SSL wrap the connection - if not ssl: - raise self.EClose("SSL connection but no 'ssl' module") - if not os.path.exists(self.cert): - raise self.EClose("SSL connection but '%s' not found" - % self.cert) - retsock = None - try: - retsock = ssl.wrap_socket( - sock, - server_side=True, - certfile=self.cert, - keyfile=self.key) - except ssl.SSLError: - _, x, _ = sys.exc_info() - if x.args[0] == ssl.SSL_ERROR_EOF: - if len(x.args) > 1: - raise self.EClose(x.args[1]) - else: - raise self.EClose("Got SSL_ERROR_EOF") - else: - raise - - elif self.ssl_only: - raise self.EClose("non-SSL connection received but disallowed") - - else: - retsock = sock - - # If the address is like (host, port), we are extending it - # with a flag indicating SSL. Not many other options - # available... - if len(address) == 2: - address = (address[0], address[1], (retsock != sock)) - - self.RequestHandlerClass(retsock, address, self) - - # Return the WebSockets socket which may be SSL wrapped - return retsock - - # - # WebSocketServer logging/output functions - # - - def msg(self, *args, **kwargs): - """ Output message as info """ - self.logger.log(logging.INFO, *args, **kwargs) - - def vmsg(self, *args, **kwargs): - """ Same as msg() but as debug. """ - self.logger.log(logging.DEBUG, *args, **kwargs) - - def warn(self, *args, **kwargs): - """ Same as msg() but as warning. """ - self.logger.log(logging.WARN, *args, **kwargs) - - - # - # Events that can/should be overridden in sub-classes - # - def started(self): - """ Called after WebSockets startup """ - self.vmsg("WebSockets server started") - - def poll(self): - """ Run periodically while waiting for connections. """ - #self.vmsg("Running poll()") - pass - - def terminate(self): - raise self.Terminate() - - def multiprocessing_SIGCHLD(self, sig, stack): - self.vmsg('Reaing zombies, active child count is %s', len(multiprocessing.active_children())) - - def fallback_SIGCHLD(self, sig, stack): - # Reap zombies when using os.fork() (python 2.4) - self.vmsg("Got SIGCHLD, reaping zombies") - try: - result = os.waitpid(-1, os.WNOHANG) - while result[0]: - self.vmsg("Reaped child process %s" % result[0]) - result = os.waitpid(-1, os.WNOHANG) - except (OSError): - pass - - def do_SIGINT(self, sig, stack): - self.msg("Got SIGINT, exiting") - self.terminate() - - def do_SIGTERM(self, sig, stack): - self.msg("Got SIGTERM, exiting") - self.terminate() - - def top_new_client(self, startsock, address): - """ Do something with a WebSockets client connection. """ - # handler process - client = None - try: - try: - client = self.do_handshake(startsock, address) - except self.EClose: - _, exc, _ = sys.exc_info() - # Connection was not a WebSockets connection - if exc.args[0]: - self.msg("%s: %s" % (address[0], exc.args[0])) - except WebSocketServer.Terminate: - raise - except Exception: - _, exc, _ = sys.exc_info() - self.msg("handler exception: %s" % str(exc)) - self.vmsg("exception", exc_info=True) - finally: - - if client and client != startsock: - # Close the SSL wrapped socket - # Original socket closed by caller - client.close() - - def start_server(self): - """ - Daemonize if requested. Listen for for connections. Run - do_handshake() method for each connection. If the connection - is a WebSockets client then call new_websocket_client() method (which must - be overridden) for each new client connection. - """ - lsock = self.socket(self.listen_host, self.listen_port, False, - self.prefer_ipv6, - tcp_keepalive=self.tcp_keepalive, - tcp_keepcnt=self.tcp_keepcnt, - tcp_keepidle=self.tcp_keepidle, - tcp_keepintvl=self.tcp_keepintvl) - - if self.daemon: - self.daemonize(keepfd=lsock.fileno(), chdir=self.web) - - self.started() # Some things need to happen after daemonizing - - # Allow override of signals - original_signals = { - signal.SIGINT: signal.getsignal(signal.SIGINT), - signal.SIGTERM: signal.getsignal(signal.SIGTERM), - signal.SIGCHLD: signal.getsignal(signal.SIGCHLD), - } - signal.signal(signal.SIGINT, self.do_SIGINT) - signal.signal(signal.SIGTERM, self.do_SIGTERM) - if not multiprocessing: - # os.fork() (python 2.4) child reaper - signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD) - else: - # make sure that _cleanup is called when children die - # by calling active_children on SIGCHLD - signal.signal(signal.SIGCHLD, self.multiprocessing_SIGCHLD) - - last_active_time = self.launch_time - try: - while True: - try: - try: - startsock = None - pid = err = 0 - child_count = 0 - - if multiprocessing: - # Collect zombie child processes - child_count = len(multiprocessing.active_children()) - - time_elapsed = time.time() - self.launch_time - if self.timeout and time_elapsed > self.timeout: - self.msg('listener exit due to --timeout %s' - % self.timeout) - break - - if self.idle_timeout: - idle_time = 0 - if child_count == 0: - idle_time = time.time() - last_active_time - else: - idle_time = 0 - last_active_time = time.time() - - if idle_time > self.idle_timeout and child_count == 0: - self.msg('listener exit due to --idle-timeout %s' - % self.idle_timeout) - break - - try: - self.poll() - - ready = select.select([lsock], [], [], 1)[0] - if lsock in ready: - startsock, address = lsock.accept() - else: - continue - except self.Terminate: - raise - except Exception: - _, exc, _ = sys.exc_info() - if hasattr(exc, 'errno'): - err = exc.errno - elif hasattr(exc, 'args'): - err = exc.args[0] - else: - err = exc[0] - if err == errno.EINTR: - self.vmsg("Ignoring interrupted syscall") - continue - else: - raise - - if self.run_once: - # Run in same process if run_once - self.top_new_client(startsock, address) - if self.ws_connection : - self.msg('%s: exiting due to --run-once' - % address[0]) - break - elif multiprocessing: - self.vmsg('%s: new handler Process' % address[0]) - p = multiprocessing.Process( - target=self.top_new_client, - args=(startsock, address)) - p.start() - # child will not return - else: - # python 2.4 - self.vmsg('%s: forking handler' % address[0]) - pid = os.fork() - if pid == 0: - # child handler process - self.top_new_client(startsock, address) - break # child process exits - - # parent process - self.handler_id += 1 - - except (self.Terminate, SystemExit, KeyboardInterrupt): - self.msg("In exit") - break - except Exception: - self.msg("handler exception: %s", str(exc)) - self.vmsg("exception", exc_info=True) - - finally: - if startsock: - startsock.close() - finally: - # Close listen port - self.vmsg("Closing socket listening at %s:%s", - self.listen_host, self.listen_port) - lsock.close() - - # Restore signals - for sig, func in original_signals.items(): - signal.signal(sig, func) - - diff --git a/utils/websockify.py b/utils/websockify.py deleted file mode 120000 index 05b5af45..00000000 --- a/utils/websockify.py +++ /dev/null @@ -1 +0,0 @@ -websockify \ No newline at end of file diff --git a/utils/wsproxy.py b/utils/wsproxy.py deleted file mode 120000 index 05b5af45..00000000 --- a/utils/wsproxy.py +++ /dev/null @@ -1 +0,0 @@ -websockify \ No newline at end of file From 4e534a804ea52502374daeb03d0f610d818f4355 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 16 Feb 2015 17:13:00 -0500 Subject: [PATCH 054/527] Update package.json to specify version 0.5.1 The current noVNC release is 0.5.1, but package.json still says version 0.5.0. This fixes that. Fixes #441 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 63c26662..ddd2558a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "noVNC", - "version": "0.5.0", + "version": "0.5.1", "description": "An HTML5 VNC client", "main": "karma.conf.js", "directories": { From 1188993c0fed7d617c5dceb47402b13b0c0bdbff Mon Sep 17 00:00:00 2001 From: "Jesper Dam (jalfd)" Date: Sat, 31 Jan 2015 12:49:29 +0100 Subject: [PATCH 055/527] Update tests/input.html to include keysym.js instead of keysymdef.js --- tests/input.html | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/input.html b/tests/input.html index bc5d2e15..8416379b 100644 --- a/tests/input.html +++ b/tests/input.html @@ -26,6 +26,7 @@ + From 5cd6de495f25b6f7edf6097fa12f8d6f5385cc61 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 17 Feb 2015 17:45:57 -0500 Subject: [PATCH 056/527] Actually remove the "websockify" file Somehow, `utils/websockify` itself manage to sneak back in to 6f5148648bd0e0ffa7ad00474c230f5f1ff4e39f. This actually removes it. --- utils/websockify | 471 ----------------------------------------------- 1 file changed, 471 deletions(-) delete mode 100755 utils/websockify diff --git a/utils/websockify b/utils/websockify deleted file mode 100755 index 7b3ec111..00000000 --- a/utils/websockify +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/env python - -''' -A WebSocket to TCP socket proxy with support for "wss://" encryption. -Copyright 2011 Joel Martin -Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) - -You can make a cert/key with openssl using: -openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem -as taken from http://docs.python.org/dev/library/ssl.html#certificates - -''' - -import signal, socket, optparse, time, os, sys, subprocess, logging -try: from socketserver import ForkingMixIn -except: from SocketServer import ForkingMixIn -try: from http.server import HTTPServer -except: from BaseHTTPServer import HTTPServer -from select import select -import websocket -try: - from urllib.parse import parse_qs, urlparse -except: - from cgi import parse_qs - from urlparse import urlparse - -class ProxyRequestHandler(websocket.WebSocketRequestHandler): - - traffic_legend = """ -Traffic Legend: - } - Client receive - }. - Client receive partial - { - Target receive - - > - Target send - >. - Target send partial - < - Client send - <. - Client send partial -""" - - def new_websocket_client(self): - """ - Called after a new WebSocket connection has been established. - """ - # Checks if we receive a token, and look - # for a valid target for it then - if self.server.target_cfg: - (self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path) - - # Connect to the target - if self.server.wrap_cmd: - msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port) - elif self.server.unix_target: - msg = "connecting to unix socket: %s" % self.server.unix_target - else: - msg = "connecting to: %s:%s" % ( - self.server.target_host, self.server.target_port) - - if self.server.ssl_target: - msg += " (using SSL)" - self.log_message(msg) - - tsock = websocket.WebSocketServer.socket(self.server.target_host, - self.server.target_port, - connect=True, use_ssl=self.server.ssl_target, unix_socket=self.server.unix_target) - - self.print_traffic(self.traffic_legend) - - # Start proxying - try: - self.do_proxy(tsock) - except: - if tsock: - tsock.shutdown(socket.SHUT_RDWR) - tsock.close() - if self.verbose: - self.log_message("%s:%s: Closed target", - self.server.target_host, self.server.target_port) - raise - - def get_target(self, target_cfg, path): - """ - Parses the path, extracts a token, and looks for a valid - target for that token in the configuration file(s). Sets - target_host and target_port if successful - """ - # The files in targets contain the lines - # in the form of token: host:port - - # Extract the token parameter from url - args = parse_qs(urlparse(path)[4]) # 4 is the query from url - - if not args.has_key('token') or not len(args['token']): - raise self.EClose("Token not present") - - token = args['token'][0].rstrip('\n') - - # target_cfg can be a single config file or directory of - # config files - if os.path.isdir(target_cfg): - cfg_files = [os.path.join(target_cfg, f) - for f in os.listdir(target_cfg)] - else: - cfg_files = [target_cfg] - - targets = {} - for f in cfg_files: - for line in [l.strip() for l in file(f).readlines()]: - if line and not line.startswith('#'): - ttoken, target = line.split(': ') - targets[ttoken] = target.strip() - - self.vmsg("Target config: %s" % repr(targets)) - - if targets.has_key(token): - return targets[token].split(':') - else: - raise self.EClose("Token '%s' not found" % token) - - def do_proxy(self, target): - """ - Proxy client WebSocket to normal target socket. - """ - cqueue = [] - c_pend = 0 - tqueue = [] - rlist = [self.request, target] - - while True: - wlist = [] - - if tqueue: wlist.append(target) - if cqueue or c_pend: wlist.append(self.request) - ins, outs, excepts = select(rlist, wlist, [], 1) - if excepts: raise Exception("Socket exception") - - if self.request in outs: - # Send queued target data to the client - c_pend = self.send_frames(cqueue) - - cqueue = [] - - if self.request in ins: - # Receive client data, decode it, and queue for target - bufs, closed = self.recv_frames() - tqueue.extend(bufs) - - if closed: - # TODO: What about blocking on client socket? - if self.verbose: - self.log_message("%s:%s: Client closed connection", - self.server.target_host, self.server.target_port) - raise self.CClose(closed['code'], closed['reason']) - - - if target in outs: - # Send queued client data to the target - dat = tqueue.pop(0) - sent = target.send(dat) - if sent == len(dat): - self.print_traffic(">") - else: - # requeue the remaining data - tqueue.insert(0, dat[sent:]) - self.print_traffic(".>") - - - if target in ins: - # Receive target data, encode it and queue for client - buf = target.recv(self.buffer_size) - if len(buf) == 0: - if self.verbose: - self.log_message("%s:%s: Target closed connection", - self.server.target_host, self.server.target_port) - raise self.CClose(1000, "Target closed") - - cqueue.append(buf) - self.print_traffic("{") - -class WebSocketProxy(websocket.WebSocketServer): - """ - Proxy traffic to and from a WebSockets client to a normal TCP - socket server target. All traffic to/from the client is base64 - encoded/decoded to allow binary data to be sent/received to/from - the target. - """ - - buffer_size = 65536 - - def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs): - # Save off proxy specific options - self.target_host = kwargs.pop('target_host', None) - self.target_port = kwargs.pop('target_port', None) - self.wrap_cmd = kwargs.pop('wrap_cmd', None) - self.wrap_mode = kwargs.pop('wrap_mode', None) - self.unix_target = kwargs.pop('unix_target', None) - self.ssl_target = kwargs.pop('ssl_target', None) - self.target_cfg = kwargs.pop('target_cfg', None) - # Last 3 timestamps command was run - self.wrap_times = [0, 0, 0] - - if self.wrap_cmd: - wsdir = os.path.dirname(sys.argv[0]) - rebinder_path = [os.path.join(wsdir, "..", "lib"), - os.path.join(wsdir, "..", "lib", "websockify"), - wsdir] - self.rebinder = None - - for rdir in rebinder_path: - rpath = os.path.join(rdir, "rebind.so") - if os.path.exists(rpath): - self.rebinder = rpath - break - - if not self.rebinder: - raise Exception("rebind.so not found, perhaps you need to run make") - self.rebinder = os.path.abspath(self.rebinder) - - self.target_host = "127.0.0.1" # Loopback - # Find a free high port - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(('', 0)) - self.target_port = sock.getsockname()[1] - sock.close() - - os.environ.update({ - "LD_PRELOAD": self.rebinder, - "REBIND_OLD_PORT": str(kwargs['listen_port']), - "REBIND_NEW_PORT": str(self.target_port)}) - - websocket.WebSocketServer.__init__(self, RequestHandlerClass, *args, **kwargs) - - def run_wrap_cmd(self): - self.msg("Starting '%s'", " ".join(self.wrap_cmd)) - self.wrap_times.append(time.time()) - self.wrap_times.pop(0) - self.cmd = subprocess.Popen( - self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) - self.spawn_message = True - - def started(self): - """ - Called after Websockets server startup (i.e. after daemonize) - """ - # Need to call wrapped command after daemonization so we can - # know when the wrapped command exits - if self.wrap_cmd: - dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) - elif self.unix_target: - dst_string = self.unix_target - else: - dst_string = "%s:%s" % (self.target_host, self.target_port) - - if self.target_cfg: - msg = " - proxying from %s:%s to targets in %s" % ( - self.listen_host, self.listen_port, self.target_cfg) - else: - msg = " - proxying from %s:%s to %s" % ( - self.listen_host, self.listen_port, dst_string) - - if self.ssl_target: - msg += " (using SSL)" - - self.msg("%s", msg) - - if self.wrap_cmd: - self.run_wrap_cmd() - - def poll(self): - # If we are wrapping a command, check it's status - - if self.wrap_cmd and self.cmd: - ret = self.cmd.poll() - if ret != None: - self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) - self.cmd = None - - if self.wrap_cmd and self.cmd == None: - # Response to wrapped command being gone - if self.wrap_mode == "ignore": - pass - elif self.wrap_mode == "exit": - sys.exit(ret) - elif self.wrap_mode == "respawn": - now = time.time() - avg = sum(self.wrap_times)/len(self.wrap_times) - if (now - avg) < 10: - # 3 times in the last 10 seconds - if self.spawn_message: - self.warn("Command respawning too fast") - self.spawn_message = False - else: - self.run_wrap_cmd() - - -def _subprocess_setup(): - # Python installs a SIGPIPE handler by default. This is usually not what - # non-Python successfulbprocesses expect. - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - - -def logger_init(): - logger = logging.getLogger(WebSocketProxy.log_prefix) - logger.propagate = False - logger.setLevel(logging.INFO) - h = logging.StreamHandler() - h.setLevel(logging.DEBUG) - h.setFormatter(logging.Formatter("%(message)s")) - logger.addHandler(h) - - -def websockify_init(): - logger_init() - - usage = "\n %prog [options]" - usage += " [source_addr:]source_port [target_addr:target_port]" - usage += "\n %prog [options]" - usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" - parser = optparse.OptionParser(usage=usage) - parser.add_option("--verbose", "-v", action="store_true", - help="verbose messages") - parser.add_option("--traffic", action="store_true", - help="per frame traffic") - parser.add_option("--record", - help="record sessions to FILE.[session_number]", metavar="FILE") - parser.add_option("--daemon", "-D", - dest="daemon", action="store_true", - help="become a daemon (background process)") - parser.add_option("--run-once", action="store_true", - help="handle a single WebSocket connection and exit") - parser.add_option("--timeout", type=int, default=0, - help="after TIMEOUT seconds exit when not connected") - parser.add_option("--idle-timeout", type=int, default=0, - help="server exits after TIMEOUT seconds if there are no " - "active connections") - parser.add_option("--cert", default="self.pem", - help="SSL certificate file") - parser.add_option("--key", default=None, - help="SSL key file (if separate from cert)") - parser.add_option("--ssl-only", action="store_true", - help="disallow non-encrypted client connections") - parser.add_option("--ssl-target", action="store_true", - help="connect to SSL target as SSL client") - parser.add_option("--unix-target", - help="connect to unix socket target", metavar="FILE") - parser.add_option("--web", default=None, metavar="DIR", - help="run webserver on same port. Serve files from DIR.") - parser.add_option("--wrap-mode", default="exit", metavar="MODE", - choices=["exit", "ignore", "respawn"], - help="action to take when the wrapped program exits " - "or daemonizes: exit (default), ignore, respawn") - parser.add_option("--prefer-ipv6", "-6", - action="store_true", dest="source_is_ipv6", - help="prefer IPv6 when resolving source_addr") - parser.add_option("--target-config", metavar="FILE", - dest="target_cfg", - help="Configuration file containing valid targets " - "in the form 'token: host:port' or, alternatively, a " - "directory containing configuration files of this form") - parser.add_option("--libserver", action="store_true", - help="use Python library SocketServer engine") - (opts, args) = parser.parse_args() - - if opts.verbose: - logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG) - - # Sanity checks - if len(args) < 2 and not (opts.target_cfg or opts.unix_target): - parser.error("Too few arguments") - if sys.argv.count('--'): - opts.wrap_cmd = args[1:] - else: - opts.wrap_cmd = None - if len(args) > 2: - parser.error("Too many arguments") - - if not websocket.ssl and opts.ssl_target: - parser.error("SSL target requested and Python SSL module not loaded."); - - if opts.ssl_only and not os.path.exists(opts.cert): - parser.error("SSL only and %s not found" % opts.cert) - - # Parse host:port and convert ports to numbers - if args[0].count(':') > 0: - opts.listen_host, opts.listen_port = args[0].rsplit(':', 1) - opts.listen_host = opts.listen_host.strip('[]') - else: - opts.listen_host, opts.listen_port = '', args[0] - - try: opts.listen_port = int(opts.listen_port) - except: parser.error("Error parsing listen port") - - if opts.wrap_cmd or opts.unix_target or opts.target_cfg: - opts.target_host = None - opts.target_port = None - else: - if args[1].count(':') > 0: - opts.target_host, opts.target_port = args[1].rsplit(':', 1) - opts.target_host = opts.target_host.strip('[]') - else: - parser.error("Error parsing target") - try: opts.target_port = int(opts.target_port) - except: parser.error("Error parsing target port") - - # Transform to absolute path as daemon may chdir - if opts.target_cfg: - opts.target_cfg = os.path.abspath(opts.target_cfg) - - # Create and start the WebSockets proxy - libserver = opts.libserver - del opts.libserver - if libserver: - # Use standard Python SocketServer framework - server = LibProxyServer(**opts.__dict__) - server.serve_forever() - else: - # Use internal service framework - server = WebSocketProxy(**opts.__dict__) - server.start_server() - - -class LibProxyServer(ForkingMixIn, HTTPServer): - """ - Just like WebSocketProxy, but uses standard Python SocketServer - framework. - """ - - def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs): - # Save off proxy specific options - self.target_host = kwargs.pop('target_host', None) - self.target_port = kwargs.pop('target_port', None) - self.wrap_cmd = kwargs.pop('wrap_cmd', None) - self.wrap_mode = kwargs.pop('wrap_mode', None) - self.unix_target = kwargs.pop('unix_target', None) - self.ssl_target = kwargs.pop('ssl_target', None) - self.target_cfg = kwargs.pop('target_cfg', None) - self.daemon = False - self.target_cfg = None - - # Server configuration - listen_host = kwargs.pop('listen_host', '') - listen_port = kwargs.pop('listen_port', None) - web = kwargs.pop('web', '') - - # Configuration affecting base request handler - self.only_upgrade = not web - self.verbose = kwargs.pop('verbose', False) - record = kwargs.pop('record', '') - if record: - self.record = os.path.abspath(record) - self.run_once = kwargs.pop('run_once', False) - self.handler_id = 0 - - for arg in kwargs.keys(): - print("warning: option %s ignored when using --libserver" % arg) - - if web: - os.chdir(web) - - HTTPServer.__init__(self, (listen_host, listen_port), - RequestHandlerClass) - - - def process_request(self, request, client_address): - """Override process_request to implement a counter""" - self.handler_id += 1 - ForkingMixIn.process_request(self, request, client_address) - - -if __name__ == '__main__': - websockify_init() From 9db6a906770d188a855de41787d6fedd4def5691 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 17 Feb 2015 17:54:21 -0500 Subject: [PATCH 057/527] Make sure websockify is cloned to the correct dir When `utils/launch.sh` clones websockify, it can be cloned into the incorrect directory, depending on how `utils/launch.sh` is run. This commit ensures that websockify is always cloned into `utils/websockify`. --- .gitignore | 1 + utils/launch.sh | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7cf2a2a6..a707ba70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.o tests/data_*.js utils/rebind.so +utils/websockify node_modules diff --git a/utils/launch.sh b/utils/launch.sh index 1492bf99..ab9a6efe 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -118,8 +118,7 @@ else if [[ $? -ne 0 ]]; then echo "No installed websockify, attempting to clone websockify..." WEBSOCKIFY=${HERE}/websockify/run - git clone https://github.com/kanaka/websockify - git update-index --assume-unchanged websockify + git clone https://github.com/kanaka/websockify ${HERE}/websockify if [[ ! -e $WEBSOCKIFY ]]; then echo "Unable to locate ${HERE}/websockify/run after downloading" From fe8a4dc9d80ca49b42bc36ab430db8916556ab3a Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 17 Feb 2015 23:14:30 -0500 Subject: [PATCH 058/527] Remove last bits of websockify cruft Issue #449 pointed out that there were some files that were missed in 6f5148648bd0e0ffa7ad00474c230f5f1ff4e39f. This fixes that. Closes #449 Closes #450 --- utils/Makefile | 11 ---------- utils/README.md | 4 ++++ utils/web.py | 55 ------------------------------------------------- 3 files changed, 4 insertions(+), 66 deletions(-) delete mode 100644 utils/Makefile delete mode 100755 utils/web.py diff --git a/utils/Makefile b/utils/Makefile deleted file mode 100644 index 7dc1bc4f..00000000 --- a/utils/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -TARGETS=rebind.so -CFLAGS += -fPIC - -all: $(TARGETS) - -rebind.so: rebind.o - $(CC) $(LDFLAGS) $^ -shared -fPIC -ldl -o $@ - -clean: - rm -f rebind.o rebind.so - diff --git a/utils/README.md b/utils/README.md index b90a387c..344f199e 100644 --- a/utils/README.md +++ b/utils/README.md @@ -1,5 +1,9 @@ ## WebSockets Proxy/Bridge +Websockify has been forked out into its own project. `launch.sh` wil +automatically download it here if it is not already present and not +installed as system-wide. + For more detailed description and usage information please refer to the [websockify README](https://github.com/kanaka/websockify/blob/master/README.md). diff --git a/utils/web.py b/utils/web.py deleted file mode 100755 index 23afca08..00000000 --- a/utils/web.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -''' -A super simple HTTP/HTTPS webserver for python. Automatically detect - -You can make a cert/key with openssl using: -openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem -as taken from http://docs.python.org/dev/library/ssl.html#certificates - -''' - -import traceback, sys -import socket -import ssl -#import http.server as server # python 3.X -import SimpleHTTPServer as server # python 2.X - -def do_request(connstream, from_addr): - x = object() - server.SimpleHTTPRequestHandler(connstream, from_addr, x) - connstream.close() - -def serve(): - bindsocket = socket.socket() - bindsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - #bindsocket.bind(('localhost', PORT)) - bindsocket.bind(('', PORT)) - bindsocket.listen(5) - - print("serving on port", PORT) - - while True: - try: - newsocket, from_addr = bindsocket.accept() - peek = newsocket.recv(1024, socket.MSG_PEEK) - if peek.startswith("\x16"): - connstream = ssl.wrap_socket( - newsocket, - server_side=True, - certfile='self.pem', - ssl_version=ssl.PROTOCOL_TLSv1) - else: - connstream = newsocket - - do_request(connstream, from_addr) - - except Exception: - traceback.print_exc() - -try: - PORT = int(sys.argv[1]) -except: - print "%s port" % sys.argv[0] - sys.exit(2) - -serve() From 2ec29db7524201bdb6e95bfc6d925bbf4aa55f34 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 20 Feb 2015 17:31:37 -0500 Subject: [PATCH 059/527] Remove 'debian' directory This commit removes the old debian packaging information, which is out of date. People who wish to create Debain packages for noVNC are better off using the package information from the actual Debian or Ubuntu packages. Closes #453 --- debian/changelog | 35 ----------------------------------- debian/compat | 1 - debian/control | 13 ------------- debian/copyright | 37 ------------------------------------- debian/novnc.install | 31 ------------------------------- debian/rules | 14 -------------- docs/packaging.txt | 23 ----------------------- docs/release.txt | 2 +- 8 files changed, 1 insertion(+), 155 deletions(-) delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100644 debian/novnc.install delete mode 100755 debian/rules delete mode 100644 docs/packaging.txt diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index 4d307424..00000000 --- a/debian/changelog +++ /dev/null @@ -1,35 +0,0 @@ -novnc (0.4) maverick; urgency=low - - * Clarify permissive licenses of HTML, CSS, images. - * Use render queue and requestAnimationFrame - * UltraVNC repeater support - - -- Joel Martin Fri, 14 Sep 2012 05:00:00 -0600 - -novnc (0.3) maverick; urgency=low - - * add tight encoding support - * release pressed key when focus lost (fixes locked Alt key) - * Support Apple Remote Desktop - * Add nova/openstack proxy wrapper - * Better connection close handling/reporting - - -- Joel Martin Fri, 11 May 2012 03:00:00 -0600 - -novnc (0.2) maverick; urgency=low - - * Mobile device support with viewport clipping - * Much better styling that also works on mobile devices well - * Update websockify to support latest WebSocket protocol HyBi 13 - (i.e. support IETF 6455) - * Better support in websockify for python 2.4 through 3.2 - * Support VMWare ESX and Intel AMT KVM - * View only mode - - -- Joel Martin Tue, 05 Jul 2011 01:00:00 -0600 - -novnc (0.1) maverick; urgency=low - - * First upstream release - - -- Joel Martin Tue, 05 Jul 2011 01:00:00 -0600 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7f8f011e..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100644 index 0f79f889..00000000 --- a/debian/control +++ /dev/null @@ -1,13 +0,0 @@ -Source: novnc -Section: web -Priority: optional -Maintainer: Joel Martin -Build-Depends: debhelper (>= 7.0.0~) -Standards-Version: 3.8.3 -Homepage: https://github.com/kanaka/noVNC/ - -Package: novnc -Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, python (>= 2.4), websockify -Description: HTML5 VNC client - VNC client using HTML5 (WebSockets, Canvas) with encryption (wss://) support. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index db00fa1f..00000000 --- a/debian/copyright +++ /dev/null @@ -1,37 +0,0 @@ -Upstream Project: https://github.com/kanaka/noVNC/ - ---------------------- Original LICENSE.txt --------------------------- - -noVNC is Copyright (C) 2012 Joel Martin - -Some portions of noVNC are copyright to their individual authors. -Please refer to the individual source files and/or to the noVNC commit -history: https://github.com/kanaka/noVNC/commits/master - -noVNC is licensed under the MPL 2.0 (Mozilla Public License) with the -following exceptions: - - *.html, *.css : 2-Clause BSD license - - include/Orbitron* : SIL Open Font License 1.1 - (Copyright 2009 Matt McInerney) - - images/ : Creative Commons Attribution-ShareAlike - http://creativecommons.org/licenses/by-sa/3.0/ - - include/base64.js : MPL 2.0 - - include/des.js : Various BSD style licenses - - include/jsunzip.js : zlib/libpng license - - include/web-socket-js/ : New BSD license (3-clause). Source code at - http://github.com/gimite/web-socket-js - - include/chrome-app/tcp-stream.js - : Apache 2.0 license - ----------------------------------------------------------------------- - -The MPL-2.0 license text may be found here: - http://www.mozilla.org/MPL/2.0/ diff --git a/debian/novnc.install b/debian/novnc.install deleted file mode 100644 index 54cb49ec..00000000 --- a/debian/novnc.install +++ /dev/null @@ -1,31 +0,0 @@ -vnc.html /usr/share/novnc -vnc_auto.html /usr/share/novnc -README.md /usr/share/doc/novnc -LICENSE.txt /usr/share/doc/novnc -utils/Makefile /usr/share/novnc/utils -utils/launch.sh /usr/share/novnc/utils -utils/websocket.py /usr/share/novnc/utils -utils/websockify /usr/share/novnc/utils -utils/rebind.c /usr/share/novnc/utils -utils/rebind.so /usr/share/novnc/utils -images /usr/share/novnc -images/favicon.ico /usr/share/novnc -include/base64.js /usr/share/novnc/include -include/des.js /usr/share/novnc/include -include/display.js /usr/share/novnc/include -include/keysymdef.js /usr/share/novnc/include -include/keysym.js /usr/share/novnc/include -include/keyboard.js /usr/share/novnc/include -include/input.js /usr/share/novnc/include -include/logo.js /usr/share/novnc/include -include/base.css /usr/share/novnc/include -include/blue.css /usr/share/novnc/include -include/black.css /usr/share/novnc/include -include/playback.js /usr/share/novnc/include -include/rfb.js /usr/share/novnc/include -include/ui.js /usr/share/novnc/include -include/util.js /usr/share/novnc/include -include/websock.js /usr/share/novnc/include -include/webutil.js /usr/share/novnc/include -include/jsunzip.js /usr/share/novnc/include -include/web-socket-js/* /usr/share/novnc/include/web-socket-js diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 25b08123..00000000 --- a/debian/rules +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/make -f - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -clean: - make -C utils clean - dh clean - -build: - make -C utils rebind.so - -%: - dh ${@} diff --git a/docs/packaging.txt b/docs/packaging.txt deleted file mode 100644 index 95524549..00000000 --- a/docs/packaging.txt +++ /dev/null @@ -1,23 +0,0 @@ -noVNC packaging steps for Debian/Ubuntu: - -- Update the noVNC version in docs/VERSION and add a new entry for the - version in debian/changelog - -- Rename the novnc source directory to match the form "novnc-VERSION". - -- In the novnc source directory, run the packaging command: - - debuild -I -uc -us - - - The -I option ignores the .git directory when generating the - source tarball. - - the -uc and -us may be omitted in order to create a signed - package. - - - Alternatively, use pbuilder instead of debuild in order to build - for other distributions and to guarantee a sanitized package. - -- Verify the package and then commit the changes to docs/VERSION and - debian/changelog. - -- Upload the new package file(s). diff --git a/docs/release.txt b/docs/release.txt index 596482cf..1660b9b8 100644 --- a/docs/release.txt +++ b/docs/release.txt @@ -1,4 +1,4 @@ -- Update and commit docs/VERSION and debian/changelog +- Update and commit docs/VERSION - Create version tag and tarball from tag WVER=0.3 git tag v${WVER} From e543525faa9cf0d683f41e183e89cd909f3dd229 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 20 Feb 2015 17:27:27 -0500 Subject: [PATCH 060/527] Fix disconnect/reconnect issues Commit 155d78b39933a396d2555376f4e70f976c30b86d prevented reconnections from working properly. This fixes that by creating a new RFB object after disconnecting or failing. Furthermore, this ensures that a new connection cannot be opened util we've actually disconnected (either by timer or by receiving a `close` event). Closes #452 --- include/rfb.js | 1 + include/ui.js | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 03d4b816..909112d5 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -482,6 +482,7 @@ var RFB; Util.Debug("Clearing disconnect timer"); clearTimeout(this._disconnTimer); this._disconnTimer = null; + this._sock.off('close'); // make sure we don't get a double event } switch (state) { diff --git a/include/ui.js b/include/ui.js index 8e13fee0..e923ea8c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -110,13 +110,7 @@ var UI; UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); - UI.rfb = new RFB({'target': $D('noVNC_canvas'), - 'onUpdateState': UI.updateState, - 'onXvpInit': UI.updateXvpVisualState, - 'onClipboard': UI.clipReceive, - 'onFBUComplete': UI.FBUComplete, - 'onFBResize': UI.updateViewDragButton, - 'onDesktopName': UI.updateDocumentTitle}); + UI.initRFB(); var autoconnect = WebUtil.getQueryVar('autoconnect', false); if (autoconnect === 'true' || autoconnect == '1') { @@ -192,6 +186,16 @@ var UI; } }, + initRFB: function () { + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpVisualState, + 'onClipboard': UI.clipReceive, + 'onFBUComplete': UI.FBUComplete, + 'onFBResize': UI.updateViewDragButton, + 'onDesktopName': UI.updateDocumentTitle}); + }, + addMouseHandlers: function() { // Setup interface handlers that can't be inline $D("noVNC_view_drag_button").onclick = UI.setViewDrag; @@ -602,6 +606,13 @@ var UI; break; } + switch (state) { + case 'fatal': + case 'failed': + case 'disconnected': + UI.initRFB(); + } + if (typeof(msg) !== 'undefined') { $D('noVNC-control-bar').setAttribute("class", klass); $D('noVNC_status').innerHTML = msg; @@ -654,8 +665,13 @@ var UI; switch (UI.rfb_state) { case 'fatal': case 'failed': - case 'loaded': case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + break; + case 'loaded': $D('connectButton').style.display = ""; $D('disconnectButton').style.display = "none"; break; @@ -736,8 +752,7 @@ var UI; UI.rfb.set_onFBUComplete(UI.FBUComplete); $D('noVNC_logo').style.display = "block"; - UI.connSettingsOpen = false; - UI.toggleConnectPanel(); + // Don't display the connection settings until we're actually disconnected }, displayBlur: function() { From 88224c3fa7d31bcd945ba8df993d159c9e22a0a2 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 27 Feb 2015 12:42:43 -0500 Subject: [PATCH 061/527] Update dependencies to the latest versions This commit updates the test dependencies to the latest versions. --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ddd2558a..00288876 100644 --- a/package.json +++ b/package.json @@ -28,22 +28,22 @@ "devDependencies": { "ansi": "^0.3.0", "casperjs": "^1.1.0-beta3", - "chai": "^1.10.0", - "commander": "^2.5.0", - "karma": "^0.12.25", + "chai": "^2.1.0", + "commander": "^2.6.0", + "karma": "^0.12.31", "karma-chai": "^0.1.0", - "karma-mocha": "^0.1.9", - "karma-mocha-reporter": "^0.3.1", + "karma-mocha": "^0.1.10", + "karma-mocha-reporter": "^1.0.0", "karma-phantomjs-launcher": "^0.1.4", "karma-sauce-launcher": "^0.2.10", - "karma-sinon": "^1.0.3", + "karma-sinon": "^1.0.4", "karma-sinon-chai-latest": "^0.1.0", - "mocha": "^2.0.1", + "mocha": "^2.1.0", "open": "^0.0.5", - "phantom": "^0.7.0", - "phantomjs": "^1.9.12", - "sinon": "^1.12.1", - "sinon-chai": "^2.6.0", + "phantom": "^0.7.2", + "phantomjs": "^1.9.15", + "sinon": "^1.12.2", + "sinon-chai": "^2.7.0", "spooky": "^0.2.5", "temp": "^0.8.1" } From 20d3fb66659e2f30417d7cbb255e34e079677e66 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 27 Feb 2015 13:21:43 -0500 Subject: [PATCH 062/527] Increase Karma-Sauce Connector Timeout This commit increases the browserNoActivityTimeout to 100s (from the default of 10s) in an attempt to alliviate the timeout issues that we are seeing. --- karma.conf.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/karma.conf.js b/karma.conf.js index 94b69868..9893df67 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -178,7 +178,10 @@ module.exports = function(config) { // Increase timeout in case connection is slow/we run more browsers than possible // (we currently get 3 for free, and we try to run 7, so it can take a while) - captureTimeout: 240000 + captureTimeout: 240000, + + // similarly to above + browserNoActivityTimeout: 100000, }; if (useSauce) { From 7e54fb93dd60f4cb08685a3a747f8f0a691519cf Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 25 Feb 2015 17:02:16 -0500 Subject: [PATCH 063/527] Make Util.getPosition be relative to page Commit 5108c4635c847de9be0edadf572f7426f351b66a caused a regression in the case where scrolling is used -- getPosition return position relative to the viewport, while getEventPosition expected a position relative to the page. As per https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect, the fix for this is simply to add the `pageXOffset` and `pageYOffset` to the output of `getBoundingClientRect()`. Fixes #459. --- include/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/util.js b/include/util.js index 482b10c9..effb0705 100644 --- a/include/util.js +++ b/include/util.js @@ -436,7 +436,7 @@ Util.load_scripts = function (files) { Util.getPosition = function(obj) { "use strict"; var objPosition = obj.getBoundingClientRect(); - return {'x': objPosition.left, 'y': objPosition.top}; + return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset}; }; From 7e161007abe8338afc501e907993bf5a72e085d9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 27 Feb 2015 15:04:34 -0500 Subject: [PATCH 064/527] Launch Sauce Connect through Travis This commit makes Travis launch sauce connect, instead of using karma to start Sauce Connect --- .travis.yml | 2 ++ karma.conf.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 371b974a..81922524 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,3 +14,5 @@ env: - secure: QE5GqGd2hrpQsIgd8dlv3oRUUHqZayomzzQjNXOB81VQi241uz/ru+3GtBZLB5WLZCq/Gj89vbLnR0LN4ixlmPaWv3/WJQGyDGuRD/vMnccVl+rBUP/Hh2zdYwiISIGcrywNAE+KLus/lyt/ahVgzbaRaDSzrM1HaZFT/rndGck= - secure: g75sdctEwj0hoLW0Y08Tdv8s5scNzplB6a9EtaJ2vJD9S/bK+AsPqbWesGv1UlrFPCWdbV7Vg61vkmoUjcmb5xhqFIjcM9TlYJoKWeOTsOmnQoSIkIq6gMF1k02+LmKInbPgIzrp3m3jluS1qaOs/EzFpDnJp9hWBiAfXa12Jxk= before_script: npm install -g karma-cli +addons: + sauce_connect: true diff --git a/karma.conf.js b/karma.conf.js index 9893df67..d8b8e905 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -188,7 +188,7 @@ module.exports = function(config) { my_conf.captureTimeout = 0; // use SL timeout my_conf.sauceLabs = { testName: 'noVNC Tests (all)', - startConnect: true, + startConnect: false, tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER }; } From 8b46c0deb027f06b9db0601d518d1b1490ece66c Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 17 Feb 2015 18:53:38 -0500 Subject: [PATCH 065/527] Update UI to allow for different scaling modes This commit updates the UI to allow for different scaling modes. The "resize" option was changed to be a dropdown with the following options: "None" (nothing), "Remote Resizing" (SetDesktopSize). --- include/ui.js | 4 ++-- vnc.html | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/include/ui.js b/include/ui.js index e923ea8c..57e630bd 100644 --- a/include/ui.js +++ b/include/ui.js @@ -46,7 +46,7 @@ var UI; }, onresize: function (callback) { - if (UI.getSetting('resize')) { + if (UI.getSetting('resize') === 'remote') { var innerW = window.innerWidth; var innerH = window.innerHeight; var controlbarH = $D('noVNC-control-bar').offsetHeight; @@ -104,7 +104,7 @@ var UI; UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('true_color', true); UI.initSetting('cursor', !UI.isTouchDevice); - UI.initSetting('resize', false); + UI.initSetting('resize', 'off'); UI.initSetting('shared', true); UI.initSetting('view_only', false); UI.initSetting('path', 'websockify'); diff --git a/vnc.html b/vnc.html index 7cc07cf9..1d1abaaf 100644 --- a/vnc.html +++ b/vnc.html @@ -157,10 +157,16 @@
  • True Color
  • Local Cursor
  • Clip to Window
  • -
  • Resize Remote to Window
  • Shared Mode
  • View Only
  • +
  • Path
  • +
  • +
  • Repeater ID

  • From 72747869a7a95b72d26914508ce8ec5670eecc5b Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 17 Feb 2015 22:41:34 -0500 Subject: [PATCH 066/527] Support local scaling This commit adds two new addition scaling options. Both options do local scaling. The first "Local Scaling", does both upscaling and downscaling. The second option, "Local Downscaling", only downscales. This is based on work by @mightypenguin (with an additional bug reported by @glazik12). --- include/display.js | 68 +++++++++++++++++++++---------------- include/ui.js | 61 +++++++++++++++++++++++++++------ include/util.js | 10 ++++-- tests/test.display.js | 78 +++++++++++++++++++++++++++++++++++++++++++ vnc.html | 2 ++ 5 files changed, 176 insertions(+), 43 deletions(-) diff --git a/include/display.js b/include/display.js index d1278681..2b1b827b 100644 --- a/include/display.js +++ b/include/display.js @@ -518,38 +518,48 @@ var Display; return this._fb_height; }, + autoscale: function (containerWidth, containerHeight, downscaleOnly) { + var targetAspectRatio = containerWidth / containerHeight; + var fbAspectRatio = this._fb_width / this._fb_height; + + var scaleRatio; + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / this._fb_width; + } else { + scaleRatio = containerHeight / this._fb_height; + } + + var targetW, targetH; + if (scaleRatio > 1.0 && downscaleOnly) { + targetW = this._fb_width; + targetH = this._fb_height; + scaleRatio = 1.0; + } else if (fbAspectRatio >= targetAspectRatio) { + targetW = containerWidth; + targetH = Math.round(containerWidth / fbAspectRatio); + } else { + targetW = Math.round(containerHeight * fbAspectRatio); + targetH = containerHeight; + } + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + this._target.style.width = targetW + 'px'; + this._target.style.height = targetH + 'px'; + + this._scale = scaleRatio; + + return scaleRatio; // so that the mouse, etc scale can be set + }, + // Private Methods _rescale: function (factor) { - var canvas = this._target; - var properties = ['transform', 'WebkitTransform', 'MozTransform']; - var transform_prop; - while ((transform_prop = properties.shift())) { - if (typeof canvas.style[transform_prop] !== 'undefined') { - break; - } - } - - if (transform_prop === null) { - Util.Debug("No scaling support"); - return; - } - - if (typeof(factor) === "undefined") { - factor = this._scale; - } else if (factor > 1.0) { - factor = 1.0; - } else if (factor < 0.1) { - factor = 0.1; - } - - if (this._scale === factor) { - return; - } - this._scale = factor; - var x = canvas.width - (canvas.width * factor); - var y = canvas.height - (canvas.height * factor); - canvas.style[transform_prop] = 'scale(' + this._scale + ') translate(-' + x + 'px, -' + y + 'px)'; + + this._target.style.width = Math.round(factor * this._fb_width) + 'px'; + this._target.style.height = Math.round(factor * this._fb_height) + 'px'; }, _setFillColor: function (color) { diff --git a/include/ui.js b/include/ui.js index 57e630bd..a5433dce 100644 --- a/include/ui.js +++ b/include/ui.js @@ -46,15 +46,29 @@ var UI; }, onresize: function (callback) { - if (UI.getSetting('resize') === 'remote') { - var innerW = window.innerWidth; - var innerH = window.innerHeight; - var controlbarH = $D('noVNC-control-bar').offsetHeight; - // For some unknown reason the container is higher than the canvas, - // 5px higher in Firefox and 4px higher in Chrome - var padding = 5; - if (innerW !== undefined && innerH !== undefined) - UI.rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + var innerW = window.innerWidth; + var innerH = window.innerHeight; + var controlbarH = $D('noVNC-control-bar').offsetHeight; + // For some unknown reason the container is higher than the canvas, + // 5px higher in Firefox and 4px higher in Chrome + var padding = 5; + var effectiveH = innerH - controlbarH - padding; + + var display = UI.rfb.get_display(); + + if (innerW !== undefined && innerH !== undefined) { + var scaleType = UI.getSetting('resize'); + if (scaleType === 'remote') { + // use remote resizing + Util.Debug('Attempting setDesktopSize(' + innerW + ', ' + effectiveH + ')'); + UI.rfb.setDesktopSize(innerW, effectiveH); + } else if (scaleType === 'scale' || scaleType === 'downscale') { + // use local scaling + var downscaleOnly = scaleType === 'downscale'; + var scaleRatio = display.autoscale(innerW, effectiveH, downscaleOnly); + UI.rfb.get_mouse().set_scale(scaleRatio); + Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); + } } }, @@ -237,6 +251,11 @@ var UI; $D("noVNC_apply").onclick = UI.settingsApply; $D("noVNC_connect_button").onclick = UI.connect; + + $D("noVNC_resize").onchange = function () { + var connected = UI.rfb_state === 'normal' ? true : false; + UI.enableDisableClip(connected); + }; }, // Read form control compatible setting from cookie @@ -510,8 +529,14 @@ var UI; if (UI.rfb.get_display().get_cursor_uri()) { UI.saveSetting('cursor'); } - UI.saveSetting('clip'); + UI.saveSetting('resize'); + + if (UI.getSetting('resize') === 'downscale' || UI.getSetting('resize') === 'scale') { + UI.forceSetting('clip', false); + } + + UI.saveSetting('clip'); UI.saveSetting('shared'); UI.saveSetting('view_only'); UI.saveSetting('path'); @@ -635,7 +660,8 @@ var UI; UI.updateSetting('cursor', !UI.isTouchDevice); $D('noVNC_cursor').disabled = true; } - $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + + UI.enableDisableClip(connected); $D('noVNC_resize').disabled = connected; $D('noVNC_shared').disabled = connected; $D('noVNC_view_only').disabled = connected; @@ -697,6 +723,19 @@ var UI; } }, + enableDisableClip: function (connected) { + var resizeElem = $D('noVNC_resize'); + if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { + UI.forceSetting('clip', false); + $D('noVNC_clip').disabled = true; + } else { + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + if (UI.isTouchDevice) { + UI.forceSetting('clip', true); + } + } + }, + // This resize can not be done until we know from the first Frame Buffer Update // if it is supported or not. // The resize is needed to make sure the server desktop size is updated to the diff --git a/include/util.js b/include/util.js index effb0705..02e72256 100644 --- a/include/util.js +++ b/include/util.js @@ -435,8 +435,12 @@ Util.load_scripts = function (files) { Util.getPosition = function(obj) { "use strict"; + // NB(sross): the Mozilla developer reference seems to indicate that + // getBoundingClientRect includes border and padding, so the canvas + // style should NOT include either. var objPosition = obj.getBoundingClientRect(); - return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset}; + return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset, + 'width': objPosition.width, 'height': objPosition.height}; }; @@ -462,8 +466,8 @@ Util.getEventPosition = function (e, obj, scale) { } var realx = docX - pos.x; var realy = docY - pos.y; - var x = Math.max(Math.min(realx, obj.width - 1), 0); - var y = Math.max(Math.min(realy, obj.height - 1), 0); + var x = Math.max(Math.min(realx, pos.width - 1), 0); + var y = Math.max(Math.min(realy, pos.height - 1), 0); return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; }; diff --git a/tests/test.display.js b/tests/test.display.js index 949aca1e..f122dca9 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -154,6 +154,84 @@ describe('Display/Canvas Helper', function () { }); }); + describe('rescaling', function () { + var display; + var canvas; + + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + canvas = display.get_target(); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should not change the bitmap size of the canvas', function () { + display.set_scale(0.5); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + + it('should change the effective rendered size of the canvas', function () { + display.set_scale(0.5); + expect(canvas.clientWidth).to.equal(2); + expect(canvas.clientHeight).to.equal(2); + }); + }); + + describe('autoscaling', function () { + var display; + var canvas; + + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + canvas = display.get_target(); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should preserve aspect ratio while autoscaling', function () { + display.autoscale(16, 9); + expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3); + }); + + it('should use width to determine scale when the current aspect ratio is wider than the target', function () { + expect(display.autoscale(9, 16)).to.equal(9 / 4); + expect(canvas.clientWidth).to.equal(9); + expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) + }); + + it('should use height to determine scale when the current aspect ratio is taller than the target', function () { + expect(display.autoscale(16, 9)).to.equal(3); // 9 / 3 + expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) + expect(canvas.clientHeight).to.equal(9); + + }); + + it('should not change the bitmap size of the canvas', function () { + display.autoscale(16, 9); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + + it('should not upscale when downscaleOnly is true', function () { + expect(display.autoscale(2, 2, true)).to.equal(0.5); + expect(canvas.clientWidth).to.equal(2); + expect(canvas.clientHeight).to.equal(2); + + expect(display.autoscale(16, 9, true)).to.equal(1.0); + expect(canvas.clientWidth).to.equal(4); + expect(canvas.clientHeight).to.equal(3); + }); + }); + describe('drawing', function () { // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the diff --git a/vnc.html b/vnc.html index 1d1abaaf..faa4e33b 100644 --- a/vnc.html +++ b/vnc.html @@ -164,6 +164,8 @@
  • From 3b8ec46fd26d644e6edbea4f46e630929297e448 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 5 Mar 2015 09:54:56 +0100 Subject: [PATCH 067/527] Make the touch-keyboard code more robust through verifying that global variables are set before use. --- include/ui.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index a5433dce..fb28e3ea 100644 --- a/include/ui.js +++ b/include/ui.js @@ -925,8 +925,15 @@ var UI; // This code is required since some browsers on Android are inconsistent in // sending keyCodes in the normal keyboard events when using on screen keyboards. keyInput: function(event) { + + if (!UI.rfb) { return; } + var newValue = event.target.value; - var oldValue = UI.lastKeyboardinput; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + var oldvalue = UI.lastKeyboardinput; var newLen; try { From 798340b98d097afd7e7cd949d9256dcf2a74eef4 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 5 Mar 2015 14:56:47 +0100 Subject: [PATCH 068/527] * Change name of ext_desktop_size to the proper ExtendedDesktopSize * Added better error handling in ExtendedDesktopSize * Added helper function to share code with DesktopSize * Update test.rfb.js to only check for error handling if we were the ones requesting the resize --- include/rfb.js | 99 +++++++++++++++++++++++++++++------------------ tests/test.rfb.js | 2 +- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 909112d5..6fcdab6f 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -37,24 +37,24 @@ var RFB; // In preference order this._encodings = [ - ['COPYRECT', 0x01 ], - ['TIGHT', 0x07 ], - ['TIGHT_PNG', -260 ], - ['HEXTILE', 0x05 ], - ['RRE', 0x02 ], - ['RAW', 0x00 ], - ['DesktopSize', -223 ], - ['Cursor', -239 ], + ['COPYRECT', 0x01 ], + ['TIGHT', 0x07 ], + ['TIGHT_PNG', -260 ], + ['HEXTILE', 0x05 ], + ['RRE', 0x02 ], + ['RAW', 0x00 ], + ['DesktopSize', -223 ], + ['Cursor', -239 ], // Psuedo-encoding settings - //['JPEG_quality_lo', -32 ], - ['JPEG_quality_med', -26 ], - //['JPEG_quality_hi', -23 ], - //['compress_lo', -255 ], - ['compress_hi', -247 ], - ['last_rect', -224 ], - ['xvp', -309 ], - ['ext_desktop_size', -308 ] + //['JPEG_quality_lo', -32 ], + ['JPEG_quality_med', -26 ], + //['JPEG_quality_hi', -23 ], + //['compress_lo', -255 ], + ['compress_hi', -247 ], + ['last_rect', -224 ], + ['xvp', -309 ], + ['ExtendedDesktopSize', -308 ] ]; this._encHandlers = {}; @@ -1871,15 +1871,27 @@ var RFB; return true; }, - ext_desktop_size: function () { + handle_FB_resize: function () { + this._fb_width = this._FBU.width; + this._fb_height = this._FBU.height; + this._display.resize(this._fb_width, this._fb_height); + this._onFBResize(this, this._fb_width, this._fb_height); + this._timing.fbu_rt_start = (new Date()).getTime(); + + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + ExtendedDesktopSize: function () { this._FBU.bytes = 1; - if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } this._supportsSetDesktopSize = true; var number_of_screens = this._sock.rQpeek8(); this._FBU.bytes = 4 + (number_of_screens * 16); - if (this._sock.rQwait("ext_desktop_size", this._FBU.bytes)) { return false; } + if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } this._sock.rQskipBytes(1); // number-of-screens this._sock.rQskipBytes(3); // padding @@ -1898,31 +1910,42 @@ var RFB; } } - if (this._FBU.x == 0 && this._FBU.y != 0) { return true; } + /* + * The x-position indicates the reason for the change: + * + * 0 - server resized on its own + * 1 - this client requested the resize + * 2 - another client requested the resize + */ - this._fb_width = this._FBU.width; - this._fb_height = this._FBU.height; - this._display.resize(this._fb_width, this._fb_height); - this._onFBResize(this, this._fb_width, this._fb_height); - - this._FBU.bytes = 0; - this._FBU.rects -= 1; + // We need to handle errors when we requested the resize. + if (this._FBU.x == 1 && this._FBU.y != 0) { + var msg = ""; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = "Resize is administratively prohibited"; + break; + case 2: + msg = "Out of resources"; + break; + case 3: + msg = "Invalid screen layout"; + break; + default: + msg = "Unknown reason"; + break; + } + Util.Info("Server did not accept the resize request: " + msg); + return true; + } + this._encHandlers.handle_FB_resize(); return true; }, DesktopSize: function () { - Util.Debug(">> set_desktopsize"); - this._fb_width = this._FBU.width; - this._fb_height = this._FBU.height; - this._display.resize(this._fb_width, this._fb_height); - this._onFBResize(this, this._fb_width, this._fb_height); - this._timing.fbu_rt_start = (new Date()).getTime(); - - this._FBU.bytes = 0; - this._FBU.rects--; - - Util.Debug("<< set_desktopsize"); + this._encHandlers.handle_FB_resize(); return true; }, diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 444e42c4..006b5fa9 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1584,7 +1584,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should not handle a failed request', function () { - var reason_for_change = 0; // non-incremental + var reason_for_change = 1; // requested by this client var status_code = 1; // Resize is administratively prohibited send_fbu_msg([{ x: reason_for_change, y: status_code, From fdedbafb1de63c8c704db5396fbb15a4187d9c62 Mon Sep 17 00:00:00 2001 From: samhed Date: Mon, 9 Mar 2015 14:30:56 +0100 Subject: [PATCH 069/527] * Don't check specific html elements from the display code (Fixes #446) * Renamed and reworked fbuClip to clippingDisplay * Added tests for clippingDisplay * Use the a noVNC_container which covers the entire page to get the full size (Fixes #463) * Added maxWidth and maxHeight to the canvas which can limit the viewport size * Only show either the canvas or the logo, hide one when the other is shown * Always center the canvas (previously it was only centered when not clipping) * Removed iOS specific "position-fixed" fixes and start calling setBarPosition on every resize * Removed the noVNC_screen_pad --- include/base.css | 22 ++++--- include/display.js | 98 ++++++++++++++++++---------- include/ui.js | 147 +++++++++++++++++++++++------------------- tests/test.display.js | 34 ++++++++++ vnc.html | 4 +- 5 files changed, 194 insertions(+), 111 deletions(-) diff --git a/include/base.css b/include/base.css index e2c9a96d..478b4d01 100644 --- a/include/base.css +++ b/include/base.css @@ -112,13 +112,7 @@ html { /* Do not set width/height for VNC_screen or VNC_canvas or incorrect * scaling will occur. Canvas resizes to remote VNC settings */ -#noVNC_screen_pad { - margin: 0px; - padding: 0px; - height: 36px; -} #noVNC_screen { - text-align: center; display: table; width:100%; height:100%; @@ -127,13 +121,25 @@ html { /*border-top-left-radius: 800px 600px;*/ } -#noVNC_container, #noVNC_canvas { +#noVNC_container { + display: none; + position: absolute; margin: 0px; padding: 0px; + bottom: 0px; + top: 36px; /* the height of the control bar */ + left: 0px; + right: 0px; + width: auto; + height: auto; } #noVNC_canvas { - left: 0px; + position: absolute; + left: 0; + right: 0; + margin-left: auto; + margin-right: auto; } #VNC_clipboard_clear_button { diff --git a/include/display.js b/include/display.js index 2b1b827b..d10d9b9a 100644 --- a/include/display.js +++ b/include/display.js @@ -1,6 +1,7 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin + * Copyright (C) 2015 Samuel Mannehed for Cendio AB * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. @@ -24,6 +25,10 @@ var Display; this._fb_width = 0; this._fb_height = 0; + // the size limit of the viewport (start disabled) + this._maxWidth = 0; + this._maxHeight = 0; + // the visible "physical canvas" viewport this._viewportLoc = { 'x': 0, 'y': 0, 'w': 0, 'h': 0 }; this._cleanRect = { 'x1': 0, 'y1': 0, 'x2': -1, 'y2': -1 }; @@ -202,8 +207,7 @@ var Display; viewportChangeSize: function(width, height) { - if (!this._viewport || - typeof(width) === "undefined" || typeof(height) === "undefined") { + if (typeof(width) === "undefined" || typeof(height) === "undefined") { Util.Debug("Setting viewport to full display region"); width = this._fb_width; @@ -213,41 +217,49 @@ var Display; var vp = this._viewportLoc; if (vp.w !== width || vp.h !== height) { + if (this._viewport) { + if (this._maxWidth !== 0 && width > this._maxWidth) { + width = this._maxWidth; + } + if (this._maxHeight !== 0 && height > this._maxHeight) { + height = this._maxHeight; + } + } + var cr = this._cleanRect; if (width < vp.w && cr.x2 > vp.x + width - 1) { cr.x2 = vp.x + width - 1; } - if (height < vp.h && cr.y2 > vp.y + height - 1) { cr.y2 = vp.y + height - 1; } - if (this.fbuClip()) { - // clipping - vp.w = window.innerWidth; - var cb = document.getElementById('noVNC-control-bar'); - var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; - vp.h = window.innerHeight - controlbar_h - 5; - } else { - // scrollbars - vp.w = width; - vp.h = height; - } + vp.w = width; + vp.h = height; - var saveImg = null; var canvas = this._target; - if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { - var img_width = canvas.width < vp.w ? canvas.width : vp.w; - var img_height = canvas.height < vp.h ? canvas.height : vp.h; - saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); - } + if (canvas.width !== width || canvas.height !== height) { - canvas.width = vp.w; - canvas.height = vp.h; + // We have to save the canvas data since changing the size will clear it + var saveImg = null; + if (vp.w > 0 && vp.h > 0 && canvas.width > 0 && canvas.height > 0) { + var img_width = canvas.width < vp.w ? canvas.width : vp.w; + var img_height = canvas.height < vp.h ? canvas.height : vp.h; + saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); + } - if (saveImg) { - this._drawCtx.putImageData(saveImg, 0, 0); + if (canvas.width !== width) { canvas.width = width; } + if (canvas.height !== height) { canvas.height = height; } + + if (this._viewport) { + canvas.style.height = height + 'px'; + canvas.style.width = width + 'px'; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } } } }, @@ -487,12 +499,18 @@ var Display; this._target.style.cursor = "none"; }, - fbuClip: function () { - var cb = document.getElementById('noVNC-control-bar'); - var controlbar_h = (cb !== null) ? cb.offsetHeight : 0; - return (this._viewport && - (this._fb_width > window.innerWidth - || this._fb_height > window.innerHeight - controlbar_h - 5)); + clippingDisplay: function () { + var vp = this._viewportLoc; + + var fbClip = this._fb_width > vp.w || this._fb_height > vp.h; + var limitedVp = this._maxWidth !== 0 && this._maxHeight !== 0; + var clipping = false; + + if (limitedVp) { + clipping = vp.w > this._maxWidth || vp.h > this._maxHeight; + } + + return fbClip || (limitedVp && clipping); }, // Overridden getters/setters @@ -558,8 +576,20 @@ var Display; _rescale: function (factor) { this._scale = factor; - this._target.style.width = Math.round(factor * this._fb_width) + 'px'; - this._target.style.height = Math.round(factor * this._fb_height) + 'px'; + var w; + var h; + + if (this._viewport && + this._maxWidth !== 0 && this._maxHeight !== 0) { + w = Math.min(this._fb_width, this._maxWidth); + h = Math.min(this._fb_height, this._maxHeight); + } else { + w = this._fb_width; + h = this._fb_height; + } + + this._target.style.width = Math.round(factor * w) + 'px'; + this._target.style.height = Math.round(factor * h) + 'px'; }, _setFillColor: function (color) { @@ -661,9 +691,11 @@ var Display; ['true_color', 'rw', 'bool'], // Use true-color pixel data ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color) ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0 - ['viewport', 'rw', 'bool'], // Use a viewport set with viewportChange() + ['viewport', 'rw', 'bool'], // Use viewport clipping ['width', 'rw', 'int'], // Display area width ['height', 'rw', 'int'], // Display area height + ['maxWidth', 'rw', 'int'], // Viewport max width (0 if disabled) + ['maxHeight', 'rw', 'int'], // Viewport max height (0 if disabled) ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only) diff --git a/include/ui.js b/include/ui.js index fb28e3ea..bcc0a948 100644 --- a/include/ui.js +++ b/include/ui.js @@ -1,7 +1,7 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Copyright (C) 2015 Samuel Mannehed for Cendio AB * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. @@ -45,33 +45,6 @@ var UI; WebUtil.initSettings(UI.start, callback); }, - onresize: function (callback) { - var innerW = window.innerWidth; - var innerH = window.innerHeight; - var controlbarH = $D('noVNC-control-bar').offsetHeight; - // For some unknown reason the container is higher than the canvas, - // 5px higher in Firefox and 4px higher in Chrome - var padding = 5; - var effectiveH = innerH - controlbarH - padding; - - var display = UI.rfb.get_display(); - - if (innerW !== undefined && innerH !== undefined) { - var scaleType = UI.getSetting('resize'); - if (scaleType === 'remote') { - // use remote resizing - Util.Debug('Attempting setDesktopSize(' + innerW + ', ' + effectiveH + ')'); - UI.rfb.setDesktopSize(innerW, effectiveH); - } else if (scaleType === 'scale' || scaleType === 'downscale') { - // use local scaling - var downscaleOnly = scaleType === 'downscale'; - var scaleRatio = display.autoscale(innerW, effectiveH, downscaleOnly); - UI.rfb.get_mouse().set_scale(scaleRatio); - Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); - } - } - }, - // Render default UI and initialize settings menu start: function(callback) { UI.isTouchDevice = 'ontouchstart' in document.documentElement; @@ -136,6 +109,8 @@ var UI; UI.updateVisualState(); + $D('noVNC_host').focus(); + // Show mouse selector buttons on touch screen devices if (UI.isTouchDevice) { // Show mobile buttons @@ -148,29 +123,14 @@ var UI; UI.initSetting('clip', false); } - //iOS Safari does not support CSS position:fixed. - //This detects iOS devices and enables javascript workaround. - if ((navigator.userAgent.match(/iPhone/i)) || - (navigator.userAgent.match(/iPod/i)) || - (navigator.userAgent.match(/iPad/i))) { - //UI.setOnscroll(); - //UI.setResize(); - } + UI.setViewClip(); UI.setBarPosition(); - $D('noVNC_host').focus(); - - UI.setViewClip(); - Util.addEvent(window, 'resize', function () { + UI.onresize(); UI.setViewClip(); - // When the window has been resized, wait until the size remains - // the same for 0.5 seconds before sending the request for changing - // the resolution of the session - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function(){ - UI.onresize(); - }, 500); + UI.updateViewDragButton(); + UI.setBarPosition(); } ); Util.addEvent(window, 'load', UI.keyboardinputReset); @@ -258,6 +218,55 @@ var UI; }; }, + onresize: function (callback) { + var size = UI.getCanvasLimit(); + + if (size && UI.rfb_state === 'normal' && UI.rfb.get_display()) { + var display = UI.rfb.get_display(); + var scaleType = UI.getSetting('resize'); + if (scaleType === 'remote') { + // use remote resizing + + // When the local window has been resized, wait until the size remains + // the same for 0.5 seconds before sending the request for changing + // the resolution of the session + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(function(){ + display.set_maxWidth(size.w); + display.set_maxHeight(size.h); + Util.Debug('Attempting setDesktopSize(' + + size.w + ', ' + size.h + ')'); + UI.rfb.setDesktopSize(size.w, size.h); + }, 500); + } else if (scaleType === 'scale' || scaleType === 'downscale') { + // use local scaling + + var downscaleOnly = scaleType === 'downscale'; + var scaleRatio = display.autoscale(size.w, size.h, downscaleOnly); + UI.rfb.get_mouse().set_scale(scaleRatio); + Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); + } + } + }, + + getCanvasLimit: function () { + var container = $D('noVNC_container'); + + // Hide the scrollbars until the size is calculated + container.style.overflow = "hidden"; + + var w = Util.getPosition(container).width; + var h = Util.getPosition(container).height; + + container.style.overflow = "visible"; + + if (isNaN(w) || isNaN(h)) { + return false; + } else { + return {w: w, h: h}; + } + }, + // Read form control compatible setting from cookie getSetting: function(name) { var ctrl = $D('noVNC_' + name); @@ -613,6 +622,7 @@ var UI; break; case 'disconnected': $D('noVNC_logo').style.display = "block"; + $D('noVNC_container').style.display = "none"; /* falls through */ case 'loaded': klass = "noVNC_status_normal"; @@ -781,6 +791,7 @@ var UI; //Close dialog. setTimeout(UI.setBarPosition, 100); $D('noVNC_logo').style.display = "none"; + $D('noVNC_container').style.display = "inline"; }, disconnect: function() { @@ -791,6 +802,8 @@ var UI; UI.rfb.set_onFBUComplete(UI.FBUComplete); $D('noVNC_logo').style.display = "block"; + $D('noVNC_container').style.display = "none"; + // Don't display the connection settings until we're actually disconnected }, @@ -839,17 +852,30 @@ var UI; // Turn clipping off UI.updateSetting('clip', false); display.set_viewport(false); - $D('noVNC_canvas').style.position = 'static'; + display.set_maxWidth(0); + display.set_maxHeight(0); display.viewportChangeSize(); } if (UI.getSetting('clip')) { // If clipping, update clipping settings - $D('noVNC_canvas').style.position = 'absolute'; - var pos = Util.getPosition($D('noVNC_canvas')); - var new_w = window.innerWidth - pos.x; - var new_h = window.innerHeight - pos.y; display.set_viewport(true); - display.viewportChangeSize(new_w, new_h); + + var size = UI.getCanvasLimit(); + if (size) { + display.set_maxWidth(size.w); + display.set_maxHeight(size.h); + + // Hide potential scrollbars that can skew the position + $D('noVNC_container').style.overflow = "hidden"; + + // The x position marks the left margin of the canvas, + // remove the margin from both sides to keep it centered + var new_w = size.w - (2 * Util.getPosition($D('noVNC_canvas')).x); + + $D('noVNC_container').style.overflow = "visible"; + + display.viewportChangeSize(new_w, size.h); + } } }, @@ -878,7 +904,7 @@ var UI; var vmb = $D('noVNC_view_drag_button'); if (UI.rfb_state === 'normal' && UI.rfb.get_display().get_viewport() && - UI.rfb.get_display().fbuClip()) { + UI.rfb.get_display().clippingDisplay()) { vmb.style.display = "inline"; } else { vmb.style.display = "none"; @@ -1058,19 +1084,6 @@ var UI; UI.keyboardVisible = false; }, - // iOS < Version 5 does not support position fixed. Javascript workaround: - setOnscroll: function() { - window.onscroll = function() { - UI.setBarPosition(); - }; - }, - - setResize: function () { - window.onResize = function() { - UI.setBarPosition(); - }; - }, - //Helper to add options to dropdown. addOption: function(selectbox, text, value) { var optn = document.createElement("OPTION"); diff --git a/tests/test.display.js b/tests/test.display.js index f122dca9..d54cb82f 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -134,6 +134,40 @@ describe('Display/Canvas Helper', function () { }); }); + describe('clipping', function () { + var display; + beforeEach(function () { + display = new Display({ target: document.createElement('canvas'), prefer_js: false, viewport: true }); + display.resize(4, 3); + }); + + it('should report true when no max-size and framebuffer > viewport', function () { + display.viewportChangeSize(2,2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + + it('should report false when no max-size and framebuffer = viewport', function () { + var clipping = display.clippingDisplay(); + expect(clipping).to.be.false; + }); + + it('should report true when viewport > max-size and framebuffer > viewport', function () { + display.viewportChangeSize(2,2); + display.set_maxWidth(1); + display.set_maxHeight(2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + + it('should report true when viewport > max-size and framebuffer = viewport', function () { + display.set_maxWidth(1); + display.set_maxHeight(2); + var clipping = display.clippingDisplay(); + expect(clipping).to.be.true; + }); + }); + describe('resizing', function () { var display; beforeEach(function () { diff --git a/vnc.html b/vnc.html index faa4e33b..b8bda05a 100644 --- a/vnc.html +++ b/vnc.html @@ -203,13 +203,11 @@
    -
    -

    no
    VNC

    - + Canvas not supported.
    From 16b3ef77d15179076144d45c9edfdb6d37beb41e Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 11 Mar 2015 07:29:30 +0100 Subject: [PATCH 070/527] Make getCanvasLimit more efficient by only calling getPosition once. --- include/ui.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/ui.js b/include/ui.js index bcc0a948..25f8617c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -255,8 +255,9 @@ var UI; // Hide the scrollbars until the size is calculated container.style.overflow = "hidden"; - var w = Util.getPosition(container).width; - var h = Util.getPosition(container).height; + var pos = Util.getPosition(container); + var w = pos.width; + var h = pos.height; container.style.overflow = "visible"; From 2ace90e6d593fa0b38589239fe6ba6599d00d2b6 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 26 Mar 2015 16:57:17 -0400 Subject: [PATCH 071/527] Follow symbolic links in launch.sh Previously, in launch.sh, `$HERE` was the directory of `$0`. However, if `$0` was actually a symlink, `$HERE` would be wherever the symlink was, which could cause issues (for example, the script wouldn't be able to local `$WEB` or `$WEBSOCKIFY` properly). Now, `$HERE` looks at whatever `$0` points at instead. Closes #447. --- utils/launch.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/launch.sh b/utils/launch.sh index ab9a6efe..2a0106de 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -22,7 +22,8 @@ usage() { } NAME="$(basename $0)" -HERE="$(cd "$(dirname "$0")" && pwd)" +REAL_NAME="$(readlink -f $0)" +HERE="$(cd "$(dirname "$REAL_NAME")" && pwd)" PORT="6080" VNC_DEST="localhost:5900" CERT="" From 58ded70d150c31df2fbd78c0320af6edc72610fc Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 24 Mar 2015 15:05:38 -0400 Subject: [PATCH 072/527] Create RFB object on connect In e543525faa9cf0d683f41e183e89cd909f3dd229, we switched to creating a new RFB object on disconnect. This caused issues, however, since any errors were only displayed briefly before the new "loaded" text was displayed instead. Now, we create the RFB object on connect. This essentially removes the usefulness of the "loaded" state, but prevents the aforementioned problem. To facilitate this, the code which does detection of cursor URI support was moved from this Display constructor (which now calls the new function) into its own function, `Util.browserSupportsCursorURIs()`. Fixes #467 --- include/display.js | 26 +++----------------------- include/ui.js | 36 +++++++++++++++++------------------- include/util.js | 23 +++++++++++++++++++++++ tests/test.display.js | 16 +++++----------- vnc.html | 4 ++-- 5 files changed, 50 insertions(+), 55 deletions(-) diff --git a/include/display.js b/include/display.js index d10d9b9a..201acf33 100644 --- a/include/display.js +++ b/include/display.js @@ -86,29 +86,9 @@ var Display; } // Determine browser support for setting the cursor via data URI scheme - var curDat = []; - for (var i = 0; i < 8 * 8 * 4; i++) { - curDat.push(255); - } - try { - var curSave = this._target.style.cursor; - Display.changeCursor(this._target, curDat, curDat, 2, 2, 8, 8); - if (this._target.style.cursor) { - if (this._cursor_uri === null || this._cursor_uri === undefined) { - this._cursor_uri = true; - } - Util.Info("Data URI scheme cursor supported"); - this._target.style.cursor = curSave; - } else { - if (this._cursor_uri === null || this._cursor_uri === undefined) { - this._cursor_uri = false; - } - Util.Warn("Data URI scheme cursor not supported"); - this._target.style.cursor = "none"; - } - } catch (exc) { - Util.Error("Data URI scheme cursor test exception: " + exc); - this._cursor_uri = false; + if (this._cursor_uri || this._cursor_uri === null || + this._cursor_uri === undefined) { + this._cursor_uri = Util.browserSupportsCursorURIs(this._target); } Util.Debug("<< Display.constructor"); diff --git a/include/ui.js b/include/ui.js index 25f8617c..a72d5fa9 100644 --- a/include/ui.js +++ b/include/ui.js @@ -97,8 +97,6 @@ var UI; UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); - UI.initRFB(); - var autoconnect = WebUtil.getQueryVar('autoconnect', false); if (autoconnect === 'true' || autoconnect == '1') { autoconnect = true; @@ -136,7 +134,7 @@ var UI; Util.addEvent(window, 'load', UI.keyboardinputReset); Util.addEvent(window, 'beforeunload', function () { - if (UI.rfb_state === 'normal') { + if (UI.rfb && UI.rfb_state === 'normal') { return "You are currently connected."; } } ); @@ -213,12 +211,14 @@ var UI; $D("noVNC_connect_button").onclick = UI.connect; $D("noVNC_resize").onchange = function () { - var connected = UI.rfb_state === 'normal' ? true : false; + var connected = UI.rfb && UI.rfb_state === 'normal'; UI.enableDisableClip(connected); }; }, onresize: function (callback) { + if (!UI.rfb) return; + var size = UI.getCanvasLimit(); if (size && UI.rfb_state === 'normal' && UI.rfb.get_display()) { @@ -480,7 +480,7 @@ var UI; } else { UI.updateSetting('encrypt'); UI.updateSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { + if (Util.browserSupportsCursorURIs()) { UI.updateSetting('cursor'); } else { UI.updateSetting('cursor', !UI.isTouchDevice); @@ -536,7 +536,7 @@ var UI; //Util.Debug(">> settingsApply"); UI.saveSetting('encrypt'); UI.saveSetting('true_color'); - if (UI.rfb.get_display().get_cursor_uri()) { + if (Util.browserSupportsCursorURIs()) { UI.saveSetting('cursor'); } @@ -558,7 +558,7 @@ var UI; WebUtil.selectStylesheet(UI.getSetting('stylesheet')); WebUtil.init_logging(UI.getSetting('logging')); UI.setViewClip(); - UI.setViewDrag(UI.rfb.get_viewportDrag()); + UI.setViewDrag(UI.rfb && UI.rfb.get_viewportDrag()); //Util.Debug("<< settingsApply"); }, @@ -642,13 +642,6 @@ var UI; break; } - switch (state) { - case 'fatal': - case 'failed': - case 'disconnected': - UI.initRFB(); - } - if (typeof(msg) !== 'undefined') { $D('noVNC-control-bar').setAttribute("class", klass); $D('noVNC_status').innerHTML = msg; @@ -659,13 +652,12 @@ var UI; // Disable/enable controls depending on connection state updateVisualState: function() { - var connected = UI.rfb_state === 'normal' ? true : false; + var connected = UI.rfb && UI.rfb_state === 'normal'; //Util.Debug(">> updateVisualState"); $D('noVNC_encrypt').disabled = connected; $D('noVNC_true_color').disabled = connected; - if (UI.rfb && UI.rfb.get_display() && - UI.rfb.get_display().get_cursor_uri()) { + if (Util.browserSupportsCursorURIs()) { $D('noVNC_cursor').disabled = connected; } else { UI.updateSetting('cursor', !UI.isTouchDevice); @@ -780,6 +772,8 @@ var UI; throw new Error("Must set host and port"); } + UI.initRFB(); + UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_true_color(UI.getSetting('true_color')); UI.rfb.set_local_cursor(UI.getSetting('cursor')); @@ -809,11 +803,15 @@ var UI; }, displayBlur: function() { + if (!UI.rfb) return; + UI.rfb.get_keyboard().set_focused(false); UI.rfb.get_mouse().set_focused(false); }, displayFocus: function() { + if (!UI.rfb) return; + UI.rfb.get_keyboard().set_focused(true); UI.rfb.get_mouse().set_focused(true); }, @@ -882,7 +880,7 @@ var UI; // Toggle/set/unset the viewport drag/move button setViewDrag: function(drag) { - if (!UI.rfb) { return; } + if (!UI.rfb) return; UI.updateViewDragButton(); @@ -953,7 +951,7 @@ var UI; // sending keyCodes in the normal keyboard events when using on screen keyboards. keyInput: function(event) { - if (!UI.rfb) { return; } + if (!UI.rfb) return; var newValue = event.target.value; diff --git a/include/util.js b/include/util.js index 02e72256..ed0e3cde 100644 --- a/include/util.js +++ b/include/util.js @@ -508,6 +508,29 @@ Util.stopEvent = function (e) { else { e.returnValue = false; } }; +Util._cursor_uris_supported = null; + +Util.browserSupportsCursorURIs = function () { + if (Util._cursor_uris_supported === null) { + try { + var target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; + + if (target.style.cursor) { + Util.Info("Data URI scheme cursor supported"); + Util._cursor_uris_supported = true; + } else { + Util.Warn("Data URI scheme cursor not supported"); + Util._cursor_uris_supported = false; + } + } catch (exc) { + Util.Error("Data URI scheme cursor test exception: " + exc); + Util._cursor_uris_supported = false; + } + } + + return Util._cursor_uris_supported; +}; // Set browser engine versions. Based on mootools. Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; diff --git a/tests/test.display.js b/tests/test.display.js index d54cb82f..56dfc220 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -28,35 +28,29 @@ describe('Display/Canvas Helper', function () { describe('checking for cursor uri support', function () { beforeEach(function () { - this._old_change_cursor = Display.changeCursor; + this._old_browser_supports_cursor_uris = Util.browserSupportsCursorURIs; }); it('should disable cursor URIs if there is no support', function () { - Display.changeCursor = function(target) { - target.style.cursor = undefined; - }; + Util.browserSupportsCursorURIs = function () { return false; }; var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); expect(display._cursor_uri).to.be.false; }); it('should enable cursor URIs if there is support', function () { - Display.changeCursor = function(target) { - target.style.cursor = 'pointer'; - }; + Util.browserSupportsCursorURIs = function () { return true; }; var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false }); expect(display._cursor_uri).to.be.true; }); it('respect the cursor_uri option if there is support', function () { - Display.changeCursor = function(target) { - target.style.cursor = 'pointer'; - }; + Util.browserSupportsCursorURIs = function () { return false; }; var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false, cursor_uri: false }); expect(display._cursor_uri).to.be.false; }); afterEach(function () { - Display.changeCursor = this._old_change_cursor; + Util.browserSupportsCursorURIs = this._old_browser_supports_cursor_uris; }); }); diff --git a/vnc.html b/vnc.html index b8bda05a..b8d11c70 100644 --- a/vnc.html +++ b/vnc.html @@ -46,7 +46,7 @@ -
    +
    -
    Loading
    +
    From d9fc1c7be45f7be9f127636d5d63c3c224f05d1d Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 24 Mar 2015 16:02:53 -0400 Subject: [PATCH 073/527] Throw exceptions from RFB constructor Previously, if an error was thrown from the Display constructor in the RFB constructor, we would attempt to use `RFB#updateState` to handle this. However, `RFB#updateState` attempts to close the WebSocket connection, which doesn't exist at this point. In the constructor, it's probably just better to raise an exception instead (making sure to clean up anything relevant). Fixes #460 --- include/rfb.js | 62 +++++++++++++++++++++++++++----------------------- include/ui.js | 22 +++++++++++------- vnc_auto.html | 30 ++++++++++++++---------- 3 files changed, 66 insertions(+), 48 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 6fcdab6f..b9db39c4 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -159,11 +159,13 @@ var RFB; this._encStats[this._encodings[i][1]] = [0, 0]; } + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception try { this._display = new Display({target: this._target}); } catch (exc) { Util.Error("Display exception: " + exc); - this._updateState('fatal', "No working Display"); + throw exc; } this._keyboard = new Keyboard({target: this._focusContainer, @@ -217,9 +219,11 @@ var RFB; } else { Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); if (!Util.Flash || Util.Flash.version < 9) { - this._updateState('fatal', "WebSockets or Adobe Flash is required"); + this._cleanupSocket('fatal'); + throw new Exception("WebSockets or Adobe Flash is required"); } else if (document.location.href.substr(0, 7) === 'file://') { - this._updateState('fatal', "'file://' URL is incompatible with Adobe Flash"); + this._cleanupSocket('fatal'); + throw new Exception("'file://' URL is incompatible with Adobe Flash"); } else { this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); } @@ -398,6 +402,32 @@ var RFB; } }, + _cleanupSocket: function (state) { + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } + + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } + + if (this._display && this._display.get_context()) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + if (state !== 'connect' && state !== 'loaded') { + this._display.defaultCursor(); + } + if (Util.get_logging() !== 'debug' || state === 'loaded') { + // Show noVNC logo on load and when disconnected, unless in + // debug mode + this._display.clear(); + } + } + + this._sock.close(); + }, /* * Page states: @@ -432,31 +462,7 @@ var RFB; */ if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1, 'disconnect': 1, 'failed': 1, 'fatal': 1}) { - - if (this._sendTimer) { - clearInterval(this._sendTimer); - this._sendTimer = null; - } - - if (this._msgTimer) { - clearInterval(this._msgTimer); - this._msgTimer = null; - } - - if (this._display && this._display.get_context()) { - this._keyboard.ungrab(); - this._mouse.ungrab(); - if (state !== 'connect' && state !== 'loaded') { - this._display.defaultCursor(); - } - if (Util.get_logging() !== 'debug' || state === 'loaded') { - // Show noVNC logo on load and when disconnected, unless in - // debug mode - this._display.clear(); - } - } - - this._sock.close(); + this._cleanupSocket(state); } if (oldstate === 'fatal') { diff --git a/include/ui.js b/include/ui.js index a72d5fa9..93550780 100644 --- a/include/ui.js +++ b/include/ui.js @@ -159,13 +159,19 @@ var UI; }, initRFB: function () { - UI.rfb = new RFB({'target': $D('noVNC_canvas'), - 'onUpdateState': UI.updateState, - 'onXvpInit': UI.updateXvpVisualState, - 'onClipboard': UI.clipReceive, - 'onFBUComplete': UI.FBUComplete, - 'onFBResize': UI.updateViewDragButton, - 'onDesktopName': UI.updateDocumentTitle}); + try { + UI.rfb = new RFB({'target': $D('noVNC_canvas'), + 'onUpdateState': UI.updateState, + 'onXvpInit': UI.updateXvpVisualState, + 'onClipboard': UI.clipReceive, + 'onFBUComplete': UI.FBUComplete, + 'onFBResize': UI.updateViewDragButton, + 'onDesktopName': UI.updateDocumentTitle}); + return true; + } catch (exc) { + UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + return false; + } }, addMouseHandlers: function() { @@ -772,7 +778,7 @@ var UI; throw new Error("Must set host and port"); } - UI.initRFB(); + if (!UI.initRFB()) return; UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_true_color(UI.getSetting('true_color')); diff --git a/vnc_auto.html b/vnc_auto.html index 9fd2272a..ec18ab84 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -216,18 +216,24 @@ return; } - rfb = new RFB({'target': $D('noVNC_canvas'), - 'encrypt': WebUtil.getQueryVar('encrypt', - (window.location.protocol === "https:")), - 'repeaterID': WebUtil.getQueryVar('repeaterID', ''), - 'true_color': WebUtil.getQueryVar('true_color', true), - 'local_cursor': WebUtil.getQueryVar('cursor', true), - 'shared': WebUtil.getQueryVar('shared', true), - 'view_only': WebUtil.getQueryVar('view_only', false), - 'onUpdateState': updateState, - 'onXvpInit': xvpInit, - 'onPasswordRequired': passwordRequired, - 'onFBUComplete': FBUComplete}); + try { + rfb = new RFB({'target': $D('noVNC_canvas'), + 'encrypt': WebUtil.getQueryVar('encrypt', + (window.location.protocol === "https:")), + 'repeaterID': WebUtil.getQueryVar('repeaterID', ''), + 'true_color': WebUtil.getQueryVar('true_color', true), + 'local_cursor': WebUtil.getQueryVar('cursor', true), + 'shared': WebUtil.getQueryVar('shared', true), + 'view_only': WebUtil.getQueryVar('view_only', false), + 'onUpdateState': updateState, + 'onXvpInit': xvpInit, + 'onPasswordRequired': passwordRequired, + 'onFBUComplete': FBUComplete}); + } catch (exc) { + UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + return; // don't continue trying to connect + } + rfb.connect(host, port, password, path); }; From 8ce27ddb4bb51e7e67bfac91505da73c22b09a79 Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 29 Apr 2015 14:54:28 +0200 Subject: [PATCH 074/527] Re-fixes #428 which was broken by commit 58ded70 * Disable local cursor when the browser doesn't support data uri --- include/display.js | 2 +- include/rfb.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/include/display.js b/include/display.js index 201acf33..9a8e4559 100644 --- a/include/display.js +++ b/include/display.js @@ -88,7 +88,7 @@ var Display; // Determine browser support for setting the cursor via data URI scheme if (this._cursor_uri || this._cursor_uri === null || this._cursor_uri === undefined) { - this._cursor_uri = Util.browserSupportsCursorURIs(this._target); + this._cursor_uri = Util.browserSupportsCursorURIs(); } Util.Debug("<< Display.constructor"); diff --git a/include/rfb.js b/include/rfb.js index b9db39c4..a591ca2b 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1265,6 +1265,7 @@ var RFB; this._local_cursor = true; } else { Util.Warn("Browser does not support local cursor"); + this._display.disableLocalCursor(); } } }; From 48d26b2d474f0a7f91b0dba749272c51b471ee6a Mon Sep 17 00:00:00 2001 From: Fabian Zaremba Date: Sun, 3 May 2015 19:28:05 +0200 Subject: [PATCH 075/527] Move #keyboardinput declarations to base.css/ui.js --- include/base.css | 1 + include/ui.js | 1 + vnc.html | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/base.css b/include/base.css index 478b4d01..90c17e47 100644 --- a/include/base.css +++ b/include/base.css @@ -238,6 +238,7 @@ html { position: relative; left: -40px; z-index: -1; + ime-mode: disabled; } /* diff --git a/include/ui.js b/include/ui.js index 93550780..9baf40c6 100644 --- a/include/ui.js +++ b/include/ui.js @@ -185,6 +185,7 @@ var UI; $D("keyboardinput").oninput = UI.keyInput; $D("keyboardinput").onblur = UI.keyInputBlur; + $D("keyboardinput").onsubmit = function () { return false; }; $D("showExtraKeysButton").onclick = UI.showExtraKeys; $D("toggleCtrlButton").onclick = UI.toggleCtrl; diff --git a/vnc.html b/vnc.html index b8d11c70..4c4aeb1c 100644 --- a/vnc.html +++ b/vnc.html @@ -70,8 +70,7 @@ style for example --> + mozactionhint="Enter">
    From 613f05eea0a25d878c365f92bd2d71704087ca6a Mon Sep 17 00:00:00 2001 From: Samuel Date: Tue, 5 May 2015 13:13:17 +0200 Subject: [PATCH 076/527] Update LICENSE.txt Removed old no longer existing vnc.js and added playback.js --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 2d094089..e896efca 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -12,10 +12,10 @@ is not limited to): include/jsunzip.js include/keysym.js include/logo.js + include/playback.js include/rfb.js include/ui.js include/util.js - include/vnc.js include/websock.js include/webutil.js From cb3e4deb7365484c58d6fc7bddd345077db2381a Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 19 May 2015 12:42:33 +0200 Subject: [PATCH 077/527] Stop using keyboardinputReset at the start of keyInput * This fixes backspace when using the on-screen keyboard on Android --- include/ui.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/include/ui.js b/include/ui.js index 9baf40c6..a8239c3a 100644 --- a/include/ui.js +++ b/include/ui.js @@ -961,11 +961,7 @@ var UI; if (!UI.rfb) return; var newValue = event.target.value; - - if (!UI.lastKeyboardinput) { - UI.keyboardinputReset(); - } - var oldvalue = UI.lastKeyboardinput; + var oldValue = UI.lastKeyboardinput; var newLen; try { From 1138bdd4b7e400833eb9cf444ae8daf8702eae7b Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 19 May 2015 13:34:50 +0200 Subject: [PATCH 078/527] Reverting most of commit cb3e4de. The issue was not related to using keyboardinputReset.. the issue was infact a typo which was introduced in 3b8ec46. I must be too tired.. --- include/ui.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/ui.js b/include/ui.js index a8239c3a..3e7db636 100644 --- a/include/ui.js +++ b/include/ui.js @@ -961,6 +961,10 @@ var UI; if (!UI.rfb) return; var newValue = event.target.value; + + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } var oldValue = UI.lastKeyboardinput; var newLen; From 2bcfd58667d585fd8af36c8e525a7062a140bf6e Mon Sep 17 00:00:00 2001 From: MOZGIII Date: Fri, 12 Jun 2015 20:41:41 +0300 Subject: [PATCH 079/527] Fixed incorrect UI usage and minor updateState params errors in vnc_auto.html --- vnc_auto.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vnc_auto.html b/vnc_auto.html index ec18ab84..4361a5d5 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -211,8 +211,7 @@ path = WebUtil.getQueryVar('path', 'websockify'); if ((!host) || (!port)) { - updateState('failed', - "Must specify host and port in URL"); + updateState(null, 'fatal', null, 'Must specify host and port in URL'); return; } @@ -230,7 +229,7 @@ 'onPasswordRequired': passwordRequired, 'onFBUComplete': FBUComplete}); } catch (exc) { - UI.updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); + updateState(null, 'fatal', null, 'Unable to create RFB client -- ' + exc); return; // don't continue trying to connect } From 29a0e6a8a3d091f075b1f1e941ee70651c245795 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 16 Jun 2015 22:39:17 +0200 Subject: [PATCH 080/527] Always show the viewport drag button on touch devices to keep the GUI from "jumping around". Enable/disable the button instead of show/hide on these devices. --- include/base.css | 4 ++++ include/ui.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/include/base.css b/include/base.css index 90c17e47..888b5f27 100644 --- a/include/base.css +++ b/include/base.css @@ -320,6 +320,10 @@ html { /*box-shadow:inset 0.4px 0.4px 0.4px #000000;*/ } +.noVNC_status_button:disabled { + opacity: 0.4; +} + /*Settings Bubble*/ .triangle-right { diff --git a/include/ui.js b/include/ui.js index 3e7db636..ba04a859 100644 --- a/include/ui.js +++ b/include/ui.js @@ -697,6 +697,7 @@ var UI; // State change disables viewport dragging. // It is enabled (toggled) by direct click on the button UI.setViewDrag(false); + UI.updateViewDragButton(); switch (UI.rfb_state) { case 'fatal': @@ -889,8 +890,6 @@ var UI; setViewDrag: function(drag) { if (!UI.rfb) return; - UI.updateViewDragButton(); - if (typeof(drag) === "undefined" || typeof(drag) === "object") { // If not specified, then toggle @@ -911,8 +910,18 @@ var UI; if (UI.rfb_state === 'normal' && UI.rfb.get_display().get_viewport() && UI.rfb.get_display().clippingDisplay()) { + // Enable the viewport drag button vmb.style.display = "inline"; + vmb.disabled = false; + + } else if (UI.rfb_state === 'normal' && + UI.isTouchDevice) { + // Disable the viewport drag button + vmb.style.display = "inline"; + vmb.disabled = true; + } else { + // Hide the viewport drag button vmb.style.display = "none"; } }, From 31ddaa1c7a4f2a8f211f53da1b53bfe294ad6a38 Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 24 Jun 2015 16:20:03 +0200 Subject: [PATCH 081/527] Clarify code with regards to the viewport drag functionality * Fixes #502 so that the viewport drag functionality can't get stuck --- include/ui.js | 85 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/include/ui.js b/include/ui.js index ba04a859..fafafdcf 100644 --- a/include/ui.js +++ b/include/ui.js @@ -127,7 +127,7 @@ var UI; Util.addEvent(window, 'resize', function () { UI.onresize(); UI.setViewClip(); - UI.updateViewDragButton(); + UI.updateViewDrag(); UI.setBarPosition(); } ); @@ -165,7 +165,7 @@ var UI; 'onXvpInit': UI.updateXvpVisualState, 'onClipboard': UI.clipReceive, 'onFBUComplete': UI.FBUComplete, - 'onFBResize': UI.updateViewDragButton, + 'onFBResize': UI.updateViewDrag, 'onDesktopName': UI.updateDocumentTitle}); return true; } catch (exc) { @@ -176,7 +176,7 @@ var UI; addMouseHandlers: function() { // Setup interface handlers that can't be inline - $D("noVNC_view_drag_button").onclick = UI.setViewDrag; + $D("noVNC_view_drag_button").onclick = UI.toggleViewDrag; $D("noVNC_mouse_button0").onclick = function () { UI.setMouseButton(1); }; $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; @@ -565,7 +565,7 @@ var UI; WebUtil.selectStylesheet(UI.getSetting('stylesheet')); WebUtil.init_logging(UI.getSetting('logging')); UI.setViewClip(); - UI.setViewDrag(UI.rfb && UI.rfb.get_viewportDrag()); + UI.updateViewDrag(); //Util.Debug("<< settingsApply"); }, @@ -696,8 +696,7 @@ var UI; // State change disables viewport dragging. // It is enabled (toggled) by direct click on the button - UI.setViewDrag(false); - UI.updateViewDragButton(); + UI.updateViewDrag(false); switch (UI.rfb_state) { case 'fatal': @@ -859,6 +858,7 @@ var UI; // Turn clipping off UI.updateSetting('clip', false); display.set_viewport(false); + // Disable max dimensions display.set_maxWidth(0); display.set_maxHeight(0); display.viewportChangeSize(); @@ -886,43 +886,64 @@ var UI; } }, - // Toggle/set/unset the viewport drag/move button - setViewDrag: function(drag) { + // Update the viewport drag/move button + updateViewDrag: function(drag) { if (!UI.rfb) return; - if (typeof(drag) === "undefined" || - typeof(drag) === "object") { - // If not specified, then toggle - drag = !UI.rfb.get_viewportDrag(); - } var vmb = $D('noVNC_view_drag_button'); - if (drag) { - vmb.className = "noVNC_status_button_selected"; - UI.rfb.set_viewportDrag(true); - } else { - vmb.className = "noVNC_status_button"; - UI.rfb.set_viewportDrag(false); - } - }, - updateViewDragButton: function() { - var vmb = $D('noVNC_view_drag_button'); + // Check if viewport drag is possible if (UI.rfb_state === 'normal' && UI.rfb.get_display().get_viewport() && UI.rfb.get_display().clippingDisplay()) { - // Enable the viewport drag button + + // Show and enable the drag button vmb.style.display = "inline"; vmb.disabled = false; - } else if (UI.rfb_state === 'normal' && - UI.isTouchDevice) { - // Disable the viewport drag button - vmb.style.display = "inline"; - vmb.disabled = true; - } else { - // Hide the viewport drag button - vmb.style.display = "none"; + // The VNC content is the same size as + // or smaller than the display + + if (UI.rfb.get_viewportDrag) { + // Turn off viewport drag when it's + // active since it can't be used here + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + + // Disable or hide the drag button + if (UI.rfb_state === 'normal' && UI.isTouchDevice) { + vmb.style.display = "inline"; + vmb.disabled = true; + } else { + vmb.style.display = "none"; + } + return; + } + + if (typeof(drag) !== "undefined" && + typeof(drag) !== "object") { + if (drag) { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); + } else { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } + } + }, + + toggleViewDrag: function() { + if (!UI.rfb) return; + + var vmb = $D('noVNC_view_drag_button'); + if (UI.rfb.get_viewportDrag()) { + vmb.className = "noVNC_status_button"; + UI.rfb.set_viewportDrag(false); + } else { + vmb.className = "noVNC_status_button_selected"; + UI.rfb.set_viewportDrag(true); } }, From 4f19e5c697685f759771e2960d890a78cf5eae55 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 15:22:53 +0200 Subject: [PATCH 082/527] Allow the popupStatusPanel to show any text but close it on a 1.5 second timer. --- include/ui.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/include/ui.js b/include/ui.js index fafafdcf..fe9c706c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -29,6 +29,7 @@ var UI; settingsOpen : false, connSettingsOpen : false, popupStatusOpen : false, + popupTimeout: null, clipboardOpen: false, keyboardVisible: false, hideKeyboardTimeout: null, @@ -356,17 +357,30 @@ var UI; // Show the popup status panel - togglePopupStatusPanel: function() { + togglePopupStatusPanel: function(text) { var psp = $D('noVNC_popup_status_panel'); - if (UI.popupStatusOpen === true) { + + var closePopup = function() { psp.style.display = "none"; UI.popupStatusOpen = false; + }; + + if (UI.popupStatusOpen === true) { + clearTimeout(UI.popupTimeout); + closePopup(); } else { - psp.innerHTML = $D('noVNC_status').innerHTML; + if (typeof text === 'text') { + psp.innerHTML = text; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + } psp.style.display = "block"; psp.style.left = window.innerWidth/2 - parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; UI.popupStatusOpen = true; + + // Show the popup for a maximum of 1.5 seconds + UI.popupTimeout = setTimeout(function() { closePopup(); }, 1500); } }, From 30bfff81d977da82b37c99a6aa77d7ecb9b7f773 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 15:29:23 +0200 Subject: [PATCH 083/527] Clarified enableDisableClip (now called enableDisableViewClip) and moved it to where the other clipping functions are. --- include/ui.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/include/ui.js b/include/ui.js index fe9c706c..15fb15c1 100644 --- a/include/ui.js +++ b/include/ui.js @@ -220,7 +220,7 @@ var UI; $D("noVNC_resize").onchange = function () { var connected = UI.rfb && UI.rfb_state === 'normal'; - UI.enableDisableClip(connected); + UI.enableDisableViewClip(connected); }; }, @@ -685,7 +685,7 @@ var UI; $D('noVNC_cursor').disabled = true; } - UI.enableDisableClip(connected); + UI.enableDisableViewClip(connected); $D('noVNC_resize').disabled = connected; $D('noVNC_shared').disabled = connected; $D('noVNC_view_only').disabled = connected; @@ -747,19 +747,6 @@ var UI; } }, - enableDisableClip: function (connected) { - var resizeElem = $D('noVNC_resize'); - if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { - UI.forceSetting('clip', false); - $D('noVNC_clip').disabled = true; - } else { - $D('noVNC_clip').disabled = connected || UI.isTouchDevice; - if (UI.isTouchDevice) { - UI.forceSetting('clip', true); - } - } - }, - // This resize can not be done until we know from the first Frame Buffer Update // if it is supported or not. // The resize is needed to make sure the server desktop size is updated to the @@ -849,7 +836,7 @@ var UI; Util.Debug("<< UI.clipSend"); }, - // Enable/disable and configure viewport clipping + // Set and configure viewport clipping setViewClip: function(clip) { var display; if (UI.rfb) { @@ -900,6 +887,20 @@ var UI; } }, + // Handle special cases where clipping is forced on/off or locked + enableDisableViewClip: function (connected) { + var resizeElem = $D('noVNC_resize'); + if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { + UI.forceSetting('clip', false); + $D('noVNC_clip').disabled = true; + } else { + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + if (UI.isTouchDevice) { + UI.forceSetting('clip', true); + } + } + }, + // Update the viewport drag/move button updateViewDrag: function(drag) { if (!UI.rfb) return; From 74f2ac968d782318967d9162454ca945dcc8b3de Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 17:04:55 +0200 Subject: [PATCH 084/527] Renamed popupStatusPanel to popupStatus and removed the global variable which kept track on if it was open or not (it automatically closes after 1.5 seconds anyway). --- include/base.css | 2 +- include/ui.js | 47 +++++++++++++---------------------------------- vnc.html | 4 ++-- 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/include/base.css b/include/base.css index 888b5f27..f69e64b7 100644 --- a/include/base.css +++ b/include/base.css @@ -188,7 +188,7 @@ html { border-radius:10px; } -#noVNC_popup_status_panel { +#noVNC_popup_status { display:none; position: fixed; z-index: 1; diff --git a/include/ui.js b/include/ui.js index 15fb15c1..6cfe5b5a 100644 --- a/include/ui.js +++ b/include/ui.js @@ -28,8 +28,7 @@ var UI; rfb_state : 'loaded', settingsOpen : false, connSettingsOpen : false, - popupStatusOpen : false, - popupTimeout: null, + popupStatusTimeout: null, clipboardOpen: false, keyboardVisible: false, hideKeyboardTimeout: null, @@ -198,8 +197,8 @@ var UI; $D("xvpShutdownButton").onclick = UI.xvpShutdown; $D("xvpRebootButton").onclick = UI.xvpReboot; $D("xvpResetButton").onclick = UI.xvpReset; - $D("noVNC_status").onclick = UI.togglePopupStatusPanel; - $D("noVNC_popup_status_panel").onclick = UI.togglePopupStatusPanel; + $D("noVNC_status").onclick = UI.togglePopupStatus; + $D("noVNC_popup_status").onclick = UI.togglePopupStatus; $D("xvpButton").onclick = UI.toggleXvpPanel; $D("clipboardButton").onclick = UI.toggleClipboardPanel; $D("settingsButton").onclick = UI.toggleSettingsPanel; @@ -356,19 +355,13 @@ var UI; }, - // Show the popup status panel - togglePopupStatusPanel: function(text) { - var psp = $D('noVNC_popup_status_panel'); + // Show the popup status + togglePopupStatus: function(text) { + var psp = $D('noVNC_popup_status'); - var closePopup = function() { - psp.style.display = "none"; - UI.popupStatusOpen = false; - }; + var closePopup = function() { psp.style.display = "none"; }; - if (UI.popupStatusOpen === true) { - clearTimeout(UI.popupTimeout); - closePopup(); - } else { + if (window.getComputedStyle(psp).display === 'none') { if (typeof text === 'text') { psp.innerHTML = text; } else { @@ -376,11 +369,13 @@ var UI; } psp.style.display = "block"; psp.style.left = window.innerWidth/2 - - parseInt(window.getComputedStyle(psp, false).width)/2 -30 + "px"; - UI.popupStatusOpen = true; + parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; // Show the popup for a maximum of 1.5 seconds - UI.popupTimeout = setTimeout(function() { closePopup(); }, 1500); + UI.popupStatusTimeout = setTimeout(function() { closePopup(); }, 1500); + } else { + clearTimeout(UI.popupStatusTimeout); + closePopup(); } }, @@ -397,10 +392,6 @@ var UI; if (UI.connSettingsOpen === true) { UI.toggleConnectPanel(); } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } // Close clipboard panel if open if (UI.clipboardOpen === true) { UI.toggleClipboardPanel(); @@ -430,10 +421,6 @@ var UI; if (UI.connSettingsOpen === true) { UI.toggleConnectPanel(); } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } // Close XVP panel if open if (UI.xvpOpen === true) { UI.toggleXvpPanel(); @@ -464,10 +451,6 @@ var UI; if (UI.clipboardOpen === true) { UI.toggleClipboardPanel(); } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } // Close XVP panel if open if (UI.xvpOpen === true) { UI.toggleXvpPanel(); @@ -532,10 +515,6 @@ var UI; if (UI.connSettingsOpen === true) { UI.toggleConnectPanel(); } - // Close popup status panel if open - if (UI.popupStatusOpen === true) { - UI.togglePopupStatusPanel(); - } // Close XVP panel if open if (UI.xvpOpen === true) { UI.toggleXvpPanel(); diff --git a/vnc.html b/vnc.html index 4c4aeb1c..6acd7929 100644 --- a/vnc.html +++ b/vnc.html @@ -126,8 +126,8 @@
    - -
    + +
    From b098afc234528af8d3e6d708226d8839d8132d20 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 22:34:44 +0200 Subject: [PATCH 085/527] Fix error from a previous commit, there is no data type called 'text'.. --- include/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index 6cfe5b5a..3d3c41e3 100644 --- a/include/ui.js +++ b/include/ui.js @@ -362,7 +362,7 @@ var UI; var closePopup = function() { psp.style.display = "none"; }; if (window.getComputedStyle(psp).display === 'none') { - if (typeof text === 'text') { + if (typeof text === 'string') { psp.innerHTML = text; } else { psp.innerHTML = $D('noVNC_status').innerHTML; From 7d1dc09ad0010924c6940832225e5eb2dc803f4f Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 18 Jun 2015 10:45:59 +0200 Subject: [PATCH 086/527] Fixes #498 - Add the ability to toggle fullscreen mode --- images/fullscreen.png | Bin 0 -> 851 bytes include/ui.js | 54 ++++++++++++++++++++++++++++++++++++++++++ vnc.html | 3 +++ 3 files changed, 57 insertions(+) create mode 100644 images/fullscreen.png diff --git a/images/fullscreen.png b/images/fullscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..f4fa0ce8321de23929618a8d82ec0e42c094bafc GIT binary patch literal 851 zcmV-Z1FZasP)P000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-va^(3nu(M(YOEr0^mtR zK~zY`-Bv+s;y@IRx`*0@QVPXFFCKc8$+u~>Y$TrSyqG#dTNQ4bJ8V_B9{+$SK*ata|d z_Hu^3hQr~oH=E7W&xp)sGj%u|_Et8$s*dA0jYi{w=lQUY?Yo2L`EaArxNscD@p5vM zQfiu}NeChCjb^j?a~-L7yxPBk5JF7TG%2N&!x&>pk`fTY%yc?6m^Vy_=lRVKJlJFc z?A&xZH6Vl;Ns&A=C`x)X8a;YExk{O$k%5NkG@mu~yfX!NKkN}5v2 zI1g81E=f|tEo9ra4Iw=76M+z(*tTuE1tdvIFfV<>GF@`}7|ghHQ56<>}p_Yd&@ zhXp-|WrMKKuzF~*fjfK@hf8Rc)~>HVk7T%W}$3K$hi{VHgu;tEy^C5QJ^7NSNpOD4XPRxs7Z# zd!0-s(`=0p^1N6qUi?04u~@tiLY~vTG|`Fws00I=8X zcJJKPwBPUd%H?u$T`wt@%gKJf-*cbqcDr`~fW3S^zt!nkoc>D>vAZXilU$7{O2ZO;t_j0ZzBO&B@KA%6i*Mb(y`F#H5ehBaWNU(Scg0S-^SuF^{ dj`v;}eggqm-~P^IymkNp002ovPDHLkV1kszf;IpE literal 0 HcmV?d00001 diff --git a/include/ui.js b/include/ui.js index 3d3c41e3..0d9ad82c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -131,6 +131,19 @@ var UI; UI.setBarPosition(); } ); + // Hide the button if fullscreen isn't supported + if (!document.documentElement.requestFullscreen && + !document.documentElement.mozRequestFullScreen && + !document.documentElement.webkitRequestFullscreen && + !document.body.msRequestFullscreen) { + $D('fullscreenButton').style.display = "none"; + } else { + Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); + Util.addEvent(window, 'msfullscreenchange', UI.updateFullscreenButton); + } + Util.addEvent(window, 'load', UI.keyboardinputReset); Util.addEvent(window, 'beforeunload', function () { @@ -201,6 +214,7 @@ var UI; $D("noVNC_popup_status").onclick = UI.togglePopupStatus; $D("xvpButton").onclick = UI.toggleXvpPanel; $D("clipboardButton").onclick = UI.toggleClipboardPanel; + $D("fullscreenButton").onclick = UI.toggleFullscreen; $D("settingsButton").onclick = UI.toggleSettingsPanel; $D("connectButton").onclick = UI.toggleConnectPanel; $D("disconnectButton").onclick = UI.disconnect; @@ -437,6 +451,46 @@ var UI; } }, + // Toggle fullscreen mode + toggleFullscreen: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + } + UI.updateFullscreenButton(); + }, + + updateFullscreenButton: function() { + if (document.fullscreenElement || // alternative standard method + document.mozFullScreenElement || // currently working methods + document.webkitFullscreenElement || + document.msFullscreenElement ) { + $D('fullscreenButton').className = "noVNC_status_button_selected"; + } else { + $D('fullscreenButton').className = "noVNC_status_button"; + } + }, + // Show the connection settings panel/menu toggleConnectPanel: function() { // Close the description panel diff --git a/vnc.html b/vnc.html index 6acd7929..1a293d09 100644 --- a/vnc.html +++ b/vnc.html @@ -99,6 +99,9 @@ + From 6e296bfa8acbc19d8fd0d5ba5836f902e48ce09f Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 14:58:57 +0200 Subject: [PATCH 087/527] Adapt display to be able to handle a changing clipping-setting while connected --- include/display.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/include/display.js b/include/display.js index 9a8e4559..8994856b 100644 --- a/include/display.js +++ b/include/display.js @@ -229,13 +229,14 @@ var Display; saveImg = this._drawCtx.getImageData(0, 0, img_width, img_height); } - if (canvas.width !== width) { canvas.width = width; } - if (canvas.height !== height) { canvas.height = height; } - - if (this._viewport) { - canvas.style.height = height + 'px'; + if (canvas.width !== width) { + canvas.width = width; canvas.style.width = width + 'px'; } + if (canvas.height !== height) { + canvas.height = height; + canvas.style.height = height + 'px'; + } if (saveImg) { this._drawCtx.putImageData(saveImg, 0, 0); From a6357e827611aa5b0f2a6082372ab4e06af8cfe9 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Jun 2015 22:28:04 +0200 Subject: [PATCH 088/527] * Hide the fullscreen toggle button on Safari since alphanumerical keyboard input doesn't work in fullscreen. * Force clipping mode in Internet Explorer while in fullscreen since scrollbars doesn't work in fullscreen. --- include/base.css | 3 +++ include/ui.js | 51 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/include/base.css b/include/base.css index f69e64b7..2769357e 100644 --- a/include/base.css +++ b/include/base.css @@ -59,6 +59,9 @@ html { #sendCtrlAltDelButton { display: none; } +#fullscreenButton { + display: none; +} #noVNC_xvp_buttons { display: none; } diff --git a/include/ui.js b/include/ui.js index 0d9ad82c..2e9d731a 100644 --- a/include/ui.js +++ b/include/ui.js @@ -38,6 +38,7 @@ var UI; ctrlOn: false, altOn: false, isTouchDevice: false, + rememberedClipSetting: null, // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus @@ -131,13 +132,17 @@ var UI; UI.setBarPosition(); } ); - // Hide the button if fullscreen isn't supported - if (!document.documentElement.requestFullscreen && - !document.documentElement.mozRequestFullScreen && - !document.documentElement.webkitRequestFullscreen && - !document.body.msRequestFullscreen) { - $D('fullscreenButton').style.display = "none"; - } else { + var isSafari = (navigator.userAgent.indexOf('Safari') != -1 && + navigator.userAgent.indexOf('Chrome') == -1); + + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!isSafari && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + $D('fullscreenButton').style.display = "inline"; Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); @@ -231,10 +236,7 @@ var UI; $D("noVNC_connect_button").onclick = UI.connect; - $D("noVNC_resize").onchange = function () { - var connected = UI.rfb && UI.rfb_state === 'normal'; - UI.enableDisableViewClip(connected); - }; + $D("noVNC_resize").onchange = UI.enableDisableViewClip; }, onresize: function (callback) { @@ -456,7 +458,7 @@ var UI; if (document.fullscreenElement || // alternative standard method document.mozFullScreenElement || // currently working methods document.webkitFullscreenElement || - document.msFullscreenElement ) { + document.msFullscreenElement) { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { @@ -477,6 +479,7 @@ var UI; document.body.msRequestFullscreen(); } } + UI.enableDisableViewClip(); UI.updateFullscreenButton(); }, @@ -718,7 +721,7 @@ var UI; $D('noVNC_cursor').disabled = true; } - UI.enableDisableViewClip(connected); + UI.enableDisableViewClip(); $D('noVNC_resize').disabled = connected; $D('noVNC_shared').disabled = connected; $D('noVNC_view_only').disabled = connected; @@ -875,6 +878,7 @@ var UI; if (UI.rfb) { display = UI.rfb.get_display(); } else { + UI.forceSetting('clip', clip); return; } @@ -921,15 +925,30 @@ var UI; }, // Handle special cases where clipping is forced on/off or locked - enableDisableViewClip: function (connected) { + enableDisableViewClip: function () { var resizeElem = $D('noVNC_resize'); + var connected = UI.rfb && UI.rfb_state === 'normal'; + if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { - UI.forceSetting('clip', false); + // Disable clipping if we are scaling + UI.setViewClip(false); $D('noVNC_clip').disabled = true; + } else if (document.msFullscreenElement) { + // The browser is IE and we are in fullscreen mode. + // - We need to force clipping while in fullscreen since + // scrollbars doesn't work. + UI.togglePopupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); + UI.rememberedClipSetting = UI.getSetting('clip'); + UI.setViewClip(true); + $D('noVNC_clip').disabled = true; + } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) { + // Restore view clip to what it was before fullscreen on IE + UI.setViewClip(UI.rememberedClipSetting); + $D('noVNC_clip').disabled = connected || UI.isTouchDevice; } else { $D('noVNC_clip').disabled = connected || UI.isTouchDevice; if (UI.isTouchDevice) { - UI.forceSetting('clip', true); + UI.setViewClip(true); } } }, From bc4414f5b15c348bdc8a2abde0f4a34420c0c71c Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 6 Aug 2015 14:37:01 -0400 Subject: [PATCH 089/527] CI: Switch to Container-Based Travis This commit switches the Travis tests over to Travis's new container-based infrastructure. It also tells Travis to cache the node_modules directory for faster setup. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 81922524..aa46a313 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,8 @@ language: node_js +sudo: false +cache: + directories: + - node_modules node_js: - '0.11.13' env: From efed2eeafd7f80f012fffbb18c05079db2bdbbd7 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 15 May 2015 14:27:23 -0400 Subject: [PATCH 090/527] Fix up vnc_playback.html and playback.js This commit fixes vnc_playback.html and playback.js so that they work with the current version of noVNC. --- include/playback.js | 27 ++++++++++++++++++++++----- tests/vnc_playback.html | 9 ++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/include/playback.js b/include/playback.js index 7756529d..7d3d749f 100644 --- a/include/playback.js +++ b/include/playback.js @@ -12,19 +12,36 @@ var rfb, mode, test_state, frame_idx, frame_length, iteration, iterations, istart_time, // Pre-declarations for jslint - send_array, next_iteration, queue_next_packet, do_packet; + send_array, next_iteration, queue_next_packet, do_packet, enable_test_mode; // Override send_array send_array = function (arr) { // Stub out send_array }; +enable_test_mode = function () { + rfb._sock._mode = VNC_frame_encoding; + rfb._sock.send = send_array; + rfb._sock.close = function () {}; + rfb._sock.flush = function () {}; + rfb._checkEvents = function () {}; + rfb.connect = function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; + this._updateState('ProtocolVersion', "Starting VNC handshake"); + }; +}; + next_iteration = function () { + rfb = new RFB({'target': $D('VNC_canvas'), + 'onUpdateState': updateState}); + enable_test_mode(); + if (iteration === 0) { frame_length = VNC_frame_data.length; test_state = 'running'; - } else { - rfb.disconnect(); } if (test_state !== 'running') { return; } @@ -91,9 +108,9 @@ do_packet = function () { for (var i = 0; i < frame.length - start; i++) { u8[i] = frame.charCodeAt(start + i); } - rfb.recv_message({'data' : u8}); + rfb._sock._recv_message({'data' : u8}); } else { - rfb.recv_message({'data' : frame.slice(start)}); + rfb._sock._recv_message({'data' : frame.slice(start)}); } frame_idx += 1; diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index b5faf93c..8b0207b2 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -59,7 +59,7 @@ if (fname) { message("Loading " + fname); // Load supporting scripts - Util.load_scripts(["base64.js", "websock.js", "des.js", + Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", "jsunzip.js", "rfb.js", "playback.js", fname]); @@ -75,7 +75,6 @@ test_state = 'failed'; break; case 'loaded': - $D('startButton').disabled = false; break; } if (typeof msg !== 'undefined') { @@ -99,7 +98,8 @@ mode = 'realtime'; } - recv_message = rfb.testMode(send_array, VNC_frame_encoding); + //recv_message = rfb.testMode(send_array, VNC_frame_encoding); + next_iteration(); } @@ -130,9 +130,8 @@ } if (fname) { message("VNC_frame_data.length: " + VNC_frame_data.length); - rfb = new RFB({'target': $D('VNC_canvas'), - 'onUpdateState': updateState}); } + $D('startButton').disabled = false; } From 0442e153a1cad88e823926e596bcb3cff0aacabe Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 4 Aug 2015 15:50:20 -0400 Subject: [PATCH 091/527] Fix RFB.js JSHint Errors This fixes a couple of JSHint errors in RFB.js caused by using `==` instead of `===`. --- include/rfb.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index a591ca2b..4e8f4087 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1903,9 +1903,9 @@ var RFB; this._sock.rQskipBytes(1); // number-of-screens this._sock.rQskipBytes(3); // padding - for (var i=0; i Date: Mon, 18 May 2015 19:01:58 -0400 Subject: [PATCH 092/527] WIP: Switch to Pako for zlib This commit introduces an alternate implementation of the zlib decompressor based on Pako (https://github.com/nodeca/pako). --- LICENSE.txt | 4 + README.md | 3 +- docs/LICENSE.pako | 21 + docs/notes | 6 + include/inflator.js | 2409 +++++++++++++++++++++++++++++++++++++ include/rfb.js | 24 +- include/ui.js | 2 +- karma.conf.js | 2 +- tests/test.rfb.js | 2 +- tests/vnc_playback.html | 2 +- utils/inflator.partial.js | 32 + vnc_auto.html | 2 +- 12 files changed, 2495 insertions(+), 14 deletions(-) create mode 100644 docs/LICENSE.pako create mode 100644 include/inflator.js create mode 100644 utils/inflator.partial.js diff --git a/LICENSE.txt b/LICENSE.txt index e896efca..638cd5a5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -59,6 +59,9 @@ licenses (all MPL 2.0 compatible): utils/websockify utils/websocket.py : LGPL 3 + + utils/inflator.partial.js + include/inflator.js : MIT (for pako) The following license texts are included: @@ -70,6 +73,7 @@ The following license texts are included: docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD) docs/LICENSE.zlib docs/LICENSE.Apache-2.0 + docs/LICENSE.pako Or alternatively the license texts may be found here: diff --git a/README.md b/README.md index b5679cdd..0f9a8f6f 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,5 @@ use a WebSockets to TCP socket proxy. There is a python proxy included * web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js) * as3crypto : Henri Torgemane (code.google.com/p/as3crypto) * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) - * jsunzip : Erik Moller (github.com/operasoftware/jsunzip), - * tinflate : Joergen Ibsen (ibsensoftware.com) * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) + * Pako : Vitaly Puzrin (https://github.com/nodeca/pako) diff --git a/docs/LICENSE.pako b/docs/LICENSE.pako new file mode 100644 index 00000000..e6c9e5a5 --- /dev/null +++ b/docs/LICENSE.pako @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (C) 2014 by Vitaly Puzrin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/notes b/docs/notes index 9bcc6af3..443dbd41 100644 --- a/docs/notes +++ b/docs/notes @@ -15,3 +15,9 @@ Building web-socket-js emulator: cd include/web-socket-js/flash-src mxmlc -static-link-runtime-shared-libraries WebSocketMain.as + +Rebuilding inflator.js + +- Download pako from npm +- Install browserify using npm +- browserify utils/inflator.partial.js -o include/inflator.js diff --git a/include/inflator.js b/include/inflator.js new file mode 100644 index 00000000..68f85cbd --- /dev/null +++ b/include/inflator.js @@ -0,0 +1,2409 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.inflator = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o>> 16) & 0xffff) |0, + n = 0; + + while (len !== 0) { + // Set limit ~ twice less than 5552, to keep + // s2 in 31-bits, because we force signed ints. + // in other case %= will fail. + n = len > 2000 ? 2000 : len; + len -= n; + + do { + s1 = (s1 + buf[pos++]) |0; + s2 = (s2 + s1) |0; + } while (--n); + + s1 %= 65521; + s2 %= 65521; + } + + return (s1 | (s2 << 16)) |0; +} + + +module.exports = adler32; + +},{}],3:[function(require,module,exports){ +'use strict'; + +// Note: we can't get significant speed boost here. +// So write code to minimize size - no pregenerated tables +// and array tools dependencies. + + +// Use ordinary array, since untyped makes no boost here +function makeTable() { + var c, table = []; + + for (var n =0; n < 256; n++) { + c = n; + for (var k =0; k < 8; k++) { + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + table[n] = c; + } + + return table; +} + +// Create table on load. Just 255 signed longs. Not a problem. +var crcTable = makeTable(); + + +function crc32(crc, buf, len, pos) { + var t = crcTable, + end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++) { + crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + + +module.exports = crc32; + +},{}],4:[function(require,module,exports){ +'use strict'; + +// See state defs from inflate.js +var BAD = 30; /* got a data error -- remain here until reset */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ + +/* + Decode literal, length, and distance codes and write out the resulting + literal and match bytes until either not enough input or output is + available, an end-of-block is encountered, or a data error is encountered. + When large enough input and output buffers are supplied to inflate(), for + example, a 16K input buffer and a 64K output buffer, more than 95% of the + inflate execution time is spent in this routine. + + Entry assumptions: + + state.mode === LEN + strm.avail_in >= 6 + strm.avail_out >= 258 + start >= strm.avail_out + state.bits < 8 + + On return, state.mode is one of: + + LEN -- ran out of enough output space or enough available input + TYPE -- reached end of block code, inflate() to interpret next block + BAD -- error in block data + + Notes: + + - The maximum input bits used by a length/distance pair is 15 bits for the + length code, 5 bits for the length extra, 15 bits for the distance code, + and 13 bits for the distance extra. This totals 48 bits, or six bytes. + Therefore if strm.avail_in >= 6, then there is enough input to avoid + checking for available input while decoding. + + - The maximum bytes that a single length/distance pair can output is 258 + bytes, which is the maximum length that can be coded. inflate_fast() + requires strm.avail_out >= 258 for each loop to avoid checking for + output space. + */ +module.exports = function inflate_fast(strm, start) { + var state; + var _in; /* local strm.input */ + var last; /* have enough input while in < last */ + var _out; /* local strm.output */ + var beg; /* inflate()'s initial strm.output */ + var end; /* while out < end, enough space available */ +//#ifdef INFLATE_STRICT + var dmax; /* maximum distance from zlib header */ +//#endif + var wsize; /* window size or zero if not using window */ + var whave; /* valid bytes in the window */ + var wnext; /* window write index */ + var window; /* allocated sliding window, if wsize != 0 */ + var hold; /* local strm.hold */ + var bits; /* local strm.bits */ + var lcode; /* local strm.lencode */ + var dcode; /* local strm.distcode */ + var lmask; /* mask for first level of length codes */ + var dmask; /* mask for first level of distance codes */ + var here; /* retrieved table entry */ + var op; /* code bits, operation, extra bits, or */ + /* window position, window bytes to copy */ + var len; /* match length, unused bytes */ + var dist; /* match distance */ + var from; /* where to copy match from */ + var from_source; + + + var input, output; // JS specific, because we have no pointers + + /* copy state to local variables */ + state = strm.state; + //here = state.here; + _in = strm.next_in; + input = strm.input; + last = _in + (strm.avail_in - 5); + _out = strm.next_out; + output = strm.output; + beg = _out - (start - strm.avail_out); + end = _out + (strm.avail_out - 257); +//#ifdef INFLATE_STRICT + dmax = state.dmax; +//#endif + wsize = state.wsize; + whave = state.whave; + wnext = state.wnext; + window = state.window; + hold = state.hold; + bits = state.bits; + lcode = state.lencode; + dcode = state.distcode; + lmask = (1 << state.lenbits) - 1; + dmask = (1 << state.distbits) - 1; + + + /* decode literals and length/distances until end-of-block or not enough + input data or output space */ + + top: + do { + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + + here = lcode[hold & lmask]; + + dolen: + for (;;) { // Goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + if (op === 0) { /* literal */ + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + output[_out++] = here & 0xffff/*here.val*/; + } + else if (op & 16) { /* length base */ + len = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (op) { + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + len += hold & ((1 << op) - 1); + hold >>>= op; + bits -= op; + } + //Tracevv((stderr, "inflate: length %u\n", len)); + if (bits < 15) { + hold += input[_in++] << bits; + bits += 8; + hold += input[_in++] << bits; + bits += 8; + } + here = dcode[hold & dmask]; + + dodist: + for (;;) { // goto emulation + op = here >>> 24/*here.bits*/; + hold >>>= op; + bits -= op; + op = (here >>> 16) & 0xff/*here.op*/; + + if (op & 16) { /* distance base */ + dist = here & 0xffff/*here.val*/; + op &= 15; /* number of extra bits */ + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + if (bits < op) { + hold += input[_in++] << bits; + bits += 8; + } + } + dist += hold & ((1 << op) - 1); +//#ifdef INFLATE_STRICT + if (dist > dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } +//#endif + hold >>>= op; + bits -= op; + //Tracevv((stderr, "inflate: distance %u\n", dist)); + op = _out - beg; /* max distance in output */ + if (dist > op) { /* see if copy from window */ + op = dist - op; /* distance back in window */ + if (op > whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break top; + } + +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// if (len <= op - whave) { +// do { +// output[_out++] = 0; +// } while (--len); +// continue top; +// } +// len -= op - whave; +// do { +// output[_out++] = 0; +// } while (--op > whave); +// if (op === 0) { +// from = _out - dist; +// do { +// output[_out++] = output[from++]; +// } while (--len); +// continue top; +// } +//#endif + } + from = 0; // window index + from_source = window; + if (wnext === 0) { /* very common case */ + from += wsize - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + else if (wnext < op) { /* wrap around window */ + from += wsize + wnext - op; + op -= wnext; + if (op < len) { /* some from end of window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = 0; + if (wnext < len) { /* some from start of window */ + op = wnext; + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + } + else { /* contiguous in window */ + from += wnext - op; + if (op < len) { /* some from window */ + len -= op; + do { + output[_out++] = window[from++]; + } while (--op); + from = _out - dist; /* rest from output */ + from_source = output; + } + } + while (len > 2) { + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + output[_out++] = from_source[from++]; + len -= 3; + } + if (len) { + output[_out++] = from_source[from++]; + if (len > 1) { + output[_out++] = from_source[from++]; + } + } + } + else { + from = _out - dist; /* copy direct from output */ + do { /* minimum length is three */ + output[_out++] = output[from++]; + output[_out++] = output[from++]; + output[_out++] = output[from++]; + len -= 3; + } while (len > 2); + if (len) { + output[_out++] = output[from++]; + if (len > 1) { + output[_out++] = output[from++]; + } + } + } + } + else if ((op & 64) === 0) { /* 2nd level distance code */ + here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dodist; + } + else { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } + else if ((op & 64) === 0) { /* 2nd level length code */ + here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))]; + continue dolen; + } + else if (op & 32) { /* end-of-block */ + //Tracevv((stderr, "inflate: end of block\n")); + state.mode = TYPE; + break top; + } + else { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break top; + } + + break; // need to emulate goto via "continue" + } + } while (_in < last && _out < end); + + /* return unused bytes (on entry, bits < 8, so in won't go too far back) */ + len = bits >> 3; + _in -= len; + bits -= len << 3; + hold &= (1 << bits) - 1; + + /* update state and return */ + strm.next_in = _in; + strm.next_out = _out; + strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last)); + strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end)); + state.hold = hold; + state.bits = bits; + return; +}; + +},{}],5:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); +var adler32 = require('./adler32'); +var crc32 = require('./crc32'); +var inflate_fast = require('./inffast'); +var inflate_table = require('./inftrees'); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +/* Public constants ==========================================================*/ +/* ===========================================================================*/ + + +/* Allowed flush values; see deflate() and inflate() below for details */ +//var Z_NO_FLUSH = 0; +//var Z_PARTIAL_FLUSH = 1; +//var Z_SYNC_FLUSH = 2; +//var Z_FULL_FLUSH = 3; +var Z_FINISH = 4; +var Z_BLOCK = 5; +var Z_TREES = 6; + + +/* Return codes for the compression/decompression functions. Negative values + * are errors, positive values are used for special but normal events. + */ +var Z_OK = 0; +var Z_STREAM_END = 1; +var Z_NEED_DICT = 2; +//var Z_ERRNO = -1; +var Z_STREAM_ERROR = -2; +var Z_DATA_ERROR = -3; +var Z_MEM_ERROR = -4; +var Z_BUF_ERROR = -5; +//var Z_VERSION_ERROR = -6; + +/* The deflate compression method */ +var Z_DEFLATED = 8; + + +/* STATES ====================================================================*/ +/* ===========================================================================*/ + + +var HEAD = 1; /* i: waiting for magic header */ +var FLAGS = 2; /* i: waiting for method and flags (gzip) */ +var TIME = 3; /* i: waiting for modification time (gzip) */ +var OS = 4; /* i: waiting for extra flags and operating system (gzip) */ +var EXLEN = 5; /* i: waiting for extra length (gzip) */ +var EXTRA = 6; /* i: waiting for extra bytes (gzip) */ +var NAME = 7; /* i: waiting for end of file name (gzip) */ +var COMMENT = 8; /* i: waiting for end of comment (gzip) */ +var HCRC = 9; /* i: waiting for header crc (gzip) */ +var DICTID = 10; /* i: waiting for dictionary check value */ +var DICT = 11; /* waiting for inflateSetDictionary() call */ +var TYPE = 12; /* i: waiting for type bits, including last-flag bit */ +var TYPEDO = 13; /* i: same, but skip check to exit inflate on new block */ +var STORED = 14; /* i: waiting for stored size (length and complement) */ +var COPY_ = 15; /* i/o: same as COPY below, but only first time in */ +var COPY = 16; /* i/o: waiting for input or output to copy stored block */ +var TABLE = 17; /* i: waiting for dynamic block table lengths */ +var LENLENS = 18; /* i: waiting for code length code lengths */ +var CODELENS = 19; /* i: waiting for length/lit and distance code lengths */ +var LEN_ = 20; /* i: same as LEN below, but only first time in */ +var LEN = 21; /* i: waiting for length/lit/eob code */ +var LENEXT = 22; /* i: waiting for length extra bits */ +var DIST = 23; /* i: waiting for distance code */ +var DISTEXT = 24; /* i: waiting for distance extra bits */ +var MATCH = 25; /* o: waiting for output space to copy string */ +var LIT = 26; /* o: waiting for output space to write literal */ +var CHECK = 27; /* i: waiting for 32-bit check value */ +var LENGTH = 28; /* i: waiting for 32-bit length (gzip) */ +var DONE = 29; /* finished check, done -- remain here until reset */ +var BAD = 30; /* got a data error -- remain here until reset */ +var MEM = 31; /* got an inflate() memory error -- remain here until reset */ +var SYNC = 32; /* looking for synchronization bytes to restart inflate() */ + +/* ===========================================================================*/ + + + +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var MAX_WBITS = 15; +/* 32K LZ77 window */ +var DEF_WBITS = MAX_WBITS; + + +function ZSWAP32(q) { + return (((q >>> 24) & 0xff) + + ((q >>> 8) & 0xff00) + + ((q & 0xff00) << 8) + + ((q & 0xff) << 24)); +} + + +function InflateState() { + this.mode = 0; /* current inflate mode */ + this.last = false; /* true if processing last block */ + this.wrap = 0; /* bit 0 true for zlib, bit 1 true for gzip */ + this.havedict = false; /* true if dictionary provided */ + this.flags = 0; /* gzip header method and flags (0 if zlib) */ + this.dmax = 0; /* zlib header max distance (INFLATE_STRICT) */ + this.check = 0; /* protected copy of check value */ + this.total = 0; /* protected copy of output count */ + // TODO: may be {} + this.head = null; /* where to save gzip header information */ + + /* sliding window */ + this.wbits = 0; /* log base 2 of requested window size */ + this.wsize = 0; /* window size or zero if not using window */ + this.whave = 0; /* valid bytes in the window */ + this.wnext = 0; /* window write index */ + this.window = null; /* allocated sliding window, if needed */ + + /* bit accumulator */ + this.hold = 0; /* input bit accumulator */ + this.bits = 0; /* number of bits in "in" */ + + /* for string and stored block copying */ + this.length = 0; /* literal or length of data to copy */ + this.offset = 0; /* distance back to copy string from */ + + /* for table and code decoding */ + this.extra = 0; /* extra bits needed */ + + /* fixed and dynamic code tables */ + this.lencode = null; /* starting table for length/literal codes */ + this.distcode = null; /* starting table for distance codes */ + this.lenbits = 0; /* index bits for lencode */ + this.distbits = 0; /* index bits for distcode */ + + /* dynamic table building */ + this.ncode = 0; /* number of code length code lengths */ + this.nlen = 0; /* number of length code lengths */ + this.ndist = 0; /* number of distance code lengths */ + this.have = 0; /* number of code lengths in lens[] */ + this.next = null; /* next available space in codes[] */ + + this.lens = new utils.Buf16(320); /* temporary storage for code lengths */ + this.work = new utils.Buf16(288); /* work area for code table building */ + + /* + because we don't have pointers in js, we use lencode and distcode directly + as buffers so we don't need codes + */ + //this.codes = new utils.Buf32(ENOUGH); /* space for code tables */ + this.lendyn = null; /* dynamic table for length/literal codes (JS specific) */ + this.distdyn = null; /* dynamic table for distance codes (JS specific) */ + this.sane = 0; /* if false, allow invalid distance too far */ + this.back = 0; /* bits back of last unprocessed length/lit */ + this.was = 0; /* initial length of match */ +} + +function inflateResetKeep(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + strm.total_in = strm.total_out = state.total = 0; + strm.msg = ''; /*Z_NULL*/ + if (state.wrap) { /* to support ill-conceived Java test suite */ + strm.adler = state.wrap & 1; + } + state.mode = HEAD; + state.last = 0; + state.havedict = 0; + state.dmax = 32768; + state.head = null/*Z_NULL*/; + state.hold = 0; + state.bits = 0; + //state.lencode = state.distcode = state.next = state.codes; + state.lencode = state.lendyn = new utils.Buf32(ENOUGH_LENS); + state.distcode = state.distdyn = new utils.Buf32(ENOUGH_DISTS); + + state.sane = 1; + state.back = -1; + //Tracev((stderr, "inflate: reset\n")); + return Z_OK; +} + +function inflateReset(strm) { + var state; + + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + state.wsize = 0; + state.whave = 0; + state.wnext = 0; + return inflateResetKeep(strm); + +} + +function inflateReset2(strm, windowBits) { + var wrap; + var state; + + /* get the state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + + /* extract wrap request from windowBits parameter */ + if (windowBits < 0) { + wrap = 0; + windowBits = -windowBits; + } + else { + wrap = (windowBits >> 4) + 1; + if (windowBits < 48) { + windowBits &= 15; + } + } + + /* set number of window bits, free window if different */ + if (windowBits && (windowBits < 8 || windowBits > 15)) { + return Z_STREAM_ERROR; + } + if (state.window !== null && state.wbits !== windowBits) { + state.window = null; + } + + /* update state and reset the rest of it */ + state.wrap = wrap; + state.wbits = windowBits; + return inflateReset(strm); +} + +function inflateInit2(strm, windowBits) { + var ret; + var state; + + if (!strm) { return Z_STREAM_ERROR; } + //strm.msg = Z_NULL; /* in case we return an error */ + + state = new InflateState(); + + //if (state === Z_NULL) return Z_MEM_ERROR; + //Tracev((stderr, "inflate: allocated\n")); + strm.state = state; + state.window = null/*Z_NULL*/; + ret = inflateReset2(strm, windowBits); + if (ret !== Z_OK) { + strm.state = null/*Z_NULL*/; + } + return ret; +} + +function inflateInit(strm) { + return inflateInit2(strm, DEF_WBITS); +} + + +/* + Return state with length and distance decoding tables and index sizes set to + fixed code decoding. Normally this returns fixed tables from inffixed.h. + If BUILDFIXED is defined, then instead this routine builds the tables the + first time it's called, and returns those tables the first time and + thereafter. This reduces the size of the code by about 2K bytes, in + exchange for a little execution time. However, BUILDFIXED should not be + used for threaded applications, since the rewriting of the tables and virgin + may not be thread-safe. + */ +var virgin = true; + +var lenfix, distfix; // We have no pointers in JS, so keep tables separate + +function fixedtables(state) { + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + var sym; + + lenfix = new utils.Buf32(512); + distfix = new utils.Buf32(32); + + /* literal/length table */ + sym = 0; + while (sym < 144) { state.lens[sym++] = 8; } + while (sym < 256) { state.lens[sym++] = 9; } + while (sym < 280) { state.lens[sym++] = 7; } + while (sym < 288) { state.lens[sym++] = 8; } + + inflate_table(LENS, state.lens, 0, 288, lenfix, 0, state.work, {bits: 9}); + + /* distance table */ + sym = 0; + while (sym < 32) { state.lens[sym++] = 5; } + + inflate_table(DISTS, state.lens, 0, 32, distfix, 0, state.work, {bits: 5}); + + /* do this just once */ + virgin = false; + } + + state.lencode = lenfix; + state.lenbits = 9; + state.distcode = distfix; + state.distbits = 5; +} + + +/* + Update the window with the last wsize (normally 32K) bytes written before + returning. If window does not exist yet, create it. This is only called + when a window is already in use, or when output has been written during this + inflate call, but the end of the deflate stream has not been reached yet. + It is also called to create a window for dictionary data when a dictionary + is loaded. + + Providing output buffers larger than 32K to inflate() should provide a speed + advantage, since only the last 32K of output is copied to the sliding window + upon return from inflate(), and since all distances after the first 32K of + output will fall in the output data, making match copies simpler and faster. + The advantage may be dependent on the size of the processor's data caches. + */ +function updatewindow(strm, src, end, copy) { + var dist; + var state = strm.state; + + /* if it hasn't been done already, allocate space for the window */ + if (state.window === null) { + state.wsize = 1 << state.wbits; + state.wnext = 0; + state.whave = 0; + + state.window = new utils.Buf8(state.wsize); + } + + /* copy state->wsize or less output bytes into the circular window */ + if (copy >= state.wsize) { + utils.arraySet(state.window,src, end - state.wsize, state.wsize, 0); + state.wnext = 0; + state.whave = state.wsize; + } + else { + dist = state.wsize - state.wnext; + if (dist > copy) { + dist = copy; + } + //zmemcpy(state->window + state->wnext, end - copy, dist); + utils.arraySet(state.window,src, end - copy, dist, state.wnext); + copy -= dist; + if (copy) { + //zmemcpy(state->window, end - copy, copy); + utils.arraySet(state.window,src, end - copy, copy, 0); + state.wnext = copy; + state.whave = state.wsize; + } + else { + state.wnext += dist; + if (state.wnext === state.wsize) { state.wnext = 0; } + if (state.whave < state.wsize) { state.whave += dist; } + } + } + return 0; +} + +function inflate(strm, flush) { + var state; + var input, output; // input/output buffers + var next; /* next input INDEX */ + var put; /* next output INDEX */ + var have, left; /* available input and output */ + var hold; /* bit buffer */ + var bits; /* bits in bit buffer */ + var _in, _out; /* save starting available input and output */ + var copy; /* number of stored or match bytes to copy */ + var from; /* where to copy match bytes from */ + var from_source; + var here = 0; /* current decoding table entry */ + var here_bits, here_op, here_val; // paked "here" denormalized (JS specific) + //var last; /* parent table entry */ + var last_bits, last_op, last_val; // paked "last" denormalized (JS specific) + var len; /* length to copy for repeats, bits to drop */ + var ret; /* return code */ + var hbuf = new utils.Buf8(4); /* buffer for gzip header crc calculation */ + var opts; + + var n; // temporary var for NEED_BITS + + var order = /* permutation of code lengths */ + [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]; + + + if (!strm || !strm.state || !strm.output || + (!strm.input && strm.avail_in !== 0)) { + return Z_STREAM_ERROR; + } + + state = strm.state; + if (state.mode === TYPE) { state.mode = TYPEDO; } /* skip check */ + + + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + _in = have; + _out = left; + ret = Z_OK; + + inf_leave: // goto emulation + for (;;) { + switch (state.mode) { + case HEAD: + if (state.wrap === 0) { + state.mode = TYPEDO; + break; + } + //=== NEEDBITS(16); + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((state.wrap & 2) && hold === 0x8b1f) { /* gzip header */ + state.check = 0/*crc32(0L, Z_NULL, 0)*/; + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = FLAGS; + break; + } + state.flags = 0; /* expect zlib header */ + if (state.head) { + state.head.done = false; + } + if (!(state.wrap & 1) || /* check if zlib header allowed */ + (((hold & 0xff)/*BITS(8)*/ << 8) + (hold >> 8)) % 31) { + strm.msg = 'incorrect header check'; + state.mode = BAD; + break; + } + if ((hold & 0x0f)/*BITS(4)*/ !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// + len = (hold & 0x0f)/*BITS(4)*/ + 8; + if (state.wbits === 0) { + state.wbits = len; + } + else if (len > state.wbits) { + strm.msg = 'invalid window size'; + state.mode = BAD; + break; + } + state.dmax = 1 << len; + //Tracev((stderr, "inflate: zlib header ok\n")); + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = hold & 0x200 ? DICTID : TYPE; + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + break; + case FLAGS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.flags = hold; + if ((state.flags & 0xff) !== Z_DEFLATED) { + strm.msg = 'unknown compression method'; + state.mode = BAD; + break; + } + if (state.flags & 0xe000) { + strm.msg = 'unknown header flags set'; + state.mode = BAD; + break; + } + if (state.head) { + state.head.text = ((hold >> 8) & 1); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = TIME; + /* falls through */ + case TIME: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.time = hold; + } + if (state.flags & 0x0200) { + //=== CRC4(state.check, hold) + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + hbuf[2] = (hold >>> 16) & 0xff; + hbuf[3] = (hold >>> 24) & 0xff; + state.check = crc32(state.check, hbuf, 4, 0); + //=== + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = OS; + /* falls through */ + case OS: + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (state.head) { + state.head.xflags = (hold & 0xff); + state.head.os = (hold >> 8); + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = EXLEN; + /* falls through */ + case EXLEN: + if (state.flags & 0x0400) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length = hold; + if (state.head) { + state.head.extra_len = hold; + } + if (state.flags & 0x0200) { + //=== CRC2(state.check, hold); + hbuf[0] = hold & 0xff; + hbuf[1] = (hold >>> 8) & 0xff; + state.check = crc32(state.check, hbuf, 2, 0); + //===// + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + else if (state.head) { + state.head.extra = null/*Z_NULL*/; + } + state.mode = EXTRA; + /* falls through */ + case EXTRA: + if (state.flags & 0x0400) { + copy = state.length; + if (copy > have) { copy = have; } + if (copy) { + if (state.head) { + len = state.head.extra_len - state.length; + if (!state.head.extra) { + // Use untyped array for more conveniend processing later + state.head.extra = new Array(state.head.extra_len); + } + utils.arraySet( + state.head.extra, + input, + next, + // extra field is limited to 65536 bytes + // - no need for additional size check + copy, + /*len + copy > state.head.extra_max - len ? state.head.extra_max : copy,*/ + len + ); + //zmemcpy(state.head.extra + len, next, + // len + copy > state.head.extra_max ? + // state.head.extra_max - len : copy); + } + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + state.length -= copy; + } + if (state.length) { break inf_leave; } + } + state.length = 0; + state.mode = NAME; + /* falls through */ + case NAME: + if (state.flags & 0x0800) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + // TODO: 2 or 1 bytes? + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.name_max*/)) { + state.head.name += String.fromCharCode(len); + } + } while (len && copy < have); + + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.name = null; + } + state.length = 0; + state.mode = COMMENT; + /* falls through */ + case COMMENT: + if (state.flags & 0x1000) { + if (have === 0) { break inf_leave; } + copy = 0; + do { + len = input[next + copy++]; + /* use constant limit because in js we should not preallocate memory */ + if (state.head && len && + (state.length < 65536 /*state.head.comm_max*/)) { + state.head.comment += String.fromCharCode(len); + } + } while (len && copy < have); + if (state.flags & 0x0200) { + state.check = crc32(state.check, input, copy, next); + } + have -= copy; + next += copy; + if (len) { break inf_leave; } + } + else if (state.head) { + state.head.comment = null; + } + state.mode = HCRC; + /* falls through */ + case HCRC: + if (state.flags & 0x0200) { + //=== NEEDBITS(16); */ + while (bits < 16) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.check & 0xffff)) { + strm.msg = 'header crc mismatch'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + } + if (state.head) { + state.head.hcrc = ((state.flags >> 9) & 1); + state.head.done = true; + } + strm.adler = state.check = 0 /*crc32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + break; + case DICTID: + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + strm.adler = state.check = ZSWAP32(hold); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = DICT; + /* falls through */ + case DICT: + if (state.havedict === 0) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + return Z_NEED_DICT; + } + strm.adler = state.check = 1/*adler32(0L, Z_NULL, 0)*/; + state.mode = TYPE; + /* falls through */ + case TYPE: + if (flush === Z_BLOCK || flush === Z_TREES) { break inf_leave; } + /* falls through */ + case TYPEDO: + if (state.last) { + //--- BYTEBITS() ---// + hold >>>= bits & 7; + bits -= bits & 7; + //---// + state.mode = CHECK; + break; + } + //=== NEEDBITS(3); */ + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.last = (hold & 0x01)/*BITS(1)*/; + //--- DROPBITS(1) ---// + hold >>>= 1; + bits -= 1; + //---// + + switch ((hold & 0x03)/*BITS(2)*/) { + case 0: /* stored block */ + //Tracev((stderr, "inflate: stored block%s\n", + // state.last ? " (last)" : "")); + state.mode = STORED; + break; + case 1: /* fixed block */ + fixedtables(state); + //Tracev((stderr, "inflate: fixed codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = LEN_; /* decode codes */ + if (flush === Z_TREES) { + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break inf_leave; + } + break; + case 2: /* dynamic block */ + //Tracev((stderr, "inflate: dynamic codes block%s\n", + // state.last ? " (last)" : "")); + state.mode = TABLE; + break; + case 3: + strm.msg = 'invalid block type'; + state.mode = BAD; + } + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + break; + case STORED: + //--- BYTEBITS() ---// /* go to byte boundary */ + hold >>>= bits & 7; + bits -= bits & 7; + //---// + //=== NEEDBITS(32); */ + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if ((hold & 0xffff) !== ((hold >>> 16) ^ 0xffff)) { + strm.msg = 'invalid stored block lengths'; + state.mode = BAD; + break; + } + state.length = hold & 0xffff; + //Tracev((stderr, "inflate: stored length %u\n", + // state.length)); + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + state.mode = COPY_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case COPY_: + state.mode = COPY; + /* falls through */ + case COPY: + copy = state.length; + if (copy) { + if (copy > have) { copy = have; } + if (copy > left) { copy = left; } + if (copy === 0) { break inf_leave; } + //--- zmemcpy(put, next, copy); --- + utils.arraySet(output, input, next, copy, put); + //---// + have -= copy; + next += copy; + left -= copy; + put += copy; + state.length -= copy; + break; + } + //Tracev((stderr, "inflate: stored end\n")); + state.mode = TYPE; + break; + case TABLE: + //=== NEEDBITS(14); */ + while (bits < 14) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.nlen = (hold & 0x1f)/*BITS(5)*/ + 257; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ndist = (hold & 0x1f)/*BITS(5)*/ + 1; + //--- DROPBITS(5) ---// + hold >>>= 5; + bits -= 5; + //---// + state.ncode = (hold & 0x0f)/*BITS(4)*/ + 4; + //--- DROPBITS(4) ---// + hold >>>= 4; + bits -= 4; + //---// +//#ifndef PKZIP_BUG_WORKAROUND + if (state.nlen > 286 || state.ndist > 30) { + strm.msg = 'too many length or distance symbols'; + state.mode = BAD; + break; + } +//#endif + //Tracev((stderr, "inflate: table sizes ok\n")); + state.have = 0; + state.mode = LENLENS; + /* falls through */ + case LENLENS: + while (state.have < state.ncode) { + //=== NEEDBITS(3); + while (bits < 3) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.lens[order[state.have++]] = (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + while (state.have < 19) { + state.lens[order[state.have++]] = 0; + } + // We have separate tables & no pointers. 2 commented lines below not needed. + //state.next = state.codes; + //state.lencode = state.next; + // Switch to use dynamic table + state.lencode = state.lendyn; + state.lenbits = 7; + + opts = {bits: state.lenbits}; + ret = inflate_table(CODES, state.lens, 0, 19, state.lencode, 0, state.work, opts); + state.lenbits = opts.bits; + + if (ret) { + strm.msg = 'invalid code lengths set'; + state.mode = BAD; + break; + } + //Tracev((stderr, "inflate: code lengths ok\n")); + state.have = 0; + state.mode = CODELENS; + /* falls through */ + case CODELENS: + while (state.have < state.nlen + state.ndist) { + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) - 1)];/*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_val < 16) { + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.lens[state.have++] = here_val; + } + else { + if (here_val === 16) { + //=== NEEDBITS(here.bits + 2); + n = here_bits + 2; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + if (state.have === 0) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + len = state.lens[state.have - 1]; + copy = 3 + (hold & 0x03);//BITS(2); + //--- DROPBITS(2) ---// + hold >>>= 2; + bits -= 2; + //---// + } + else if (here_val === 17) { + //=== NEEDBITS(here.bits + 3); + n = here_bits + 3; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 3 + (hold & 0x07);//BITS(3); + //--- DROPBITS(3) ---// + hold >>>= 3; + bits -= 3; + //---// + } + else { + //=== NEEDBITS(here.bits + 7); + n = here_bits + 7; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + len = 0; + copy = 11 + (hold & 0x7f);//BITS(7); + //--- DROPBITS(7) ---// + hold >>>= 7; + bits -= 7; + //---// + } + if (state.have + copy > state.nlen + state.ndist) { + strm.msg = 'invalid bit length repeat'; + state.mode = BAD; + break; + } + while (copy--) { + state.lens[state.have++] = len; + } + } + } + + /* handle error breaks in while */ + if (state.mode === BAD) { break; } + + /* check for end-of-block code (better have one) */ + if (state.lens[256] === 0) { + strm.msg = 'invalid code -- missing end-of-block'; + state.mode = BAD; + break; + } + + /* build code tables -- note: do not change the lenbits or distbits + values here (9 and 6) without reading the comments in inftrees.h + concerning the ENOUGH constants, which depend on those values */ + state.lenbits = 9; + + opts = {bits: state.lenbits}; + ret = inflate_table(LENS, state.lens, 0, state.nlen, state.lencode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.lenbits = opts.bits; + // state.lencode = state.next; + + if (ret) { + strm.msg = 'invalid literal/lengths set'; + state.mode = BAD; + break; + } + + state.distbits = 6; + //state.distcode.copy(state.codes); + // Switch to use dynamic table + state.distcode = state.distdyn; + opts = {bits: state.distbits}; + ret = inflate_table(DISTS, state.lens, state.nlen, state.ndist, state.distcode, 0, state.work, opts); + // We have separate tables & no pointers. 2 commented lines below not needed. + // state.next_index = opts.table_index; + state.distbits = opts.bits; + // state.distcode = state.next; + + if (ret) { + strm.msg = 'invalid distances set'; + state.mode = BAD; + break; + } + //Tracev((stderr, 'inflate: codes ok\n')); + state.mode = LEN_; + if (flush === Z_TREES) { break inf_leave; } + /* falls through */ + case LEN_: + state.mode = LEN; + /* falls through */ + case LEN: + if (have >= 6 && left >= 258) { + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + inflate_fast(strm, _out); + //--- LOAD() --- + put = strm.next_out; + output = strm.output; + left = strm.avail_out; + next = strm.next_in; + input = strm.input; + have = strm.avail_in; + hold = state.hold; + bits = state.bits; + //--- + + if (state.mode === TYPE) { + state.back = -1; + } + break; + } + state.back = 0; + for (;;) { + here = state.lencode[hold & ((1 << state.lenbits) -1)]; /*BITS(state.lenbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if (here_bits <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if (here_op && (here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.lencode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + state.length = here_val; + if (here_op === 0) { + //Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ? + // "inflate: literal '%c'\n" : + // "inflate: literal 0x%02x\n", here.val)); + state.mode = LIT; + break; + } + if (here_op & 32) { + //Tracevv((stderr, "inflate: end of block\n")); + state.back = -1; + state.mode = TYPE; + break; + } + if (here_op & 64) { + strm.msg = 'invalid literal/length code'; + state.mode = BAD; + break; + } + state.extra = here_op & 15; + state.mode = LENEXT; + /* falls through */ + case LENEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.length += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } + //Tracevv((stderr, "inflate: length %u\n", state.length)); + state.was = state.length; + state.mode = DIST; + /* falls through */ + case DIST: + for (;;) { + here = state.distcode[hold & ((1 << state.distbits) -1)];/*BITS(state.distbits)*/ + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + if ((here_op & 0xf0) === 0) { + last_bits = here_bits; + last_op = here_op; + last_val = here_val; + for (;;) { + here = state.distcode[last_val + + ((hold & ((1 << (last_bits + last_op)) -1))/*BITS(last.bits + last.op)*/ >> last_bits)]; + here_bits = here >>> 24; + here_op = (here >>> 16) & 0xff; + here_val = here & 0xffff; + + if ((last_bits + here_bits) <= bits) { break; } + //--- PULLBYTE() ---// + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + //---// + } + //--- DROPBITS(last.bits) ---// + hold >>>= last_bits; + bits -= last_bits; + //---// + state.back += last_bits; + } + //--- DROPBITS(here.bits) ---// + hold >>>= here_bits; + bits -= here_bits; + //---// + state.back += here_bits; + if (here_op & 64) { + strm.msg = 'invalid distance code'; + state.mode = BAD; + break; + } + state.offset = here_val; + state.extra = (here_op) & 15; + state.mode = DISTEXT; + /* falls through */ + case DISTEXT: + if (state.extra) { + //=== NEEDBITS(state.extra); + n = state.extra; + while (bits < n) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + state.offset += hold & ((1 << state.extra) -1)/*BITS(state.extra)*/; + //--- DROPBITS(state.extra) ---// + hold >>>= state.extra; + bits -= state.extra; + //---// + state.back += state.extra; + } +//#ifdef INFLATE_STRICT + if (state.offset > state.dmax) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +//#endif + //Tracevv((stderr, "inflate: distance %u\n", state.offset)); + state.mode = MATCH; + /* falls through */ + case MATCH: + if (left === 0) { break inf_leave; } + copy = _out - left; + if (state.offset > copy) { /* copy from window */ + copy = state.offset - copy; + if (copy > state.whave) { + if (state.sane) { + strm.msg = 'invalid distance too far back'; + state.mode = BAD; + break; + } +// (!) This block is disabled in zlib defailts, +// don't enable it for binary compatibility +//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR +// Trace((stderr, "inflate.c too far\n")); +// copy -= state.whave; +// if (copy > state.length) { copy = state.length; } +// if (copy > left) { copy = left; } +// left -= copy; +// state.length -= copy; +// do { +// output[put++] = 0; +// } while (--copy); +// if (state.length === 0) { state.mode = LEN; } +// break; +//#endif + } + if (copy > state.wnext) { + copy -= state.wnext; + from = state.wsize - copy; + } + else { + from = state.wnext - copy; + } + if (copy > state.length) { copy = state.length; } + from_source = state.window; + } + else { /* copy from output */ + from_source = output; + from = put - state.offset; + copy = state.length; + } + if (copy > left) { copy = left; } + left -= copy; + state.length -= copy; + do { + output[put++] = from_source[from++]; + } while (--copy); + if (state.length === 0) { state.mode = LEN; } + break; + case LIT: + if (left === 0) { break inf_leave; } + output[put++] = state.length; + left--; + state.mode = LEN; + break; + case CHECK: + if (state.wrap) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + // Use '|' insdead of '+' to make sure that result is signed + hold |= input[next++] << bits; + bits += 8; + } + //===// + _out -= left; + strm.total_out += _out; + state.total += _out; + if (_out) { + strm.adler = state.check = + /*UPDATE(state.check, put - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, put - _out) : adler32(state.check, output, _out, put - _out)); + + } + _out = left; + // NB: crc32 stored as signed 32-bit int, ZSWAP32 returns signed too + if ((state.flags ? hold : ZSWAP32(hold)) !== state.check) { + strm.msg = 'incorrect data check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: check matches trailer\n")); + } + state.mode = LENGTH; + /* falls through */ + case LENGTH: + if (state.wrap && state.flags) { + //=== NEEDBITS(32); + while (bits < 32) { + if (have === 0) { break inf_leave; } + have--; + hold += input[next++] << bits; + bits += 8; + } + //===// + if (hold !== (state.total & 0xffffffff)) { + strm.msg = 'incorrect length check'; + state.mode = BAD; + break; + } + //=== INITBITS(); + hold = 0; + bits = 0; + //===// + //Tracev((stderr, "inflate: length matches trailer\n")); + } + state.mode = DONE; + /* falls through */ + case DONE: + ret = Z_STREAM_END; + break inf_leave; + case BAD: + ret = Z_DATA_ERROR; + break inf_leave; + case MEM: + return Z_MEM_ERROR; + case SYNC: + /* falls through */ + default: + return Z_STREAM_ERROR; + } + } + + // inf_leave <- here is real place for "goto inf_leave", emulated via "break inf_leave" + + /* + Return from inflate(), updating the total counts and the check value. + If there was no progress during the inflate() call, return a buffer + error. Call updatewindow() to create and/or update the window state. + Note: a memory error from inflate() is non-recoverable. + */ + + //--- RESTORE() --- + strm.next_out = put; + strm.avail_out = left; + strm.next_in = next; + strm.avail_in = have; + state.hold = hold; + state.bits = bits; + //--- + + if (state.wsize || (_out !== strm.avail_out && state.mode < BAD && + (state.mode < CHECK || flush !== Z_FINISH))) { + if (updatewindow(strm, strm.output, strm.next_out, _out - strm.avail_out)) { + state.mode = MEM; + return Z_MEM_ERROR; + } + } + _in -= strm.avail_in; + _out -= strm.avail_out; + strm.total_in += _in; + strm.total_out += _out; + state.total += _out; + if (state.wrap && _out) { + strm.adler = state.check = /*UPDATE(state.check, strm.next_out - _out, _out);*/ + (state.flags ? crc32(state.check, output, _out, strm.next_out - _out) : adler32(state.check, output, _out, strm.next_out - _out)); + } + strm.data_type = state.bits + (state.last ? 64 : 0) + + (state.mode === TYPE ? 128 : 0) + + (state.mode === LEN_ || state.mode === COPY_ ? 256 : 0); + if (((_in === 0 && _out === 0) || flush === Z_FINISH) && ret === Z_OK) { + ret = Z_BUF_ERROR; + } + return ret; +} + +function inflateEnd(strm) { + + if (!strm || !strm.state /*|| strm->zfree == (free_func)0*/) { + return Z_STREAM_ERROR; + } + + var state = strm.state; + if (state.window) { + state.window = null; + } + strm.state = null; + return Z_OK; +} + +function inflateGetHeader(strm, head) { + var state; + + /* check state */ + if (!strm || !strm.state) { return Z_STREAM_ERROR; } + state = strm.state; + if ((state.wrap & 2) === 0) { return Z_STREAM_ERROR; } + + /* save header structure */ + state.head = head; + head.done = false; + return Z_OK; +} + + +exports.inflateReset = inflateReset; +exports.inflateReset2 = inflateReset2; +exports.inflateResetKeep = inflateResetKeep; +exports.inflateInit = inflateInit; +exports.inflateInit2 = inflateInit2; +exports.inflate = inflate; +exports.inflateEnd = inflateEnd; +exports.inflateGetHeader = inflateGetHeader; +exports.inflateInfo = 'pako inflate (from Nodeca project)'; + +/* Not implemented +exports.inflateCopy = inflateCopy; +exports.inflateGetDictionary = inflateGetDictionary; +exports.inflateMark = inflateMark; +exports.inflatePrime = inflatePrime; +exports.inflateSetDictionary = inflateSetDictionary; +exports.inflateSync = inflateSync; +exports.inflateSyncPoint = inflateSyncPoint; +exports.inflateUndermine = inflateUndermine; +*/ + +},{"../utils/common":1,"./adler32":2,"./crc32":3,"./inffast":4,"./inftrees":6}],6:[function(require,module,exports){ +'use strict'; + + +var utils = require('../utils/common'); + +var MAXBITS = 15; +var ENOUGH_LENS = 852; +var ENOUGH_DISTS = 592; +//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS); + +var CODES = 0; +var LENS = 1; +var DISTS = 2; + +var lbase = [ /* Length codes 257..285 base */ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0 +]; + +var lext = [ /* Length codes 257..285 extra */ + 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, + 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78 +]; + +var dbase = [ /* Distance codes 0..29 base */ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577, 0, 0 +]; + +var dext = [ /* Distance codes 0..29 extra */ + 16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, + 28, 28, 29, 29, 64, 64 +]; + +module.exports = function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts) +{ + var bits = opts.bits; + //here = opts.here; /* table entry for duplication */ + + var len = 0; /* a code's length in bits */ + var sym = 0; /* index of code symbols */ + var min = 0, max = 0; /* minimum and maximum code lengths */ + var root = 0; /* number of index bits for root table */ + var curr = 0; /* number of index bits for current table */ + var drop = 0; /* code bits to drop for sub-table */ + var left = 0; /* number of prefix codes available */ + var used = 0; /* code entries in table used */ + var huff = 0; /* Huffman code */ + var incr; /* for incrementing code, index */ + var fill; /* index for replicating entries */ + var low; /* low bits for current root entry */ + var mask; /* mask for low root bits */ + var next; /* next available space in table */ + var base = null; /* base value table to use */ + var base_index = 0; +// var shoextra; /* extra bits table to use */ + var end; /* use base and extra for symbol > end */ + var count = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* number of codes of each length */ + var offs = new utils.Buf16(MAXBITS+1); //[MAXBITS+1]; /* offsets in table for each length */ + var extra = null; + var extra_index = 0; + + var here_bits, here_op, here_val; + + /* + Process a set of code lengths to create a canonical Huffman code. The + code lengths are lens[0..codes-1]. Each length corresponds to the + symbols 0..codes-1. The Huffman code is generated by first sorting the + symbols by length from short to long, and retaining the symbol order + for codes with equal lengths. Then the code starts with all zero bits + for the first code of the shortest length, and the codes are integer + increments for the same length, and zeros are appended as the length + increases. For the deflate format, these bits are stored backwards + from their more natural integer increment ordering, and so when the + decoding tables are built in the large loop below, the integer codes + are incremented backwards. + + This routine assumes, but does not check, that all of the entries in + lens[] are in the range 0..MAXBITS. The caller must assure this. + 1..MAXBITS is interpreted as that code length. zero means that that + symbol does not occur in this code. + + The codes are sorted by computing a count of codes for each length, + creating from that a table of starting indices for each length in the + sorted table, and then entering the symbols in order in the sorted + table. The sorted table is work[], with that space being provided by + the caller. + + The length counts are used for other purposes as well, i.e. finding + the minimum and maximum length codes, determining if there are any + codes at all, checking for a valid set of lengths, and looking ahead + at length counts to determine sub-table sizes when building the + decoding tables. + */ + + /* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */ + for (len = 0; len <= MAXBITS; len++) { + count[len] = 0; + } + for (sym = 0; sym < codes; sym++) { + count[lens[lens_index + sym]]++; + } + + /* bound code lengths, force root to be within code lengths */ + root = bits; + for (max = MAXBITS; max >= 1; max--) { + if (count[max] !== 0) { break; } + } + if (root > max) { + root = max; + } + if (max === 0) { /* no symbols to code at all */ + //table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */ + //table.bits[opts.table_index] = 1; //here.bits = (var char)1; + //table.val[opts.table_index++] = 0; //here.val = (var short)0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + + //table.op[opts.table_index] = 64; + //table.bits[opts.table_index] = 1; + //table.val[opts.table_index++] = 0; + table[table_index++] = (1 << 24) | (64 << 16) | 0; + + opts.bits = 1; + return 0; /* no symbols, but wait for decoding to report error */ + } + for (min = 1; min < max; min++) { + if (count[min] !== 0) { break; } + } + if (root < min) { + root = min; + } + + /* check for an over-subscribed or incomplete set of lengths */ + left = 1; + for (len = 1; len <= MAXBITS; len++) { + left <<= 1; + left -= count[len]; + if (left < 0) { + return -1; + } /* over-subscribed */ + } + if (left > 0 && (type === CODES || max !== 1)) { + return -1; /* incomplete set */ + } + + /* generate offsets into symbol table for each length for sorting */ + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) { + offs[len + 1] = offs[len] + count[len]; + } + + /* sort symbols by length, by symbol order within each length */ + for (sym = 0; sym < codes; sym++) { + if (lens[lens_index + sym] !== 0) { + work[offs[lens[lens_index + sym]]++] = sym; + } + } + + /* + Create and fill in decoding tables. In this loop, the table being + filled is at next and has curr index bits. The code being used is huff + with length len. That code is converted to an index by dropping drop + bits off of the bottom. For codes where len is less than drop + curr, + those top drop + curr - len bits are incremented through all values to + fill the table with replicated entries. + + root is the number of index bits for the root table. When len exceeds + root, sub-tables are created pointed to by the root entry with an index + of the low root bits of huff. This is saved in low to check for when a + new sub-table should be started. drop is zero when the root table is + being filled, and drop is root when sub-tables are being filled. + + When a new sub-table is needed, it is necessary to look ahead in the + code lengths to determine what size sub-table is needed. The length + counts are used for this, and so count[] is decremented as codes are + entered in the tables. + + used keeps track of how many table entries have been allocated from the + provided *table space. It is checked for LENS and DIST tables against + the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in + the initial root table size constants. See the comments in inftrees.h + for more information. + + sym increments through all symbols, and the loop terminates when + all codes of length max, i.e. all codes, have been processed. This + routine permits incomplete codes, so another loop after this one fills + in the rest of the decoding tables with invalid code markers. + */ + + /* set up for code type */ + // poor man optimization - use if-else instead of switch, + // to avoid deopts in old v8 + if (type === CODES) { + base = extra = work; /* dummy value--not used */ + end = 19; + + } else if (type === LENS) { + base = lbase; + base_index -= 257; + extra = lext; + extra_index -= 257; + end = 256; + + } else { /* DISTS */ + base = dbase; + extra = dext; + end = -1; + } + + /* initialize opts for loop */ + huff = 0; /* starting code */ + sym = 0; /* starting code symbol */ + len = min; /* starting code length */ + next = table_index; /* current table to fill in */ + curr = root; /* current table index bits */ + drop = 0; /* current bits to drop from code for index */ + low = -1; /* trigger new sub-table when len > root */ + used = 1 << root; /* use root table entries */ + mask = used - 1; /* mask for comparing low */ + + /* check available table space */ + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + var i=0; + /* process all codes and make table entries */ + for (;;) { + i++; + /* create table entry */ + here_bits = len - drop; + if (work[sym] < end) { + here_op = 0; + here_val = work[sym]; + } + else if (work[sym] > end) { + here_op = extra[extra_index + work[sym]]; + here_val = base[base_index + work[sym]]; + } + else { + here_op = 32 + 64; /* end of block */ + here_val = 0; + } + + /* replicate for those indices with low len bits equal to huff */ + incr = 1 << (len - drop); + fill = 1 << curr; + min = fill; /* save offset to next table */ + do { + fill -= incr; + table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0; + } while (fill !== 0); + + /* backwards increment the len-bit code huff */ + incr = 1 << (len - 1); + while (huff & incr) { + incr >>= 1; + } + if (incr !== 0) { + huff &= incr - 1; + huff += incr; + } else { + huff = 0; + } + + /* go to next symbol, update count, len */ + sym++; + if (--count[len] === 0) { + if (len === max) { break; } + len = lens[lens_index + work[sym]]; + } + + /* create new sub-table if needed */ + if (len > root && (huff & mask) !== low) { + /* if first time, transition to sub-tables */ + if (drop === 0) { + drop = root; + } + + /* increment past last table */ + next += min; /* here min is 1 << curr */ + + /* determine length of next table */ + curr = len - drop; + left = 1 << curr; + while (curr + drop < max) { + left -= count[curr + drop]; + if (left <= 0) { break; } + curr++; + left <<= 1; + } + + /* check for enough space */ + used += 1 << curr; + if ((type === LENS && used > ENOUGH_LENS) || + (type === DISTS && used > ENOUGH_DISTS)) { + return 1; + } + + /* point entry in root table to sub-table */ + low = huff & mask; + /*table.op[low] = curr; + table.bits[low] = root; + table.val[low] = next - opts.table_index;*/ + table[low] = (root << 24) | (curr << 16) | (next - table_index) |0; + } + } + + /* fill in remaining table entry if code is incomplete (guaranteed to have + at most one remaining entry, since if the code is incomplete, the + maximum code length that was allowed to get this far is one bit) */ + if (huff !== 0) { + //table.op[next + huff] = 64; /* invalid code marker */ + //table.bits[next + huff] = len - drop; + //table.val[next + huff] = 0; + table[next + huff] = ((len - drop) << 24) | (64 << 16) |0; + } + + /* set return parameters */ + //opts.table_index += used; + opts.bits = root; + return 0; +}; + +},{"../utils/common":1}],7:[function(require,module,exports){ +'use strict'; + + +function ZStream() { + /* next input byte */ + this.input = null; // JS specific, because we have no pointers + this.next_in = 0; + /* number of bytes available at input */ + this.avail_in = 0; + /* total number of input bytes read so far */ + this.total_in = 0; + /* next output byte should be put there */ + this.output = null; // JS specific, because we have no pointers + this.next_out = 0; + /* remaining free space at output */ + this.avail_out = 0; + /* total number of bytes output so far */ + this.total_out = 0; + /* last error message, NULL if no error */ + this.msg = ''/*Z_NULL*/; + /* not visible by applications */ + this.state = null; + /* best guess about the data type: binary or text */ + this.data_type = 2/*Z_UNKNOWN*/; + /* adler32 value of the uncompressed data */ + this.adler = 0; +} + +module.exports = ZStream; + +},{}],"/partial_inflator.js":[function(require,module,exports){ +var zlib = require('./lib/zlib/inflate.js'); +var ZStream = require('./lib/zlib/zstream.js'); + +var Inflate = function () { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + zlib.inflateInit(this.strm, this.windowBits); +}; + +Inflate.prototype = { + inflate: function (data, flush) { + this.strm.input = new Uint8Array(data); + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + this.strm.avail_out = this.chunkSize; + + zlib.inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + }, + + reset: function () { + zlib.inflateReset(this.strm); + } +}; + +module.exports = {Inflate: Inflate}; + +},{"./lib/zlib/inflate.js":5,"./lib/zlib/zstream.js":7}]},{},[])("/partial_inflator.js") +}); diff --git a/include/rfb.js b/include/rfb.js index 4e8f4087..3b54701e 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -380,8 +380,9 @@ var RFB; } for (i = 0; i < 4; i++) { - this._FBU.zlibs[i] = new TINF(); - this._FBU.zlibs[i].init(); + //this._FBU.zlibs[i] = new TINF(); + //this._FBU.zlibs[i].init(); + this._FBU.zlibs[i] = new inflator.Inflate(); } }, @@ -1181,7 +1182,14 @@ var RFB; this._timing.last_fbu = (new Date()).getTime(); - ret = this._encHandlers[this._FBU.encoding](); + var handler = this._encHandlers[this._FBU.encoding]; + try { + //ret = this._encHandlers[this._FBU.encoding](); + ret = handler(); + } catch (ex) { + console.log("missed " + this._FBU.encoding + ": " + handler); + ret = this._encHandlers[this._FBU.encoding](); + } now = (new Date()).getTime(); this._timing.cur_fbu += (now - this._timing.last_fbu); @@ -1639,12 +1647,14 @@ var RFB; } } - var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); - if (uncompressed.status !== 0) { + //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + var uncompressed = this._FBU.zlibs[streamId].inflate(data, true); + /*if (uncompressed.status !== 0) { Util.Error("Invalid data in zlib stream"); - } + }*/ - return uncompressed.data; + //return uncompressed.data; + return uncompressed; }.bind(this); var indexedToRGB = function (data, numColors, palette, width, height) { diff --git a/include/ui.js b/include/ui.js index 2e9d731a..929e37f8 100644 --- a/include/ui.js +++ b/include/ui.js @@ -21,7 +21,7 @@ var UI; window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js", "keysym.js"]); + "rfb.js", "keysym.js", "inflator.js"]); UI = { diff --git a/karma.conf.js b/karma.conf.js index d8b8e905..870b8551 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -119,9 +119,9 @@ module.exports = function(config) { 'include/input.js', 'include/websock.js', 'include/rfb.js', - 'include/jsunzip.js', 'include/des.js', 'include/display.js', + 'include/inflator.js', 'tests/test.*.js' ], diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 006b5fa9..5a6ac4a8 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1,4 +1,4 @@ -// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, jsunzip, des, display +// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display // requires test modules: fake.websocket, assertions /* jshint expr: true */ var assert = chai.assert; diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index 8b0207b2..f36f1e65 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -61,7 +61,7 @@ // Load supporting scripts Util.load_scripts(["base64.js", "websock.js", "des.js", "keysym.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js", "playback.js", fname]); + "rfb.js", "playback.js", "inflator.js", fname]); } else { message("Must specify data=FOO in query string."); diff --git a/utils/inflator.partial.js b/utils/inflator.partial.js new file mode 100644 index 00000000..2522d781 --- /dev/null +++ b/utils/inflator.partial.js @@ -0,0 +1,32 @@ +var zlib = require('./lib/zlib/inflate.js'); +var ZStream = require('./lib/zlib/zstream.js'); + +var Inflate = function () { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; + + zlib.inflateInit(this.strm, this.windowBits); +}; + +Inflate.prototype = { + inflate: function (data, flush) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + this.strm.avail_out = this.chunkSize; + + zlib.inflate(this.strm, flush); + + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + }, + + reset: function () { + zlib.inflateReset(this.strm); + } +}; + +module.exports = {Inflate: Inflate}; diff --git a/vnc_auto.html b/vnc_auto.html index 4361a5d5..86cfde75 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -77,7 +77,7 @@ // Load supporting scripts Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js", "keysym.js"]); + "inflator.js", "rfb.js", "keysym.js"]); var rfb; var resizeTimeout; From 6c883653a23aec7220b7b8cd042c90554fcb28cc Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 19 May 2015 16:49:39 -0400 Subject: [PATCH 093/527] Remove unecessarily nested loop This commit removes a nested loop in indexedToRGB, converting it into a plain loop without multiplication. --- include/rfb.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 3b54701e..47409c67 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -91,6 +91,7 @@ var RFB; this._fb_width = 0; this._fb_height = 0; this._fb_name = ""; + this._dest_buff = null; this._rre_chunk_sz = 100; @@ -901,6 +902,7 @@ var RFB; /* Screen size */ this._fb_width = this._sock.rQshift16(); this._fb_height = this._sock.rQshift16(); + this._dest_buff = new Uint8Array(this._fb_width * this._fb_height * 4); /* PIXEL_FORMAT */ var bpp = this._sock.rQshift8(); @@ -1660,7 +1662,7 @@ var RFB; var indexedToRGB = function (data, numColors, palette, width, height) { // Convert indexed (palette based) image data to RGB // TODO: reduce number of calculations inside loop - var dest = []; + var dest = this._dest_buff; var x, y, dp, sp; if (numColors === 2) { var w = Math.floor((width + 7) / 8); @@ -1687,14 +1689,12 @@ var RFB; } } } else { - for (y = 0; y < height; y++) { - for (x = 0; x < width; x++) { - dp = (y * width + x) * 3; - sp = data[y * width + x] * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - } + var total = width * height * 3; + for (var i = 0, j = 0; i < total; i += 3, j++) { + sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; } } @@ -1891,6 +1891,7 @@ var RFB; handle_FB_resize: function () { this._fb_width = this._FBU.width; this._fb_height = this._FBU.height; + this._dest_buff = new Uint8Array(this._fb_width * this._fb_height * 4); this._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); From 38781d931ec18304f51ed3469faff8387e3cbc55 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 20 May 2015 17:10:59 -0400 Subject: [PATCH 094/527] Use Typed Arrays for the Websock receive queue **This commit removes Base64 (and Flash) support** This commit converts websock.js to used Typed Arrays for the receive queue (and tweaks rfb.js to ensure that it continues to function, since only Firefox implements `%TypedArray%.prototype.slice`). Base64 support was removed to simplify code paths, and pave the way for using Typed Arrays for the send queue as well. This provides two advantages: first, we allocate a buffer ahead of time, meaning the browser doesn't have to do any work dynamically increasing the receive queue size. Secondly, we are now able to pass around Typed Array Views (e.g. `Uint8Array`), which are lightweight, and don't involve copying. The downside is that we initially allocate more memory -- we currently start out with 4 MiB, and then automatically double when it looks like the amount unused is getting to small. The commit also explicitly adds a check to the compacting logic that avoids calling the copy functions if `_rQlen === _rQi`. --- .gitmodules | 3 - LICENSE.txt | 3 - README.md | 7 +- docs/notes | 18 -- include/playback.js | 3 +- include/rfb.js | 40 +-- include/web-socket-js-project | 1 - include/web-socket-js/README.txt | 109 ------- include/web-socket-js/WebSocketMain.swf | Bin 177139 -> 0 bytes include/web-socket-js/swfobject.js | 4 - include/web-socket-js/web_socket.js | 391 ------------------------ include/websock.js | 170 ++++++----- tests/assertions.js | 67 +++- tests/test.rfb.js | 27 +- tests/test.websock.js | 114 +++---- 15 files changed, 242 insertions(+), 715 deletions(-) delete mode 160000 include/web-socket-js-project delete mode 100644 include/web-socket-js/README.txt delete mode 100644 include/web-socket-js/WebSocketMain.swf delete mode 100644 include/web-socket-js/swfobject.js delete mode 100644 include/web-socket-js/web_socket.js diff --git a/.gitmodules b/.gitmodules index 45574aeb..e69de29b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "include/web-socket-js-project"] - path = include/web-socket-js-project - url = https://github.com/gimite/web-socket-js.git diff --git a/LICENSE.txt b/LICENSE.txt index 638cd5a5..82e8a6a1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -51,9 +51,6 @@ licenses (all MPL 2.0 compatible): include/jsunzip.js : zlib/libpng license - include/web-socket-js/ : New BSD license (3-clause). Source code at - http://github.com/gimite/web-socket-js - include/chrome-app/tcp-stream.js : Apache 2.0 license diff --git a/README.md b/README.md index 0f9a8f6f..59a72b10 100644 --- a/README.md +++ b/README.md @@ -69,11 +69,7 @@ See more screenshots h * HTML5 Canvas (with createImageData): Chrome, Firefox 3.6+, iOS Safari, Opera 11+, Internet Explorer 9+, etc. -* HTML5 WebSockets: For browsers that do not have builtin - WebSockets support, the project includes - web-socket-js, - a WebSockets emulator using Adobe Flash. iOS 4.2+ has built-in - WebSocket support. +* HTML5 WebSockets and Typed Arrays * Fast Javascript Engine: this is not strictly a requirement, but without a fast Javascript engine, noVNC might be painfully slow. @@ -130,7 +126,6 @@ use a WebSockets to TCP socket proxy. There is a python proxy included * tight encoding : Michael Tinglof (Mercuri.ca) * Included libraries: - * web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js) * as3crypto : Henri Torgemane (code.google.com/p/as3crypto) * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) diff --git a/docs/notes b/docs/notes index 443dbd41..6ff5ec19 100644 --- a/docs/notes +++ b/docs/notes @@ -1,21 +1,3 @@ -Some implementation notes: - -There is an included flash object (web-socket-js) that is used to -emulate websocket support on browsers without websocket support -(currently only Chrome has WebSocket support). - -Javascript doesn't have a bytearray type, so what you get out of -a WebSocket object is just Javascript strings. Javascript has UTF-16 -unicode strings and anything sent through the WebSocket gets converted -to UTF-8 and vice-versa. So, one additional (and necessary) function -of websockify is base64 encoding/decoding what is sent to/from the -browser. - -Building web-socket-js emulator: - -cd include/web-socket-js/flash-src -mxmlc -static-link-runtime-shared-libraries WebSocketMain.as - Rebuilding inflator.js - Download pako from npm diff --git a/include/playback.js b/include/playback.js index 7d3d749f..203576f6 100644 --- a/include/playback.js +++ b/include/playback.js @@ -30,6 +30,7 @@ enable_test_mode = function () { this._rfb_port = port; this._rfb_password = (password !== undefined) ? password : ""; this._rfb_path = (path !== undefined) ? path : ""; + this._sock.init('binary', 'ws'); this._updateState('ProtocolVersion', "Starting VNC handshake"); }; }; @@ -43,7 +44,7 @@ next_iteration = function () { frame_length = VNC_frame_data.length; test_state = 'running'; } - + if (test_state !== 'running') { return; } iteration += 1; diff --git a/include/rfb.js b/include/rfb.js index 47409c67..113d9419 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -129,7 +129,7 @@ var RFB; 'view_only': false, // Disable client mouse/keyboard '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 + 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection 'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'viewportDrag': false, // Move the viewport on mouse drags @@ -218,16 +218,8 @@ var RFB; Util.Info("Using native WebSockets"); this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); } else { - Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); - if (!Util.Flash || Util.Flash.version < 9) { - this._cleanupSocket('fatal'); - throw new Exception("WebSockets or Adobe Flash is required"); - } else if (document.location.href.substr(0, 7) === 'file://') { - this._cleanupSocket('fatal'); - throw new Exception("'file://' URL is incompatible with Adobe Flash"); - } else { - this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode); - } + this._cleanupSocket('fatal'); + throw new Error("WebSocket support is required to use noVNC"); } Util.Debug("<< RFB.constructor"); @@ -363,8 +355,6 @@ var RFB; _init_vars: function () { // reset state - this._sock.init(); - this._FBU.rects = 0; this._FBU.subrects = 0; // RRE and HEXTILE this._FBU.lines = 0; // RAW @@ -760,7 +750,8 @@ var RFB; if (this._sock.rQwait("auth challenge", 16)) { return false; } - var challenge = this._sock.rQshiftBytes(16); + // TODO(directxman12): make genDES not require an Array + var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); var response = RFB.genDES(this._rfb_password, challenge); this._sock.send(response); this._updateState("SecurityResult"); @@ -1559,11 +1550,21 @@ var RFB; rQi += this._FBU.bytes - 1; } else { if (this._FBU.subencoding & 0x02) { // Background - this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp); + if (this._fb_Bpp == 1) { + this._FBU.background = rQ[rQi]; + } else { + // fb_Bpp is 4 + this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } rQi += this._fb_Bpp; } if (this._FBU.subencoding & 0x04) { // Foreground - this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); + if (this._fb_Bpp == 1) { + this._FBU.foreground = rQ[rQi]; + } else { + // this._fb_Bpp is 4 + this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } rQi += this._fb_Bpp; } @@ -1575,7 +1576,12 @@ 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); + if (this._fb_Bpp === 1) { + color = rQ[rQi]; + } else { + // _fb_Bpp is 4 + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + } rQi += this._fb_Bpp; } else { color = this._FBU.foreground; diff --git a/include/web-socket-js-project b/include/web-socket-js-project deleted file mode 160000 index c0855c6c..00000000 --- a/include/web-socket-js-project +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c0855c6caec589c33acc22b6ee5e562287e65f3d diff --git a/include/web-socket-js/README.txt b/include/web-socket-js/README.txt deleted file mode 100644 index 2e32ea7f..00000000 --- a/include/web-socket-js/README.txt +++ /dev/null @@ -1,109 +0,0 @@ -* How to try - -Assuming you have Web server (e.g. Apache) running at http://example.com/ . - -- Download web_socket.rb from: - http://github.com/gimite/web-socket-ruby/tree/master -- Run sample Web Socket server (echo server) in example.com with: (#1) - $ ruby web-socket-ruby/samples/echo_server.rb example.com 10081 -- If your server already provides socket policy file at port 843, modify the file to allow access to port 10081. Otherwise you can skip this step. See below for details. -- Publish the web-socket-js directory with your Web server (e.g. put it in ~/public_html). -- Change ws://localhost:10081 to ws://example.com:10081 in sample.html. -- Open sample.html in your browser. -- After "onopen" is shown, input something, click [Send] and confirm echo back. - -#1: First argument of echo_server.rb means that it accepts Web Socket connection from HTML pages in example.com. - - -* Troubleshooting - -If it doesn't work, try these: - -1. Try Chrome and Firefox 3.x. -- It doesn't work on Chrome: --- It's likely an issue of your code or the server. Debug your code as usual e.g. using console.log. -- It works on Chrome but it doesn't work on Firefox: --- It's likely an issue of web-socket-js specific configuration (e.g. 3 and 4 below). -- It works on both Chrome and Firefox, but it doesn't work on your browser: --- Check "Supported environment" section below. Your browser may not be supported by web-socket-js. - -2. Add this line before your code: - WEB_SOCKET_DEBUG = true; -and use Developer Tools (Chrome/Safari) or Firebug (Firefox) to see if console.log outputs any errors. - -3. Make sure you do NOT open your HTML page as local file e.g. file:///.../sample.html. web-socket-js doesn't work on local file. Open it via Web server e.g. http:///.../sample.html. - -4. If you are NOT using web-socket-ruby as your WebSocket server, you need to place Flash socket policy file on your server. See "Flash socket policy file" section below for details. - -5. Check if sample.html bundled with web-socket-js works. - -6. Make sure the port used for WebSocket (10081 in example above) is not blocked by your server/client's firewall. - -7. Install debugger version of Flash Player available here to see Flash errors: -http://www.adobe.com/support/flashplayer/downloads.html - - -* Supported environments - -It should work on: -- Google Chrome 4 or later (just uses native implementation) -- Firefox 3.x, Internet Explorer 8 + Flash Player 9 or later - -It may or may not work on other browsers such as Safari, Opera or IE 6. Patch for these browsers are appreciated, but I will not work on fixing issues specific to these browsers by myself. - - -* Flash socket policy file - -This implementation uses Flash's socket, which means that your server must provide Flash socket policy file to declare the server accepts connections from Flash. - -If you use web-socket-ruby available at -http://github.com/gimite/web-socket-ruby/tree/master -, you don't need anything special, because web-socket-ruby handles Flash socket policy file request. But if you already provide socket policy file at port 843, you need to modify the file to allow access to Web Socket port, because it precedes what web-socket-ruby provides. - -If you use other Web Socket server implementation, you need to provide socket policy file yourself. See -http://www.lightsphere.com/dev/articles/flash_socket_policy.html -for details and sample script to run socket policy file server. node.js implementation is available here: -http://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/transports/flashsocket.js - -Actually, it's still better to provide socket policy file at port 843 even if you use web-socket-ruby. Flash always try to connect to port 843 first, so providing the file at port 843 makes startup faster. - - -* Cookie considerations - -Cookie is sent if Web Socket host is the same as the origin of JavaScript. Otherwise it is not sent, because I don't know way to send right Cookie (which is Cookie of the host of Web Socket, I heard). - -Note that it's technically possible that client sends arbitrary string as Cookie and any other headers (by modifying this library for example) once you place Flash socket policy file in your server. So don't trust Cookie and other headers if you allow connection from untrusted origin. - - -* Proxy considerations - -The WebSocket spec (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol) specifies instructions for User Agents to support proxied connections by implementing the HTTP CONNECT method. - -The AS3 Socket class doesn't implement this mechanism, which renders it useless for the scenarios where the user trying to open a socket is behind a proxy. - -The class RFC2817Socket (by Christian Cantrell) effectively lets us implement this, as long as the proxy settings are known and provided by the interface that instantiates the WebSocket. As such, if you want to support proxied conncetions, you'll have to supply this information to the WebSocket constructor when Flash is being used. One way to go about it would be to ask the user for proxy settings information if the initial connection fails. - - -* How to host HTML file and SWF file in different domains - -By default, HTML file and SWF file must be in the same domain. You can follow steps below to allow hosting them in different domain. - -WARNING: If you use the method below, HTML files in ANY domains can send arbitrary TCP data to your WebSocket server, regardless of configuration in Flash socket policy file. Arbitrary TCP data means that they can even fake request headers including Origin and Cookie. - -- Unzip WebSocketMainInsecure.zip to extract WebSocketMainInsecure.swf. -- Put WebSocketMainInsecure.swf on your server, instead of WebSocketMain.swf. -- In JavaScript, set WEB_SOCKET_SWF_LOCATION to URL of your WebSocketMainInsecure.swf. - - -* How to build WebSocketMain.swf - -Install Flex 4 SDK: -http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4 - -$ cd flash-src -$ ./build.sh - - -* License - -New BSD License. diff --git a/include/web-socket-js/WebSocketMain.swf b/include/web-socket-js/WebSocketMain.swf deleted file mode 100644 index f286c81aacc954953086a97dfff7e4e18e78b6bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177139 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�{?S0&?Rq)c&c-(}SFHOPy4tz`b5gun?zakh{ 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:-!a@ddza^dI1O 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 D+>;X~ diff --git a/include/web-socket-js/swfobject.js b/include/web-socket-js/swfobject.js deleted file mode 100644 index 8eafe9dd..00000000 --- a/include/web-socket-js/swfobject.js +++ /dev/null @@ -1,4 +0,0 @@ -/* SWFObject v2.2 - is released under the MIT License -*/ -var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab -// License: New BSD License -// Reference: http://dev.w3.org/html5/websockets/ -// Reference: http://tools.ietf.org/html/rfc6455 - -(function() { - - if (window.WEB_SOCKET_FORCE_FLASH) { - // Keeps going. - } else if (window.WebSocket) { - return; - } else if (window.MozWebSocket) { - // Firefox. - window.WebSocket = MozWebSocket; - return; - } - - var logger; - if (window.WEB_SOCKET_LOGGER) { - logger = WEB_SOCKET_LOGGER; - } else if (window.console && window.console.log && window.console.error) { - // In some environment, console is defined but console.log or console.error is missing. - logger = window.console; - } else { - logger = {log: function(){ }, error: function(){ }}; - } - - // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash. - if (swfobject.getFlashPlayerVersion().major < 10) { - logger.error("Flash Player >= 10.0.0 is required."); - return; - } - if (location.protocol == "file:") { - logger.error( - "WARNING: web-socket-js doesn't work in file:///... URL " + - "unless you set Flash Security Settings properly. " + - "Open the page via Web server i.e. http://..."); - } - - /** - * Our own implementation of WebSocket class using Flash. - * @param {string} url - * @param {array or string} protocols - * @param {string} proxyHost - * @param {int} proxyPort - * @param {string} headers - */ - window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { - var self = this; - self.__id = WebSocket.__nextId++; - WebSocket.__instances[self.__id] = self; - self.readyState = WebSocket.CONNECTING; - self.bufferedAmount = 0; - self.__events = {}; - if (!protocols) { - protocols = []; - } else if (typeof protocols == "string") { - protocols = [protocols]; - } - // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. - // Otherwise, when onopen fires immediately, onopen is called before it is set. - self.__createTask = setTimeout(function() { - WebSocket.__addTask(function() { - self.__createTask = null; - WebSocket.__flash.create( - self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); - }); - }, 0); - }; - - /** - * Send data to the web socket. - * @param {string} data The data to send to the socket. - * @return {boolean} True for success, false for failure. - */ - WebSocket.prototype.send = function(data) { - if (this.readyState == WebSocket.CONNECTING) { - throw "INVALID_STATE_ERR: Web Socket connection has not been established"; - } - // We use encodeURIComponent() here, because FABridge doesn't work if - // the argument includes some characters. We don't use escape() here - // because of this: - // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions - // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't - // preserve all Unicode characters either e.g. "\uffff" in Firefox. - // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require - // additional testing. - var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); - if (result < 0) { // success - return true; - } else { - this.bufferedAmount += result; - return false; - } - }; - - /** - * Close this web socket gracefully. - */ - WebSocket.prototype.close = function() { - if (this.__createTask) { - clearTimeout(this.__createTask); - this.__createTask = null; - this.readyState = WebSocket.CLOSED; - return; - } - if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { - return; - } - this.readyState = WebSocket.CLOSING; - WebSocket.__flash.close(this.__id); - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.addEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) { - this.__events[type] = []; - } - this.__events[type].push(listener); - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) return; - var events = this.__events[type]; - for (var i = events.length - 1; i >= 0; --i) { - if (events[i] === listener) { - events.splice(i, 1); - break; - } - } - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {Event} event - * @return void - */ - WebSocket.prototype.dispatchEvent = function(event) { - var events = this.__events[event.type] || []; - for (var i = 0; i < events.length; ++i) { - events[i](event); - } - var handler = this["on" + event.type]; - if (handler) handler.apply(this, [event]); - }; - - /** - * Handles an event from Flash. - * @param {Object} flashEvent - */ - WebSocket.prototype.__handleEvent = function(flashEvent) { - - if ("readyState" in flashEvent) { - this.readyState = flashEvent.readyState; - } - if ("protocol" in flashEvent) { - this.protocol = flashEvent.protocol; - } - - var jsEvent; - if (flashEvent.type == "open" || flashEvent.type == "error") { - jsEvent = this.__createSimpleEvent(flashEvent.type); - } else if (flashEvent.type == "close") { - jsEvent = this.__createSimpleEvent("close"); - jsEvent.wasClean = flashEvent.wasClean ? true : false; - jsEvent.code = flashEvent.code; - jsEvent.reason = flashEvent.reason; - } else if (flashEvent.type == "message") { - var data = decodeURIComponent(flashEvent.message); - jsEvent = this.__createMessageEvent("message", data); - } else { - throw "unknown event type: " + flashEvent.type; - } - - this.dispatchEvent(jsEvent); - - }; - - WebSocket.prototype.__createSimpleEvent = function(type) { - if (document.createEvent && window.Event) { - var event = document.createEvent("Event"); - event.initEvent(type, false, false); - return event; - } else { - return {type: type, bubbles: false, cancelable: false}; - } - }; - - WebSocket.prototype.__createMessageEvent = function(type, data) { - if (document.createEvent && window.MessageEvent && !window.opera) { - var event = document.createEvent("MessageEvent"); - event.initMessageEvent("message", false, false, data, null, null, window, null); - return event; - } else { - // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. - return {type: type, data: data, bubbles: false, cancelable: false}; - } - }; - - /** - * Define the WebSocket readyState enumeration. - */ - WebSocket.CONNECTING = 0; - WebSocket.OPEN = 1; - WebSocket.CLOSING = 2; - WebSocket.CLOSED = 3; - - // Field to check implementation of WebSocket. - WebSocket.__isFlashImplementation = true; - WebSocket.__initialized = false; - WebSocket.__flash = null; - WebSocket.__instances = {}; - WebSocket.__tasks = []; - WebSocket.__nextId = 0; - - /** - * Load a new flash security policy file. - * @param {string} url - */ - WebSocket.loadFlashPolicyFile = function(url){ - WebSocket.__addTask(function() { - WebSocket.__flash.loadManualPolicyFile(url); - }); - }; - - /** - * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. - */ - WebSocket.__initialize = function() { - - if (WebSocket.__initialized) return; - WebSocket.__initialized = true; - - if (WebSocket.__swfLocation) { - // For backword compatibility. - window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; - } - if (!window.WEB_SOCKET_SWF_LOCATION) { - logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); - return; - } - if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR && - !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) && - WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) { - var swfHost = RegExp.$1; - if (location.host != swfHost) { - logger.error( - "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " + - "('" + location.host + "' != '" + swfHost + "'). " + - "See also 'How to host HTML file and SWF file in different domains' section " + - "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " + - "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;"); - } - } - var container = document.createElement("div"); - container.id = "webSocketContainer"; - // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents - // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). - // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash - // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is - // the best we can do as far as we know now. - container.style.position = "absolute"; - if (WebSocket.__isFlashLite()) { - container.style.left = "0px"; - container.style.top = "0px"; - } else { - container.style.left = "-100px"; - container.style.top = "-100px"; - } - var holder = document.createElement("div"); - holder.id = "webSocketFlash"; - container.appendChild(holder); - document.body.appendChild(container); - // See this article for hasPriority: - // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html - swfobject.embedSWF( - WEB_SOCKET_SWF_LOCATION, - "webSocketFlash", - "1" /* width */, - "1" /* height */, - "10.0.0" /* SWF version */, - null, - null, - {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, - null, - function(e) { - if (!e.success) { - logger.error("[WebSocket] swfobject.embedSWF failed"); - } - } - ); - - }; - - /** - * Called by Flash to notify JS that it's fully loaded and ready - * for communication. - */ - WebSocket.__onFlashInitialized = function() { - // We need to set a timeout here to avoid round-trip calls - // to flash during the initialization process. - setTimeout(function() { - WebSocket.__flash = document.getElementById("webSocketFlash"); - WebSocket.__flash.setCallerUrl(location.href); - WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); - for (var i = 0; i < WebSocket.__tasks.length; ++i) { - WebSocket.__tasks[i](); - } - WebSocket.__tasks = []; - }, 0); - }; - - /** - * Called by Flash to notify WebSockets events are fired. - */ - WebSocket.__onFlashEvent = function() { - setTimeout(function() { - try { - // Gets events using receiveEvents() instead of getting it from event object - // of Flash event. This is to make sure to keep message order. - // It seems sometimes Flash events don't arrive in the same order as they are sent. - var events = WebSocket.__flash.receiveEvents(); - for (var i = 0; i < events.length; ++i) { - WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); - } - } catch (e) { - logger.error(e); - } - }, 0); - return true; - }; - - // Called by Flash. - WebSocket.__log = function(message) { - logger.log(decodeURIComponent(message)); - }; - - // Called by Flash. - WebSocket.__error = function(message) { - logger.error(decodeURIComponent(message)); - }; - - WebSocket.__addTask = function(task) { - if (WebSocket.__flash) { - task(); - } else { - WebSocket.__tasks.push(task); - } - }; - - /** - * Test if the browser is running flash lite. - * @return {boolean} True if flash lite is running, false otherwise. - */ - WebSocket.__isFlashLite = function() { - if (!window.navigator || !window.navigator.mimeTypes) { - return false; - } - var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; - if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { - return false; - } - return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; - }; - - if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { - // NOTE: - // This fires immediately if web_socket.js is dynamically loaded after - // the document is loaded. - swfobject.addDomLoadEvent(function() { - WebSocket.__initialize(); - }); - } - -})(); diff --git a/include/websock.js b/include/websock.js index cc82e5a2..71ae36ca 100644 --- a/include/websock.js +++ b/include/websock.js @@ -15,7 +15,7 @@ */ /*jslint browser: true, bitwise: true */ -/*global Util, Base64 */ +/*global Util*/ // Load Flash WebSocket emulator if needed @@ -34,29 +34,22 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { /* no builtin WebSocket so load web_socket.js */ Websock_native = false; - (function () { - window.WEB_SOCKET_SWF_LOCATION = Util.get_include_uri() + - "web-socket-js/WebSocketMain.swf"; - if (Util.Engine.trident) { - Util.Debug("Forcing uncached load of WebSocketMain.swf"); - window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random(); - } - Util.load_scripts(["web-socket-js/swfobject.js", - "web-socket-js/web_socket.js"]); - })(); } - function Websock() { "use strict"; this._websocket = null; // WebSocket object - this._rQ = []; // Receive queue - this._rQi = 0; // Receive queue index - this._rQmax = 10000; // Max receive queue size before compacting - this._sQ = []; // Send queue - this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64' + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + this._rQmax = this._rQbufferSize / 8; + this._sQ = []; // Send queue + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue + + this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' this.maxBufferedAmount = 200; this._eventHandlers = { @@ -69,6 +62,22 @@ function Websock() { (function () { "use strict"; + + var typedArrayToString = (function () { + // This is only for PhantomJS, which doesn't like apply-ing + // with Typed Arrays + try { + var arr = new Uint8Array([1, 2, 3]); + String.fromCharCode.apply(null, arr); + return function (a) { return String.fromCharCode.apply(null, a); }; + } catch (ex) { + return function (a) { + return String.fromCharCode.apply( + null, Array.prototype.slice.call(a)); + }; + } + })(); + Websock.prototype = { // Getters and Setters get_sQ: function () { @@ -89,7 +98,7 @@ function Websock() { // Receive Queue rQlen: function () { - return this._rQ.length - this._rQi; + return this._rQlen - this._rQi; }, rQpeek8: function () { @@ -108,15 +117,7 @@ function Websock() { this._rQi += num; }, - rQunshift8: function (num) { - if (this._rQi === 0) { - this._rQ.unshift(num); - } else { - this._rQi--; - this._rQ[this._rQi] = num; - } - }, - + // TODO(directxman12): test performance with these vs a DataView rQshift16: function () { return (this._rQ[this._rQi++] << 8) + this._rQ[this._rQi++]; @@ -131,22 +132,29 @@ function Websock() { rQshiftStr: function (len) { if (typeof(len) === 'undefined') { len = this.rQlen(); } - var arr = this._rQ.slice(this._rQi, this._rQi + len); + var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); this._rQi += len; - return String.fromCharCode.apply(null, arr); + return typedArrayToString(arr); }, rQshiftBytes: function (len) { if (typeof(len) === 'undefined') { len = this.rQlen(); } this._rQi += len; - return this._rQ.slice(this._rQi - len, this._rQi); + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + }, + + rQshiftTo: function (target, len) { + if (len === undefined) { len = this.rQlen(); } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; }, rQslice: function (start, end) { if (end) { - return this._rQ.slice(this._rQi + start, this._rQi + end); + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); } else { - return this._rQ.slice(this._rQi + start); + return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); } }, @@ -154,7 +162,7 @@ function Websock() { // to be available in the receive queue. Return true if we need to // wait (and possibly print a debug message), otherwise false. rQwait: function (msg, num, goback) { - var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call + var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call if (rQlen < num) { if (goback) { if (this._rQi < goback) { @@ -208,8 +216,12 @@ function Websock() { this._eventHandlers[evt] = handler; }, + _allocate_buffers: function () { + this._rQ = new Uint8Array(this._rQbufferSize); + }, + init: function (protocols, ws_schema) { - this._rQ = []; + this._allocate_buffers(); this._rQi = 0; this._sQ = []; this._websocket = null; @@ -238,35 +250,21 @@ function Websock() { // Default protocols if not specified if (typeof(protocols) === "undefined") { - if (wsbt) { - protocols = ['binary', 'base64']; - } else { - protocols = 'base64'; - } + protocols = 'binary'; + } + + if (Array.isArray(protocols) && protocols.indexOf('binary') > -1) { + protocols = 'binary'; } if (!wsbt) { - if (protocols === 'binary') { - throw new Error('WebSocket binary sub-protocol requested but not supported'); - } + throw new Error("noVNC no longer supports base64 WebSockets. " + + "Please use a browser which supports binary WebSockets."); + } - if (typeof(protocols) === 'object') { - var new_protocols = []; - - for (var i = 0; i < protocols.length; i++) { - if (protocols[i] === 'binary') { - Util.Error('Skipping unsupported WebSocket binary sub-protocol'); - } else { - new_protocols.push(protocols[i]); - } - } - - if (new_protocols.length > 0) { - protocols = new_protocols; - } else { - throw new Error("Only WebSocket binary sub-protocol was requested and is not supported."); - } - } + if (protocols != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); } return protocols; @@ -289,9 +287,16 @@ function Websock() { this._mode = this._websocket.protocol; Util.Info("Server choose sub-protocol: " + this._websocket.protocol); } else { - this._mode = 'base64'; + this._mode = 'binary'; Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); } + + if (this._mode != 'binary') { + throw new Error("noVNC no longer supports base64 WebSockets. Please " + + "use the binary subprotocol instead."); + + } + this._eventHandlers.open(); Util.Debug("<< WebSock.onopen"); }).bind(this); @@ -321,26 +326,15 @@ function Websock() { // private methods _encode_message: function () { - if (this._mode === 'binary') { - // Put in a binary arraybuffer - return (new Uint8Array(this._sQ)).buffer; - } else { - // base64 encode - return Base64.encode(this._sQ); - } + // Put in a binary arraybuffer + return (new Uint8Array(this._sQ)).buffer; }, _decode_message: function (data) { - if (this._mode === 'binary') { - // push arraybuffer values onto the end - var u8 = new Uint8Array(data); - for (var i = 0; i < u8.length; i++) { - this._rQ.push(u8[i]); - } - } else { - // base64 decode and concat to end - this._rQ = this._rQ.concat(Base64.decode(data, 0)); - } + // push arraybuffer values onto the end + var u8 = new Uint8Array(data); + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; }, _recv_message: function (e) { @@ -349,8 +343,26 @@ function Websock() { if (this.rQlen() > 0) { this._eventHandlers.message(); // Compact the receive queue - if (this._rQ.length > this._rQmax) { - this._rQ = this._rQ.slice(this._rQi); + if (this._rQlen == this._rQi) { + this._rQlen = 0; + this._rQi = 0; + } else if (this._rQlen > this._rQmax) { + if (this._rQlen - this._rQi > 0.5 * this._rQbufferSize) { + var old_rQbuffer = this._rQ.buffer; + this._rQbufferSize *= 2; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (this._rQ.copyWithin) { + // Firefox only, ATM + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; this._rQi = 0; } } else { diff --git a/tests/assertions.js b/tests/assertions.js index 92b11d1f..6c01c4c9 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -5,7 +5,17 @@ chai.use(function (_chai, utils) { var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that var data = new Uint8Array(data_cl); - this.assert(utils.eql(data, target_data), + var same = true; + for (var i = 0; i < obj.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + if (!same) { + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", "expected #{this} not to have displayed the image #{act}", target_data, @@ -15,10 +25,63 @@ chai.use(function (_chai, utils) { _chai.Assertion.addMethod('sent', function (target_data) { var obj = this._obj; var data = obj._websocket._get_sent_data(); - this.assert(utils.eql(data, target_data), + var same = true; + for (var i = 0; i < obj.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + if (!same) { + console.log("expected data: %o, actual data: %o", target_data, data); + } + this.assert(same, "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", "expected #{this} not to have sent the data #{act}", target_data, data); }); + + _chai.Assertion.addProperty('array', function () { + utils.flag(this, 'array', true); + }); + + _chai.Assertion.overwriteMethod('equal', function (_super) { + return function assertArrayEqual(target) { + if (utils.flag(this, 'array')) { + var obj = this._obj; + + var i; + var same = true; + + if (utils.flag(this, 'deep')) { + for (i = 0; i < obj.length; i++) { + if (!utils.eql(obj[i], target[i])) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements deeply equal to #{exp}", + "expected #{this} not to have elements deeply equal to #{exp}", + Array.prototype.slice.call(target)); + } else { + for (i = 0; i < obj.length; i++) { + if (obj[i] != target[i]) { + same = false; + break; + } + } + + this.assert(same, + "expected #{this} to have elements equal to #{exp}", + "expected #{this} not to have elements equal to #{exp}", + Array.prototype.slice.call(target)); + } + } else { + _super.apply(this, arguments); + } + }; + }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 5a6ac4a8..f22314da 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1,4 +1,4 @@ -// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display +// requires local modules: util, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display // requires test modules: fake.websocket, assertions /* jshint expr: true */ var assert = chai.assert; @@ -18,6 +18,25 @@ describe('Remote Frame Buffer Protocol Client', function() { before(FakeWebSocket.replace); after(FakeWebSocket.restore); + before(function () { + this.clock = sinon.useFakeTimers(); + // Use a single set of buffers instead of reallocating to + // speed up tests + var sock = new Websock(); + var rQ = new Uint8Array(sock._rQbufferSize); + + Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; + Websock.prototype._allocate_buffers = function () { + this._rQ = rQ; + }; + + }); + + after(function () { + Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; + this.clock.restore(); + }); + describe('Public API Basic Behavior', function () { var client; beforeEach(function () { @@ -1826,7 +1845,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client.connect('host', 8675); client._rfb_state = 'normal'; client._normal_msg = sinon.spy(); - client._sock._websocket._receive_data(Base64.encode([])); + client._sock._websocket._receive_data(new Uint8Array([])); expect(client._normal_msg).to.not.have.been.called; }); @@ -1834,7 +1853,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client.connect('host', 8675); client._rfb_state = 'normal'; client._normal_msg = sinon.spy(); - client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); expect(client._normal_msg).to.have.been.calledOnce; }); @@ -1842,7 +1861,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client.connect('host', 8675); client._rfb_state = 'ProtocolVersion'; client._init_msg = sinon.spy(); - client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); expect(client._init_msg).to.have.been.calledOnce; }); diff --git a/tests/test.websock.js b/tests/test.websock.js index 7d242d3e..a81f75da 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -1,5 +1,5 @@ -// requires local modules: websock, base64, util -// requires test modules: fake.websocket +// requires local modules: websock, util +// requires test modules: fake.websocket, assertions /* jshint expr: true */ var assert = chai.assert; var expect = chai.expect; @@ -9,13 +9,14 @@ describe('Websock', function() { describe('Queue methods', function () { var sock; - var RQ_TEMPLATE = [0, 1, 2, 3, 4, 5, 6, 7]; + var RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); beforeEach(function () { sock = new Websock(); - for (var i = RQ_TEMPLATE.length - 1; i >= 0; i--) { - sock.rQunshift8(RQ_TEMPLATE[i]); - } + // skip init + sock._allocate_buffers(); + sock._rQ.set(RQ_TEMPLATE); + sock._rQlen = RQ_TEMPLATE.length; }); describe('rQlen', function () { it('should return the length of the receive queue', function () { @@ -49,14 +50,6 @@ describe('Websock', function() { }); }); - describe('rQunshift8', function () { - it('should place a byte at the front of the queue', function () { - sock.rQunshift8(255); - expect(sock.rQpeek8()).to.equal(255); - expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length + 1); - }); - }); - describe('rQshift16', function () { it('should pop two bytes from the receive queue and return a single number', function () { var bef_len = sock.rQlen(); @@ -84,7 +77,7 @@ describe('Websock', function() { var bef_rQi = sock.get_rQi(); var shifted = sock.rQshiftStr(3); expect(shifted).to.be.a('string'); - expect(shifted).to.equal(String.fromCharCode.apply(null, RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3))); + expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); expect(sock.rQlen()).to.equal(bef_len - 3); }); @@ -99,8 +92,8 @@ describe('Websock', function() { var bef_len = sock.rQlen(); var bef_rQi = sock.get_rQi(); var shifted = sock.rQshiftBytes(3); - expect(shifted).to.be.an.instanceof(Array); - expect(shifted).to.deep.equal(RQ_TEMPLATE.slice(bef_rQi, bef_rQi + 3)); + expect(shifted).to.be.an.instanceof(Uint8Array); + expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)); expect(sock.rQlen()).to.equal(bef_len - 3); }); @@ -123,19 +116,19 @@ describe('Websock', function() { it('should return an array containing the given slice of the receive queue', function () { var sl = sock.rQslice(0, 2); - expect(sl).to.be.an.instanceof(Array); - expect(sl).to.deep.equal(RQ_TEMPLATE.slice(0, 2)); + expect(sl).to.be.an.instanceof(Uint8Array); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); }); it('should use the rest of the receive queue if no end is given', function () { var sl = sock.rQslice(1); expect(sl).to.have.length(RQ_TEMPLATE.length - 1); - expect(sl).to.deep.equal(RQ_TEMPLATE.slice(1)); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1)); }); it('should take the current rQi in to account', function () { sock.set_rQi(1); - expect(sock.rQslice(0, 2)).to.deep.equal(RQ_TEMPLATE.slice(1, 3)); + expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); }); }); @@ -257,6 +250,8 @@ describe('Websock', function() { WebSocket.CONNECTING = old_WS.CONNECTING; WebSocket.CLOSING = old_WS.CLOSING; WebSocket.CLOSED = old_WS.CLOSED; + + WebSocket.prototype.binaryType = 'arraybuffer'; }); describe('opening', function () { @@ -265,22 +260,14 @@ describe('Websock', function() { }); it('should open the actual websocket', function () { - sock.open('ws://localhost:8675', 'base64'); - expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'base64'); + sock.open('ws://localhost:8675', 'binary'); + expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary'); }); - it('should fail if we try to use binary but do not support it', function () { - expect(function () { sock.open('ws:///', 'binary'); }).to.throw(Error); + it('should fail if we specify a protocol besides binary', function () { + expect(function () { sock.open('ws:///', 'base64'); }).to.throw(Error); }); - it('should fail if we specified an array with only binary and we do not support it', function () { - expect(function () { sock.open('ws:///', ['binary']); }).to.throw(Error); - }); - - it('should skip binary if we have multiple options for encoding and do not support binary', function () { - sock.open('ws:///', ['binary', 'base64']); - expect(WebSocket).to.have.been.calledWith('ws:///', ['base64']); - }); // it('should initialize the event handlers')? }); @@ -340,16 +327,15 @@ describe('Websock', function() { expect(sock._recv_message).to.have.been.calledOnce; }); - it('should copy the mode over upon opening', function () { - sock._websocket.protocol = 'cheese'; - sock._websocket.onopen(); - expect(sock._mode).to.equal('cheese'); + it('should fail if a protocol besides binary is requested', function () { + sock._websocket.protocol = 'base64'; + expect(sock._websocket.onopen).to.throw(Error); }); - it('should assume base64 if no protocol was available on opening', function () { + it('should assume binary if no protocol was available on opening', function () { sock._websocket.protocol = null; sock._websocket.onopen(); - expect(sock._mode).to.equal('base64'); + expect(sock._mode).to.equal('binary'); }); it('should call the open event handler on opening', function () { @@ -377,13 +363,7 @@ describe('Websock', function() { var sock; beforeEach(function () { sock = new Websock(); - }); - - it('should support decoding base64 string data to add it to the receive queue', function () { - var msg = { data: Base64.encode([1, 2, 3]) }; - sock._mode = 'base64'; - sock._recv_message(msg); - expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); + sock._allocate_buffers(); }); it('should support adding binary Uint8Array data to the receive queue', function () { @@ -395,16 +375,16 @@ describe('Websock', function() { it('should call the message event handler if present', function () { sock._eventHandlers.message = sinon.spy(); - var msg = { data: Base64.encode([1, 2, 3]) }; - sock._mode = 'base64'; + var msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; sock._recv_message(msg); expect(sock._eventHandlers.message).to.have.been.calledOnce; }); it('should not call the message event handler if there is nothing in the receive queue', function () { sock._eventHandlers.message = sinon.spy(); - var msg = { data: Base64.encode([]) }; - sock._mode = 'base64'; + var msg = { data: new Uint8Array([]).buffer }; + sock._mode = 'binary'; sock._recv_message(msg); expect(sock._eventHandlers.message).not.to.have.been.called; }); @@ -412,21 +392,22 @@ describe('Websock', function() { it('should compact the receive queue', function () { // NB(sross): while this is an internal implementation detail, it's important to // test, otherwise the receive queue could become very large very quickly - sock._rQ = [0, 1, 2, 3, 4, 5]; + sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); + sock._rQlen = 6; sock.set_rQi(6); sock._rQmax = 3; - var msg = { data: Base64.encode([1, 2, 3]) }; - sock._mode = 'base64'; + var msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; sock._recv_message(msg); - expect(sock._rQ.length).to.equal(3); + expect(sock._rQlen).to.equal(3); expect(sock.get_rQi()).to.equal(0); }); it('should call the error event handler on an exception', function () { sock._eventHandlers.error = sinon.spy(); sock._eventHandlers.message = sinon.stub().throws(); - var msg = { data: Base64.encode([1, 2, 3]) }; - sock._mode = 'base64'; + var msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; sock._recv_message(msg); expect(sock._eventHandlers.error).to.have.been.calledOnce; }); @@ -455,26 +436,5 @@ describe('Websock', function() { expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]); }); }); - - describe('as Base64 data', function () { - var sock; - beforeEach(function () { - sock = new Websock(); - sock.open('ws://', 'base64'); - sock._websocket._open(); - }); - - it('should convert the send queue into a Base64-encoded string', function () { - sock._sQ = [1, 2, 3]; - expect(sock._encode_message()).to.equal(Base64.encode([1, 2, 3])); - }); - - it('should properly pass the encoded data off to the actual WebSocket', function () { - sock.send([1, 2, 3]); - expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]); - }); - - }); - }); }); From d1800d0960ad9c3d2ff20c990f5886d2edf7f514 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 28 May 2015 15:27:40 -0400 Subject: [PATCH 095/527] Avoid Creating Small Objects Frequently Creating lots of small objects frequently can drastically decrease performance. This commit introduces three fixes which avoid this: - Use a preallocated palette and indexed-to-rgb destination Typed Array (the destination typed array is currently allocated at `4 * width * height`). - Inline `getTightCLength`, which returned a two-item array. - Pass RGBX data directly in a Typed Array to the Display, which avoids an extra loop, and only creates a new Typed Array View, instead of a whole new ArrayBuffer. --- include/display.js | 28 ++++++ include/inflator.js | 2 +- include/rfb.js | 212 ++++++++++++++++++++++++++++++++------------ 3 files changed, 183 insertions(+), 59 deletions(-) diff --git a/include/display.js b/include/display.js index 8994856b..418b431d 100644 --- a/include/display.js +++ b/include/display.js @@ -15,6 +15,14 @@ var Display; (function () { "use strict"; + var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; + try { + new ImageData(new Uint8ClampedArray(1), 1, 1); + SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; + } catch (ex) { + // ignore failure + } + Display = function (defaults) { this._drawCtx = null; this._c_forceCanvas = false; @@ -435,6 +443,10 @@ var Display; } }, + blitRgbxImage: function (x, y, width, height, arr, offset) { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + }, + blitStringImage: function (str, x, y) { var img = new Image(); img.onload = function () { @@ -612,6 +624,19 @@ var Display; this._drawCtx.putImageData(img, x - vx, y - vy); }, + _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) { + // NB(directxman12): arr must be an Type Array view + // NB(directxman12): this only works + var img; + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, 0, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, 0, width * height * 4)); + } + 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; @@ -643,6 +668,9 @@ var Display; case 'blitRgb': this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); break; + case 'blitRgbx': + this.blitRgbxImage(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/inflator.js b/include/inflator.js index 68f85cbd..a9c75a62 100644 --- a/include/inflator.js +++ b/include/inflator.js @@ -2386,7 +2386,7 @@ var Inflate = function () { Inflate.prototype = { inflate: function (data, flush) { - this.strm.input = new Uint8Array(data); + this.strm.input = data; this.strm.avail_in = this.strm.input.length; this.strm.next_in = 0; this.strm.next_out = 0; diff --git a/include/rfb.js b/include/rfb.js index 113d9419..e2dd42f5 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -91,7 +91,9 @@ var RFB; this._fb_width = 0; this._fb_height = 0; this._fb_name = ""; - this._dest_buff = null; + + this._destBuff = null; + this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) this._rre_chunk_sz = 100; @@ -1665,43 +1667,77 @@ var RFB; return uncompressed; }.bind(this); - var indexedToRGB = function (data, numColors, palette, width, height) { + var indexedToRGBX2Color = function (data, palette, width, height) { // Convert indexed (palette based) image data to RGB // TODO: reduce number of calculations inside loop - var dest = this._dest_buff; - var x, y, dp, sp; - if (numColors === 2) { - var w = Math.floor((width + 7) / 8); - var w1 = Math.floor(width / 8); + var dest = this._destBuff; + var w = Math.floor((width + 7) / 8); + var w1 = Math.floor(width / 8); - for (y = 0; y < height; y++) { - var b; - for (x = 0; x < w1; x++) { - for (b = 7; b >= 0; b--) { - dp = (y * width + x * 8 + 7 - b) * 3; - sp = (data[y * w + x] >> b & 1) * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - } - } - - for (b = 7; b >= 8 - width % 8; b--) { - dp = (y * width + x * 8 + 7 - b) * 3; - sp = (data[y * w + x] >> b & 1) * 3; + /*for (var y = 0; y < height; y++) { + var b, x, dp, sp; + var yoffset = y * width; + var ybitoffset = y * w; + var xoffset, targetbyte; + for (x = 0; x < w1; x++) { + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 0; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; dest[dp] = palette[sp]; dest[dp + 1] = palette[sp + 1]; dest[dp + 2] = palette[sp + 2]; } } - } else { - var total = width * height * 3; - for (var i = 0, j = 0; i < total; i += 3, j++) { - sp = data[j] * 3; - dest[i] = palette[sp]; - dest[i + 1] = palette[sp + 1]; - dest[i + 2] = palette[sp + 2]; + + xoffset = yoffset + x * 8; + targetbyte = data[ybitoffset + x]; + for (b = 7; b >= 8 - width % 8; b--) { + dp = (xoffset + 7 - b) * 3; + sp = (targetbyte >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; } + }*/ + + for (var y = 0; y < height; y++) { + var b, x, dp, sp; + for (x = 0; x < w1; x++) { + for (b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + for (b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } + } + + return dest; + }.bind(this); + + var indexedToRGBX = function (data, palette, width, height) { + // Convert indexed (palette based) image data to RGB + var dest = this._destBuff; + var total = width * height * 4; + for (var i = 0, j = 0; i < total; i += 4, j++) { + var sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; } return dest; @@ -1709,7 +1745,8 @@ var RFB; var rQ = this._sock.get_rQ(); var rQi = this._sock.get_rQi(); - var cmode, clength, data; + var cmode, data; + var cl_header, cl_data; var handlePalette = function () { var numColors = rQ[rQi + 2] + 1; @@ -1722,37 +1759,69 @@ var RFB; var raw = false; if (rowSize * this._FBU.height < 12) { raw = true; - clength = [0, rowSize * this._FBU.height]; + cl_header = 0; + cl_data = rowSize * this._FBU.height; + //clength = [0, rowSize * this._FBU.height]; } else { - clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize, - 3 + paletteSize + 3)); + // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) + var cl_offset = rQi + 3 + paletteSize; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength } - this._FBU.bytes += clength[0] + clength[1]; + this._FBU.bytes += cl_header + cl_data; if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } // Shift ctl, filter id, num colors, palette entries, and clength off this._sock.rQskipBytes(3); - var palette = this._sock.rQshiftBytes(paletteSize); - this._sock.rQskipBytes(clength[0]); + //var palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQskipBytes(cl_header); if (raw) { - data = this._sock.rQshiftBytes(clength[1]); + data = this._sock.rQshiftBytes(cl_data); } else { - data = decompress(this._sock.rQshiftBytes(clength[1])); + data = decompress(this._sock.rQshiftBytes(cl_data)); } // Convert indexed (palette based) image data to RGB - var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height); + var rgbx; + if (numColors == 2) { + rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + + /*this._display.renderQ_push({ + 'type': 'blitRgbx', + 'data': rgbx, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + });*/ + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0); + } else { + rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + + /*this._display.renderQ_push({ + 'type': 'blitRgbx', + 'data': rgbx, + 'x': this._FBU.x, + 'y': this._FBU.y, + 'width': this._FBU.width, + 'height': this._FBU.height + });*/ + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0); + } - this._display.renderQ_push({ - 'type': 'blitRgb', - 'data': rgb, - 'x': this._FBU.x, - 'y': this._FBU.y, - 'width': this._FBU.width, - 'height': this._FBU.height - }); return true; }.bind(this); @@ -1762,20 +1831,34 @@ var RFB; var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth; if (uncompressedSize < 12) { raw = true; - clength = [0, uncompressedSize]; + cl_header = 0; + cl_data = uncompressedSize; } else { - clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength } - this._FBU.bytes = 1 + clength[0] + clength[1]; + this._FBU.bytes = 1 + cl_header + cl_data; if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } // Shift ctl, clength off - this._sock.rQshiftBytes(1 + clength[0]); + this._sock.rQshiftBytes(1 + cl_header); if (raw) { - data = this._sock.rQshiftBytes(clength[1]); + data = this._sock.rQshiftBytes(cl_data); } else { - data = decompress(this._sock.rQshiftBytes(clength[1])); + data = decompress(this._sock.rQshiftBytes(cl_data)); } this._display.renderQ_push({ @@ -1846,15 +1929,28 @@ var RFB; break; case "png": case "jpeg": - clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4)); - this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + var cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } + } + // end inline getTightCLength + this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } // We have everything, render it - this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length + this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length var img = new Image(); img.src = "data: image/" + cmode + - RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1])); + RFB.extract_data_uri(this._sock.rQshiftBytes(cl_data)); this._display.renderQ_push({ 'type': 'img', 'img': img, @@ -1897,7 +1993,7 @@ var RFB; handle_FB_resize: function () { this._fb_width = this._FBU.width; this._fb_height = this._FBU.height; - this._dest_buff = new Uint8Array(this._fb_width * this._fb_height * 4); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); this._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); From 9ff86fb718477515ede2d6457f06643935d76bcd Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 28 May 2015 15:28:30 -0400 Subject: [PATCH 096/527] Use Typed Arrays for the send queue This commit converts the send queue to use typed arrays, and converts message creation functions in 'rfb.js' to create messages directly into the socket's send queue. This commit also removes the separate mouse array, which is no longer needed. --- include/rfb.js | 256 +++++++++++++++++++++++----------------- include/websock.js | 20 ++-- tests/assertions.js | 10 +- tests/fake.websocket.js | 9 +- tests/test.rfb.js | 180 ++++++++++++++-------------- tests/test.websock.js | 18 +-- 6 files changed, 277 insertions(+), 216 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index e2dd42f5..b8615af8 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -259,14 +259,14 @@ var RFB; if (this._rfb_state !== 'normal' || this._view_only) { return false; } Util.Info("Sending Ctrl-Alt-Del"); - var arr = []; - arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 1)); - arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1)); - arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1)); - arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0)); - arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0)); - arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0)); - this._sock.send(arr); + RFB.messages.keyEvent(this._sock, XK_Control_L, 1); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 1); + RFB.messages.keyEvent(this._sock, XK_Delete, 0); + RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); + RFB.messages.keyEvent(this._sock, XK_Control_L, 0); + + this._sock.flush(); }, xvpOp: function (ver, op) { @@ -292,21 +292,22 @@ var RFB; // followed by an up key. sendKey: function (code, down) { if (this._rfb_state !== "normal" || this._view_only) { return false; } - var arr = []; if (typeof down !== 'undefined') { Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code); - arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0)); + RFB.messages.keyEvent(this._sock, code, down ? 1 : 0); } else { Util.Info("Sending key code (down + up): " + code); - arr = arr.concat(RFB.messages.keyEvent(code, 1)); - arr = arr.concat(RFB.messages.keyEvent(code, 0)); + RFB.messages.keyEvent(this._sock, code, 1); + RFB.messages.keyEvent(this._sock, code, 0); } - this._sock.send(arr); + + this._sock.flush(); }, clipboardPasteFrom: function (text) { if (this._rfb_state !== 'normal') { return; } - this._sock.send(RFB.messages.clientCutText(text)); + RFB.messages.clientCutText(this._sock, text); + this._sock.flush(); }, setDesktopSize: function (width, height) { @@ -572,16 +573,10 @@ var RFB; } }, - _checkEvents: function () { - if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) { - this._sock.send(this._mouse_arr); - this._mouse_arr = []; - } - }, - _handleKeyPress: function (keysym, down) { if (this._view_only) { return; } // View only, skip keyboard, events - this._sock.send(RFB.messages.keyEvent(keysym, down)); + RFB.messages.keyEvent(this._sock, keysym, down); + this._sock.flush(); }, _handleMouseButton: function (x, y, down, bmask) { @@ -605,10 +600,8 @@ var RFB; if (this._view_only) { return; } // View only, skip mouse events - this._mouse_arr = this._mouse_arr.concat( - RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); - this._sock.send(this._mouse_arr); - this._mouse_arr = []; + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, _handleMouseMove: function (x, y) { @@ -625,10 +618,8 @@ var RFB; if (this._view_only) { return; } // View only, skip mouse events - this._mouse_arr = this._mouse_arr.concat( - RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); - - this._checkEvents(); + if (this._rfb_state !== "normal") { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); }, // Message Handlers @@ -895,7 +886,7 @@ var RFB; /* Screen size */ this._fb_width = this._sock.rQshift16(); this._fb_height = this._sock.rQshift16(); - this._dest_buff = new Uint8Array(this._fb_width * this._fb_height * 4); + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); /* PIXEL_FORMAT */ var bpp = this._sock.rQshift8(); @@ -991,18 +982,13 @@ var RFB; this._fb_depth = 1; } - 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)); + RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); + RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; - this._sock.send(response); - - this._checkEvents(); + this._sock.flush(); if (this._encrypt) { this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); @@ -1104,8 +1090,8 @@ var RFB; case 0: // FramebufferUpdate var ret = this._framebufferUpdate(); if (ret) { - this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(), - this._fb_width, this._fb_height)); + RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + this._sock.flush(); } return ret; @@ -1279,64 +1265,111 @@ var RFB; // Class Methods RFB.messages = { - keyEvent: function (keysym, down) { - var arr = [4]; - arr.push8(down); - arr.push16(0); - arr.push32(keysym); - return arr; + keyEvent: function (sock, keysym, down) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; }, - pointerEvent: function (x, y, mask) { - var arr = [5]; // msg-type - arr.push8(mask); - arr.push16(x); - arr.push16(y); - return arr; + pointerEvent: function (sock, x, y, mask) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; }, // TODO(directxman12): make this unicode compatible? - clientCutText: function (text) { - var arr = [6]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - arr.push32(text.length); + clientCutText: function (sock, text) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + var n = text.length; + + buff[offset + 4] = n >> 24; + buff[offset + 5] = n >> 16; + buff[offset + 6] = n >> 8; + buff[offset + 7] = n; + for (var i = 0; i < n; i++) { - arr.push(text.charCodeAt(i)); + buff[offset + 8 + i] = text.charCodeAt(i); } - return arr; + sock._sQlen += 8 + n; }, - pixelFormat: function (bpp, depth, true_color) { - var arr = [0]; // msg-type - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding + pixelFormat: function (sock, bpp, depth, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; - 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 + buff[offset] = 0; // msg-type - 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 + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding - arr.push8(0); // padding - arr.push8(0); // padding - arr.push8(0); // padding - return arr; + buff[offset + 4] = bpp * 8; // bits-per-pixel + buff[offset + 5] = depth * 8; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = 255; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = 255; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = 255; // blue-max + + buff[offset + 14] = 16; // red-shift + buff[offset + 15] = 8; // green-shift + buff[offset + 16] = 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; }, - clientEncodings: function (encodings, local_cursor, true_color) { - var i, encList = []; + clientEncodings: function (sock, encodings, local_cursor, true_color) { + var buff = sock._sQ; + var offset = sock._sQlen; + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + // offset + 2 and offset + 3 are encoding count + + var i, j = offset + 4, cnt = 0; for (i = 0; i < encodings.length; i++) { if (encodings[i][0] === "Cursor" && !local_cursor) { Util.Debug("Skipping Cursor pseudo-encoding"); @@ -1344,23 +1377,25 @@ var RFB; // TODO: remove this when we have tight+non-true-color Util.Warn("Skipping tight as it is only supported with true color"); } else { - encList.push(encodings[i][1]); + var enc = encodings[i][1]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + cnt++; } } - var arr = [2]; // msg-type - arr.push8(0); // padding + buff[offset + 2] = cnt >> 8; + buff[offset + 3] = cnt; - arr.push16(encList.length); // encoding count - for (i = 0; i < encList.length; i++) { - arr.push32(encList[i]); - } - - return arr; + sock._sQlen += j - offset; }, - fbUpdateRequests: function (cleanDirty, fb_width, fb_height) { - var arr = []; + fbUpdateRequests: function (sock, cleanDirty, fb_width, fb_height) { + var offsetIncrement = 0; var cb = cleanDirty.cleanBox; var w, h; @@ -1368,7 +1403,7 @@ var RFB; w = typeof cb.w === "undefined" ? fb_width : cb.w; h = typeof cb.h === "undefined" ? fb_height : cb.h; // Request incremental for clean box - arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h)); + RFB.messages.fbUpdateRequest(sock, 1, cb.x, cb.y, w, h); } for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) { @@ -1376,24 +1411,33 @@ var RFB; // Force all (non-incremental) for dirty box w = typeof db.w === "undefined" ? fb_width : db.w; h = typeof db.h === "undefined" ? fb_height : db.h; - arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h)); + RFB.messages.fbUpdateRequest(sock, 0, db.x, db.y, w, h); } - - return arr; }, - fbUpdateRequest: function (incremental, x, y, w, h) { + fbUpdateRequest: function (sock, incremental, x, y, w, h) { + var buff = sock._sQ; + var offset = sock._sQlen; + if (typeof(x) === "undefined") { x = 0; } if (typeof(y) === "undefined") { y = 0; } - var arr = [3]; // msg-type - arr.push8(incremental); - arr.push16(x); - arr.push16(y); - arr.push16(w); - arr.push16(h); + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental; - return arr; + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; } }; diff --git a/include/websock.js b/include/websock.js index 71ae36ca..61d94672 100644 --- a/include/websock.js +++ b/include/websock.js @@ -45,10 +45,14 @@ function Websock() { this._rQlen = 0; // Next write position in the receive queue this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) this._rQmax = this._rQbufferSize / 8; - this._sQ = []; // Send queue // called in init: this._rQ = new Uint8Array(this._rQbufferSize); this._rQ = null; // Receive queue + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue + this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' this.maxBufferedAmount = 200; @@ -183,9 +187,9 @@ function Websock() { } if (this._websocket.bufferedAmount < this.maxBufferedAmount) { - if (this._sQ.length > 0) { + if (this._sQlen > 0) { this._websocket.send(this._encode_message()); - this._sQ = []; + this._sQlen = 0; } return true; @@ -197,8 +201,9 @@ function Websock() { }, send: function (arr) { - this._sQ = this._sQ.concat(arr); - return this.flush(); + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + return this.flush(); }, send_string: function (str) { @@ -218,12 +223,12 @@ function Websock() { _allocate_buffers: function () { this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); }, init: function (protocols, ws_schema) { this._allocate_buffers(); this._rQi = 0; - this._sQ = []; this._websocket = null; // Check for full typed array support @@ -327,7 +332,8 @@ function Websock() { // private methods _encode_message: function () { // Put in a binary arraybuffer - return (new Uint8Array(this._sQ)).buffer; + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); }, _decode_message: function (data) { diff --git a/tests/assertions.js b/tests/assertions.js index 6c01c4c9..930e1460 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -24,6 +24,12 @@ chai.use(function (_chai, utils) { _chai.Assertion.addMethod('sent', function (target_data) { var obj = this._obj; + obj.inspect = function () { + var res = { _websocket: obj._websocket, rQi: obj._rQi, _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen), + _sQ: new Uint8Array(obj._sQ.buffer, 0, obj._sQlen) }; + res.prototype = obj; + return res; + }; var data = obj._websocket._get_sent_data(); var same = true; for (var i = 0; i < obj.length; i++) { @@ -38,8 +44,8 @@ chai.use(function (_chai, utils) { this.assert(same, "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", "expected #{this} not to have sent the data #{act}", - target_data, - data); + Array.prototype.slice.call(target_data), + Array.prototype.slice.call(data)); }); _chai.Assertion.addProperty('array', function () { diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index 749c0eaf..21012059 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -51,14 +51,9 @@ var FakeWebSocket; }, _get_sent_data: function () { - var arr = []; - for (var i = 0; i < this.bufferedAmount; i++) { - arr[i] = this._send_queue[i]; - } - + var res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount); this.bufferedAmount = 0; - - return arr; + return res; }, _open: function (data) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js index f22314da..961d9eb5 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -23,10 +23,12 @@ describe('Remote Frame Buffer Protocol Client', function() { // Use a single set of buffers instead of reallocating to // speed up tests var sock = new Websock(); + var _sQ = new Uint8Array(sock._sQbufferSize); var rQ = new Uint8Array(sock._rQbufferSize); Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; Websock.prototype._allocate_buffers = function () { + this._sQ = _sQ; this._rQ = rQ; }; @@ -124,34 +126,34 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); client._sock._websocket._open(); - sinon.spy(client._sock, 'send'); + sinon.spy(client._sock, 'flush'); client._rfb_state = "normal"; client._view_only = false; }); it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { - var expected = []; - expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 1)); - expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 1)); - expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 1)); - expected = expected.concat(RFB.messages.keyEvent(0xFFFF, 0)); - expected = expected.concat(RFB.messages.keyEvent(0xFFE9, 0)); - expected = expected.concat(RFB.messages.keyEvent(0xFFE3, 0)); + var expected = {_sQ: new Uint8Array(48), _sQlen: 0}; + RFB.messages.keyEvent(expected, 0xFFE3, 1); + RFB.messages.keyEvent(expected, 0xFFE9, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 0); + RFB.messages.keyEvent(expected, 0xFFE9, 0); + RFB.messages.keyEvent(expected, 0xFFE3, 0); client.sendCtrlAltDel(); - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(expected._sQ); }); it('should not send the keys if we are not in a normal state', function () { client._rfb_state = "broken"; client.sendCtrlAltDel(); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should not send the keys if we are set as view_only', function () { client._view_only = true; client.sendCtrlAltDel(); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); @@ -160,34 +162,36 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); client._sock._websocket._open(); - sinon.spy(client._sock, 'send'); + sinon.spy(client._sock, 'flush'); client._rfb_state = "normal"; client._view_only = false; }); it('should send a single key with the given code and state (down = true)', function () { - var expected = RFB.messages.keyEvent(123, 1); + var expected = {_sQ: new Uint8Array(8), _sQlen: 0}; + RFB.messages.keyEvent(expected, 123, 1); client.sendKey(123, true); - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(expected._sQ); }); it('should send both a down and up event if the state is not specified', function () { - var expected = RFB.messages.keyEvent(123, 1); - expected = expected.concat(RFB.messages.keyEvent(123, 0)); + var expected = {_sQ: new Uint8Array(16), _sQlen: 0}; + RFB.messages.keyEvent(expected, 123, 1); + RFB.messages.keyEvent(expected, 123, 0); client.sendKey(123); - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(expected._sQ); }); it('should not send the key if we are not in a normal state', function () { client._rfb_state = "broken"; client.sendKey(123); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should not send the key if we are set as view_only', function () { client._view_only = true; client.sendKey(123); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); @@ -196,21 +200,22 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); client._sock._websocket._open(); - sinon.spy(client._sock, 'send'); + sinon.spy(client._sock, 'flush'); client._rfb_state = "normal"; client._view_only = false; }); it('should send the given text in a paste event', function () { - var expected = RFB.messages.clientCutText('abc'); + var expected = {_sQ: new Uint8Array(11), _sQlen: 0}; + RFB.messages.clientCutText(expected, 'abc'); client.clipboardPasteFrom('abc'); - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(expected._sQ); }); it('should not send the text if we are not in a normal state', function () { client._rfb_state = "broken"; client.clipboardPasteFrom('abc'); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); @@ -219,7 +224,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); client._sock._websocket._open(); - sinon.spy(client._sock, 'send'); + sinon.spy(client._sock, 'flush'); client._rfb_state = "normal"; client._view_only = false; client._supportsSetDesktopSize = true; @@ -240,19 +245,19 @@ describe('Remote Frame Buffer Protocol Client', function() { expected.push32(0); // flags client.setDesktopSize(1, 2); - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(new Uint8Array(expected)); }); it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { client._supportsSetDesktopSize = false; client.setDesktopSize(1,2); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should not send the request if we are not in a normal state', function () { client._rfb_state = "broken"; client.setDesktopSize(1,2); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); @@ -261,7 +266,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); client._sock._websocket._open(); - sinon.spy(client._sock, 'send'); + sinon.spy(client._sock, 'flush'); client._rfb_state = "normal"; client._view_only = false; client._rfb_xvp_ver = 1; @@ -269,27 +274,27 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send the shutdown signal on #xvpShutdown', function () { client.xvpShutdown(); - expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x02]); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02])); }); it('should send the reboot signal on #xvpReboot', function () { client.xvpReboot(); - expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x03]); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03])); }); it('should send the reset signal on #xvpReset', function () { client.xvpReset(); - expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x04]); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04])); }); it('should support sending arbitrary XVP operations via #xvpOp', function () { client.xvpOp(1, 7); - expect(client._sock).to.have.sent([0xFA, 0x00, 0x01, 0x07]); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x07])); }); it('should not send XVP operations with higher versions than we support', function () { expect(client.xvpOp(2, 7)).to.be.false; - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); }); @@ -502,7 +507,7 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_version).to.equal(0); var sent_data = client._sock._websocket._get_sent_data(); - expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]); + expect(new Uint8Array(sent_data.buffer, 0, 5)).to.array.equal(new Uint8Array([1, 2, 3, 4, 5])); }); it('should interpret version 003.003 as version 3.3', function () { @@ -559,7 +564,7 @@ describe('Remote Frame Buffer Protocol Client', function() { send_ver('000.000', client); expect(client._rfb_version).to.equal(0); var sent_data = client._sock._websocket._get_sent_data(); - expect(sent_data.slice(0, 5)).to.deep.equal([1, 2, 3, 4, 5]); + expect(new Uint8Array(sent_data.buffer, 0, 5)).to.array.equal(new Uint8Array([1, 2, 3, 4, 5])); expect(sent_data).to.have.length(250); send_ver('003.008', client); @@ -582,7 +587,7 @@ describe('Remote Frame Buffer Protocol Client', function() { expected[i] = expected_str.charCodeAt(i); } - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(new Uint8Array(expected)); }); it('should transition to the Security state on successful negotiation', function () { @@ -615,7 +620,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var auth_schemes = [2, 1, 2]; client._sock._websocket._receive_data(auth_schemes); expect(client._rfb_auth_scheme).to.equal(2); - expect(client._sock).to.have.sent([2]); + expect(client._sock).to.have.sent(new Uint8Array([2])); }); it('should fail if there are no supported schemes for versions >= 3.7', function () { @@ -721,7 +726,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._sock._websocket._receive_data(new Uint8Array(challenge)); var des_pass = RFB.genDES('passwd', challenge); - expect(client._sock).to.have.sent(des_pass); + expect(client._sock).to.have.sent(new Uint8Array(des_pass)); }); it('should transition to SecurityResult immediately after sending the password', function () { @@ -778,7 +783,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var expected = [22, 4, 6]; // auth selection, len user, len target for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } - expect(client._sock).to.have.sent(expected); + expect(client._sock).to.have.sent(new Uint8Array(expected)); }); }); @@ -826,14 +831,14 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should choose the notunnel tunnel type', function () { send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); - expect(client._sock).to.have.sent([0, 0, 0, 0]); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); }); it('should continue to sub-auth negotiation after tunnel negotiation', function () { send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); client._sock._websocket._get_sent_data(); // skip the tunnel choice here send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); - expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); expect(client._rfb_state).to.equal('SecurityResult'); }); @@ -849,7 +854,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should accept the "no auth" auth type and transition to SecurityResult', function () { client._rfb_tightvnc = true; send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); - expect(client._sock).to.have.sent([0, 0, 0, 1]); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); expect(client._rfb_state).to.equal('SecurityResult'); }); @@ -857,7 +862,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_tightvnc = true; client._negotiate_std_vnc_auth = sinon.spy(); send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); - expect(client._sock).to.have.sent([0, 0, 0, 2]); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; expect(client._rfb_auth_scheme).to.equal(2); }); @@ -921,13 +926,13 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send 1 if we are in shared mode', function () { client.set_shared(true); client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._sock).to.have.sent([1]); + expect(client._sock).to.have.sent(new Uint8Array([1])); }); it('should send 0 if we are not in shared mode', function () { client.set_shared(false); client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._sock).to.have.sent([0]); + expect(client._sock).to.have.sent(new Uint8Array([0])); }); }); @@ -1064,21 +1069,16 @@ describe('Remote Frame Buffer Protocol Client', function() { 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); - expected = expected.concat(RFB.messages.clientEncodings(client._encodings, false, true)); + // we skip the cursor encoding + var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), _sQlen: 0}; + RFB.messages.pixelFormat(expected, 4, 3, true); + RFB.messages.clientEncodings(expected, 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 } ] }; - expected = expected.concat(RFB.messages.fbUpdateRequests(expected_cdr, 27, 32)); + RFB.messages.fbUpdateRequests(expected, expected_cdr, 27, 32); send_server_init({ width: 27, height: 32 }, client); - expect(client._sock).to.have.sent(expected); - }); - - it('should check for sending mouse events', function () { - // be lazy with our checking so we don't have to check through the whole sent buffer - sinon.spy(client, '_checkEvents'); - send_server_init({}, client); - expect(client._checkEvents).to.have.been.calledOnce; + expect(client._sock).to.have.sent(expected._sQ); }); it('should transition to the "normal" state', function () { @@ -1161,14 +1161,15 @@ describe('Remote Frame Buffer Protocol Client', function() { } it('should send an update request if there is sufficient data', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; - var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 240, 20); + RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); client._framebufferUpdate = function () { return true; }; client._sock._websocket._receive_data(new Uint8Array([0])); - expect(client._sock).to.have.sent(expected_msg); + expect(client._sock).to.have.sent(expected_msg._sQ); }); it('should not send an update request if we need more data', function () { @@ -1177,9 +1178,10 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should resume receiving an update if we previously did not have enough data', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; - var expected_msg = RFB.messages.fbUpdateRequests(expected_cdr, 240, 20); + RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); // just enough to set FBU.rects client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); @@ -1188,7 +1190,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._framebufferUpdate = function () { return true; }; // we magically have enough data // 247 should *not* be used as the message type here client._sock._websocket._receive_data(new Uint8Array([247])); - expect(client._sock).to.have.sent(expected_msg); + expect(client._sock).to.have.sent(expected_msg._sQ); }); it('should parse out information from a header before any actual data comes in', function () { @@ -1710,58 +1712,61 @@ describe('Remote Frame Buffer Protocol Client', function() { var client; beforeEach(function () { client = make_rfb(); - client._sock.send = sinon.spy(); + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); client._rfb_state = 'normal'; }); it('should not send button messages in view-only mode', function () { client._view_only = true; client._mouse._onMouseButton(0, 0, 1, 0x001); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should not send movement messages in view-only mode', function () { client._view_only = true; client._mouse._onMouseMove(0, 0); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should send a pointer event on mouse button presses', function () { client._mouse._onMouseButton(10, 12, 1, 0x001); - expect(client._sock.send).to.have.been.calledOnce; - var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001); - expect(client._sock.send).to.have.been.calledWith(pointer_msg); + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a mask of 1 on mousedown', function () { client._mouse._onMouseButton(10, 12, 1, 0x001); - expect(client._sock.send).to.have.been.calledOnce; - var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x001); - expect(client._sock.send).to.have.been.calledWith(pointer_msg); + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + RFB.messages.pointerEvent(pointer_msg, 0, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a mask of 0 on mouseup', function () { client._mouse_buttonMask = 0x001; client._mouse._onMouseButton(10, 12, 0, 0x001); - expect(client._sock.send).to.have.been.calledOnce; - var pointer_msg = RFB.messages.pointerEvent(10, 12, 0x000); - expect(client._sock.send).to.have.been.calledWith(pointer_msg); + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a pointer event on mouse movement', function () { client._mouse._onMouseMove(10, 12); - expect(client._sock.send).to.have.been.calledOnce; - var pointer_msg = RFB.messages.pointerEvent(10, 12, 0); - expect(client._sock.send).to.have.been.calledWith(pointer_msg); + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should set the button mask so that future mouse movements use it', function () { client._mouse._onMouseButton(10, 12, 1, 0x010); - client._sock.send = sinon.spy(); client._mouse._onMouseMove(13, 9); - expect(client._sock.send).to.have.been.calledOnce; - var pointer_msg = RFB.messages.pointerEvent(13, 9, 0x010); - expect(client._sock.send).to.have.been.calledWith(pointer_msg); + var pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0}; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); + RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); + expect(client._sock).to.have.sent(pointer_msg._sQ); }); // NB(directxman12): we don't need to test not sending messages in @@ -1772,13 +1777,13 @@ describe('Remote Frame Buffer Protocol Client', function() { client._viewportDragging = true; client._display.viewportChangePos = sinon.spy(); client._mouse._onMouseMove(13, 9); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should not send button messages when initiating viewport dragging', function () { client._viewportDrag = true; client._mouse._onMouseButton(13, 9, 0x001); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); it('should be initiate viewport dragging on a button down event, if enabled', function () { @@ -1814,20 +1819,23 @@ describe('Remote Frame Buffer Protocol Client', function() { var client; beforeEach(function () { client = make_rfb(); - client._sock.send = sinon.spy(); + client._sock = new Websock(); + client._sock.open('ws://', 'binary'); + client._sock._websocket._open(); + sinon.spy(client._sock, 'flush'); }); it('should send a key message on a key press', function () { client._keyboard._onKeyPress(1234, 1); - expect(client._sock.send).to.have.been.calledOnce; - var key_msg = RFB.messages.keyEvent(1234, 1); - expect(client._sock.send).to.have.been.calledWith(key_msg); + var key_msg = {_sQ: new Uint8Array(8), _sQlen: 0}; + RFB.messages.keyEvent(key_msg, 1234, 1); + expect(client._sock).to.have.sent(key_msg._sQ); }); it('should not send messages in view-only mode', function () { client._view_only = true; client._keyboard._onKeyPress(1234, 1); - expect(client._sock.send).to.not.have.been.called; + expect(client._sock.flush).to.not.have.been.called; }); }); diff --git a/tests/test.websock.js b/tests/test.websock.js index a81f75da..14d57832 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -173,7 +173,8 @@ describe('Websock', function() { it('should actually send on the websocket if the websocket does not have too much buffered', function () { sock.maxBufferedAmount = 10; sock._websocket.bufferedAmount = 8; - sock._sQ = [1, 2, 3]; + sock._sQ = new Uint8Array([1, 2, 3]); + sock._sQlen = 3; var encoded = sock._encode_message(); sock.flush(); @@ -189,7 +190,7 @@ describe('Websock', function() { }); it('should not call send if we do not have anything queued up', function () { - sock._sQ = []; + sock._sQlen = 0; sock.maxBufferedAmount = 10; sock._websocket.bufferedAmount = 8; @@ -215,7 +216,7 @@ describe('Websock', function() { it('should add to the send queue', function () { sock.send([1, 2, 3]); var sq = sock.get_sQ(); - expect(sock.get_sQ().slice(sq.length - 3)).to.deep.equal([1, 2, 3]); + expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); }); it('should call flush', function () { @@ -425,15 +426,16 @@ describe('Websock', function() { sock._websocket._open(); }); - it('should convert the send queue into an ArrayBuffer', function () { - sock._sQ = [1, 2, 3]; - var res = sock._encode_message(); // An ArrayBuffer - expect(new Uint8Array(res)).to.deep.equal(new Uint8Array(res)); + it('should only send the send queue up to the send queue length', function () { + sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); + sock._sQlen = 3; + var res = sock._encode_message(); + expect(res).to.array.equal(new Uint8Array([1, 2, 3])); }); it('should properly pass the encoded data off to the actual WebSocket', function () { sock.send([1, 2, 3]); - expect(sock._websocket._get_sent_data()).to.deep.equal([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3])); }); }); }); From 07f514d887c27369f7675296a2cb1cb6d664cd91 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 28 May 2015 15:07:43 -0400 Subject: [PATCH 097/527] Allow the use of the PhantomJS remote debugger This commit adds the '--debugger ' option, which triggers the PhantomJS remote debugger. The initial output of the terminal when running the debugger gives more information on how to use it. --- tests/run_from_console.casper.js | 14 +++++++++++++- tests/run_from_console.js | 3 ++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/run_from_console.casper.js b/tests/run_from_console.casper.js index 57ed2be2..6a738a3e 100644 --- a/tests/run_from_console.casper.js +++ b/tests/run_from_console.casper.js @@ -15,7 +15,19 @@ var casper_opts = { } }; -var provide_emitter = function(file_paths) { +var provide_emitter = function(file_paths, debug_port) { + if (debug_port) { + casper_opts.child['remote-debugger-port'] = debug_port; + var debug_url = ('https://localhost:' + debug_port + + '/webkit/inspector/inspector.html?page='); + console.info('[remote-debugger] Navigate to ' + debug_url + '1 and ' + + 'run `__run();` in the console to continue loading.' + + '\n[remote-debugger] Navigate to ' + debug_url + '2 to ' + + 'view the actual page source.\n' + + '[remote-debugger] Use the `debugger;` statement to ' + + 'trigger an initial breakpoint.'); + } + var spooky = new Spooky(casper_opts, function(err) { if (err) { if (err.stack) console.warn(err.stack); diff --git a/tests/run_from_console.js b/tests/run_from_console.js index 2a5bb70b..f5c5bb4b 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -20,6 +20,7 @@ program .option('--output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') .option('-d, --debug', 'Show debug output (the "console" event) from the provider') .option('-r, --relative', 'Use relative paths in the generated HTML file') + .option('--debugger ', 'Enable the remote debugger for CasperJS') .parse(process.argv); if (program.tests.length === 0) { @@ -202,7 +203,7 @@ if (!program.outputHtml && !program.generateHtml) { .write("\n"); //console.log("Running tests %s using provider %s", program.tests.join(', '), prov.name); - var provider = prov.provide_emitter(file_paths); + var provider = prov.provide_emitter(file_paths, program.debugger); provider.on('test_ready', function(test_json) { console.log(''); From b0b5fc55e1fe5c575855104e9ce6df79329a8db7 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 28 May 2015 15:09:31 -0400 Subject: [PATCH 098/527] Fix multi-line assertion messages in test runner This commit prevents multi-line error messages from being truncated in the local test runner ('tests/run_from_console.js'). --- tests/run_from_console.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/run_from_console.js b/tests/run_from_console.js index f5c5bb4b..371e861a 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -250,6 +250,24 @@ if (!program.outputHtml && !program.generateHtml) { console.log(''); if (test_json.num_fails > 0 || program.printAll) { + var extract_error_lines = function (err) { + // the split is to avoid a weird thing where in PhantomJS where we get a stack trace too + var err_lines = err.split('\n'); + if (err_lines.length == 1) { + return err_lines[0]; + } else { + var ind; + for (ind = 0; ind < err_lines.length; ind++) { + var at_ind = err_lines[ind].trim().indexOf('at '); + if (at_ind === 0) { + break; + } + } + + return err_lines.slice(0, ind).join('\n'); + } + }; + var traverse_tree = function(indentation, node) { if (node.type == 'suite') { if (!node.has_subfailures && !program.printAll) return; @@ -281,7 +299,7 @@ if (!program.outputHtml && !program.generateHtml) { cursor.magenta(); console.log('- failed: '+node.text+test_json.replay); cursor.red(); - console.log(' '+node.error.split("\n")[0]); // the split is to avoid a weird thing where in PhantomJS where we get a stack trace too + console.log(' '+extract_error_lines(node.error)); cursor.reset(); console.log(''); } From f00193e08f7c55a7b0b55556ef96b10b41dd3bc3 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 2 Jun 2015 15:32:14 -0400 Subject: [PATCH 099/527] Skip unnecessary render queue object creation This commit skips object creation for the render queue when not needed. Instead of pushing an object onto the queue, and then immediately running the result, you call the function directly. Then, if the render queue is not empty, an object is created and pushed onto the queue. Otherwise, the functionality is just run directly. --- include/display.js | 106 ++++++++++++++++++++++++++++++++++----------- include/rfb.js | 57 +++++------------------- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/include/display.js b/include/display.js index 418b431d..c30a977d 100644 --- a/include/display.js +++ b/include/display.js @@ -339,18 +339,41 @@ var Display; this._renderQ = []; }, - fillRect: function (x, y, width, height, color) { - this._setFillColor(color); - this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + fillRect: function (x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'fill', + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'color': color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x - this._viewportLoc.x, y - this._viewportLoc.y, width, height); + } }, - copyImage: function (old_x, old_y, new_x, new_y, w, h) { - var x1 = old_x - this._viewportLoc.x; - var y1 = old_y - this._viewportLoc.y; - var x2 = new_x - this._viewportLoc.x; - var y2 = new_y - this._viewportLoc.y; + copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'copy', + 'old_x': old_x, + 'old_y': old_y, + 'x': new_x, + 'y': new_y, + 'width': w, + 'height': h, + }); + } else { + var x1 = old_x - this._viewportLoc.x; + var y1 = old_y - this._viewportLoc.y; + var x2 = new_x - this._viewportLoc.x; + var y2 = new_y - this._viewportLoc.y; - this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + this._drawCtx.drawImage(this._target, x1, y1, w, h, x2, y2, w, h); + } }, // start updating a tile @@ -382,7 +405,7 @@ var Display; data[i + 3] = 255; } } else { - this.fillRect(x, y, width, height, color); + this.fillRect(x, y, width, height, color, true); } }, @@ -413,7 +436,7 @@ var Display; } } } else { - this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color); + this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true); } }, @@ -426,16 +449,34 @@ var Display; // else: No-op -- already done by setSubTile }, - blitImage: function (x, y, width, height, arr, offset) { - if (this._true_color) { + blitImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'blit', + 'data': arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { this._bgrxImageData(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) { + blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this.renderQ_push({ + 'type': 'blitRgb', + 'data': arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else if (this._true_color) { this._rgbImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); } else { // probably wrong? @@ -443,8 +484,24 @@ var Display; } }, - blitRgbxImage: function (x, y, width, height, arr, offset) { - this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, but it + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this.renderQ_push({ + 'type': 'blitRgbx', + 'data': new_arr, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + }); + } else { + this._rgbxImageData(x, y, this._viewportLoc.x, this._viewportLoc.y, width, height, arr, offset); + } }, blitStringImage: function (str, x, y) { @@ -626,13 +683,12 @@ var Display; _rgbxImageData: function (x, y, vx, vy, width, height, arr, offset) { // NB(directxman12): arr must be an Type Array view - // NB(directxman12): this only works var img; if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { - img = new ImageData(new Uint8ClampedArray(arr.buffer, 0, width * height * 4), width, height); + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); } else { img = this._drawCtx.createImageData(width, height); - img.data.set(new Uint8ClampedArray(arr.buffer, 0, width * height * 4)); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); } this._drawCtx.putImageData(img, x - vx, y - vy); }, @@ -657,19 +713,19 @@ var Display; var a = this._renderQ[0]; switch (a.type) { case 'copy': - this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height); + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); break; case 'fill': - this.fillRect(a.x, a.y, a.width, a.height, a.color); + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); break; case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0); + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0); + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; case 'blitRgbx': - this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0); + this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); break; case 'img': if (a.img.complete) { diff --git a/include/rfb.js b/include/rfb.js index b8615af8..b7a811d5 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1483,15 +1483,10 @@ var RFB; COPYRECT: function () { this._FBU.bytes = 4; if (this._sock.rQwait("COPYRECT", 4)) { return false; } - this._display.renderQ_push({ - 'type': 'copy', - 'old_x': this._sock.rQshift16(), - 'old_y': this._sock.rQshift16(), - 'x': this._FBU.x, - 'y': this._FBU.y, - 'width': this._FBU.width, - 'height': this._FBU.height - }); + this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), + this._FBU.x, this._FBU.y, this._FBU.width, + this._FBU.height); + this._FBU.rects--; this._FBU.bytes = 0; return true; @@ -1842,28 +1837,10 @@ var RFB; var rgbx; if (numColors == 2) { rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); - - /*this._display.renderQ_push({ - 'type': 'blitRgbx', - 'data': rgbx, - 'x': this._FBU.x, - 'y': this._FBU.y, - 'width': this._FBU.width, - 'height': this._FBU.height - });*/ - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } else { rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); - - /*this._display.renderQ_push({ - 'type': 'blitRgbx', - 'data': rgbx, - 'x': this._FBU.x, - 'y': this._FBU.y, - 'width': this._FBU.width, - 'height': this._FBU.height - });*/ - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0); + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); } @@ -1905,14 +1882,7 @@ var RFB; data = decompress(this._sock.rQshiftBytes(cl_data)); } - this._display.renderQ_push({ - 'type': 'blitRgb', - 'data': data, - 'x': this._FBU.x, - 'y': this._FBU.y, - 'width': this._FBU.width, - 'height': this._FBU.height - }); + this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); return true; }.bind(this); @@ -1960,16 +1930,9 @@ var RFB; // Determine FBU.bytes 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]] - }); + // skip ctl byte + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._sock.rQskipBytes(4); break; case "png": case "jpeg": From a825582196c69b5999032af00fd87590fc62fdc3 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 14 Aug 2015 17:02:00 +0200 Subject: [PATCH 100/527] Only work with integers when panning to avoid getting a blurry image. Also disable image-smoothing to avoid bugs seen on Android which were also causing a blurry image while panning. --- include/display.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/include/display.js b/include/display.js index 8994856b..f20a5571 100644 --- a/include/display.js +++ b/include/display.js @@ -98,6 +98,8 @@ var Display; // Public methods viewportChangePos: function (deltaX, deltaY) { var vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); if (!this._viewport) { deltaX = -vp.w; // clamped later of out of bounds @@ -170,16 +172,34 @@ var Display; h = deltaY; } - // Copy the valid part of the viewport to the shifted location var saveStyle = this._drawCtx.fillStyle; var canvas = this._target; this._drawCtx.fillStyle = "rgb(255,255,255)"; + + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when panning. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + if (this._drawCtx.mozImageSmoothingEnabled) { + this._drawCtx.mozImageSmoothingEnabled = false; + } else if (this._drawCtx.webkitImageSmoothingEnabled) { + this._drawCtx.webkitImageSmoothingEnabled = false; + } else if (this._drawCtx.msImageSmoothingEnabled) { + this._drawCtx.msImageSmoothingEnabled = false; + } else if (this._drawCtx.imageSmoothingEnabled) { + this._drawCtx.imageSmoothingEnabled = false; + } + + // Copy the valid part of the viewport to the shifted location + this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, -deltaY, vp.w, vp.h); + if (deltaX !== 0) { - this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, -deltaX, 0, vp.w, vp.h); this._drawCtx.fillRect(x1, 0, w, vp.h); } if (deltaY !== 0) { - this._drawCtx.drawImage(canvas, 0, 0, vp.w, vp.h, 0, -deltaY, vp.w, vp.h); this._drawCtx.fillRect(0, y1, vp.w, h); } this._drawCtx.fillStyle = saveStyle; From 340290fa1d6a53d30a38b6604bcacef5940c4c52 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 24 Aug 2015 16:41:00 -0400 Subject: [PATCH 101/527] UI: Fix typo preventing reconnect after password There was a typo in ui.js preventing the use of the connect button after a password was entered. --- include/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index 929e37f8..b2938d4d 100644 --- a/include/ui.js +++ b/include/ui.js @@ -625,7 +625,7 @@ var UI; UI.rfb.sendPassword($D('noVNC_password').value); //Reset connect button. $D('noVNC_connect_button').value = "Connect"; - $D('noVNC_connect_button').onclick = UI.Connect; + $D('noVNC_connect_button').onclick = UI.connect; //Hide connection panel. UI.toggleConnectPanel(); return false; From bb180145c654c4bc1ab9eca15a7acaa494d9fa50 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 24 Aug 2015 19:30:44 -0400 Subject: [PATCH 102/527] Tests: Fixed bug in displayed assertion This `displayed` assertion had a bug that was causing it to not actually check anything (it was using obj.length instead of data_cl.length). This fixes that. --- tests/assertions.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/assertions.js b/tests/assertions.js index 930e1460..4bd0cf40 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -6,10 +6,15 @@ chai.use(function (_chai, utils) { // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that var data = new Uint8Array(data_cl); var same = true; - for (var i = 0; i < obj.length; i++) { - if (data[i] != target_data[i]) { - same = false; - break; + var len = data_cl.length; + if (len != target_data.length) { + same = false; + } else { + for (var i = 0; i < len; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } } } if (!same) { From a369a80c2480af685b7bddacddae9cca3aea95f9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 24 Aug 2015 19:34:30 -0400 Subject: [PATCH 103/527] Fix bug in non-true-color code There was a bug caused by 38781d931ec18304f51ed3469faff8387e3cbc55 which prevented color map look-ups sent by rfb.js from working properly, since display.js expected a single-item array, and rfb.js sent just them item value itself (a number) instead. This fixes that, and tweaks the corresponding test to match that behavior. --- include/display.js | 2 +- tests/test.display.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/display.js b/include/display.js index 80530022..6bf89bd6 100644 --- a/include/display.js +++ b/include/display.js @@ -667,7 +667,7 @@ var Display; if (this._true_color) { bgr = color; } else { - bgr = this._colourMap[color[0]]; + bgr = this._colourMap[color]; } var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')'; diff --git a/tests/test.display.js b/tests/test.display.js index 56dfc220..32a92e22 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -353,9 +353,9 @@ describe('Display/Canvas Helper', function () { it('should support drawing solid colors with color maps', function () { display._true_color = false; display.set_colourMap({ 0: [0xff, 0, 0], 1: [0, 0xff, 0] }); - display.fillRect(0, 0, 4, 4, [1]); - display.fillRect(0, 0, 2, 2, [0]); - display.fillRect(2, 2, 2, 2, [0]); + display.fillRect(0, 0, 4, 4, 1); + display.fillRect(0, 0, 2, 2, 0); + display.fillRect(2, 2, 2, 2, 0); expect(display).to.have.displayed(checked_data); }); From 89bdc8ce488153ba3299695791d3d2309751e1e4 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 26 Aug 2015 14:28:10 -0400 Subject: [PATCH 104/527] Fix buffer over-reads in handle_tight For performance reasons, the `handle_tight` function skips the use of the receive queue API and uses the raw receive queue directly. Because of the way that typed array receive queue gets reused, this introduced the potential for buffer over-reads. To address this, a new function, `rQwhole`, was introduced. `rQwhole` simply returns a new view into the receive queue that starts at 0 and ends at the current recorded end of the queue. `handle_tight` now makes use of this function. Fixes #522 --- include/rfb.js | 2 +- include/websock.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index b7a811d5..b45537c7 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1782,8 +1782,8 @@ var RFB; return dest; }.bind(this); - var rQ = this._sock.get_rQ(); var rQi = this._sock.get_rQi(); + var rQ = this._sock.rQwhole(); var cmode, data; var cl_header, cl_data; diff --git a/include/websock.js b/include/websock.js index 61d94672..892238b5 100644 --- a/include/websock.js +++ b/include/websock.js @@ -154,6 +154,10 @@ function Websock() { this._rQi += len; }, + rQwhole: function () { + return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + }, + rQslice: function (start, end) { if (end) { return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); From 045d922406678e0ea4167e09edabd89f25d3df0e Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 22 Sep 2015 14:59:44 +0200 Subject: [PATCH 105/527] Moved resizeTimout to the UI object, no reason to have it outside anymore.. --- include/ui.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/include/ui.js b/include/ui.js index b2938d4d..cb5717cb 100644 --- a/include/ui.js +++ b/include/ui.js @@ -15,8 +15,6 @@ var UI; (function () { "use strict"; - var resizeTimeout; - // Load supporting scripts window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", @@ -25,9 +23,10 @@ var UI; UI = { - rfb_state : 'loaded', - settingsOpen : false, - connSettingsOpen : false, + rfb_state: 'loaded', + resizeTimeout: null, + settingsOpen: false, + connSettingsOpen: false, popupStatusTimeout: null, clipboardOpen: false, keyboardVisible: false, @@ -253,8 +252,8 @@ var UI; // When the local window has been resized, wait until the size remains // the same for 0.5 seconds before sending the request for changing // the resolution of the session - clearTimeout(resizeTimeout); - resizeTimeout = setTimeout(function(){ + clearTimeout(UI.resizeTimeout); + UI.resizeTimeout = setTimeout(function(){ display.set_maxWidth(size.w); display.set_maxHeight(size.h); Util.Debug('Attempting setDesktopSize(' + From c802d9318930ab703d2a359dcf10fb5928f9029e Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 22 Sep 2015 16:19:52 -0400 Subject: [PATCH 106/527] Make sure Pako always has enough room Previously, we used a fixed chunkSize of 100KiB for Pako's output buffer. Using a hardcoded size caused issues, since Pako would assume we wanted to use multiple chunks, and we didn't deal with this. Now, `Inflator#inflate()` takes a new `expected` argument, which indicates the expected output size. If this is bigger than the current chunkSize, Inflator allocates a new output buffer that's big enough to hold the output. Fixes #531 --- docs/notes | 2 +- include/inflator.js | 35 ++++++++++++++++++++++------------- include/rfb.js | 9 +++++---- utils/inflator.partial.js | 14 +++++++++++--- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/docs/notes b/docs/notes index 6ff5ec19..036cd510 100644 --- a/docs/notes +++ b/docs/notes @@ -2,4 +2,4 @@ Rebuilding inflator.js - Download pako from npm - Install browserify using npm -- browserify utils/inflator.partial.js -o include/inflator.js +- browserify utils/inflator.partial.js -o include/inflator.js -s inflator diff --git a/include/inflator.js b/include/inflator.js index a9c75a62..48ede208 100644 --- a/include/inflator.js +++ b/include/inflator.js @@ -234,7 +234,8 @@ module.exports = function inflate_fast(strm, start) { var wsize; /* window size or zero if not using window */ var whave; /* valid bytes in the window */ var wnext; /* window write index */ - var window; /* allocated sliding window, if wsize != 0 */ + // Use `s_window` instead `window`, avoid conflict with instrumentation tools + var s_window; /* allocated sliding window, if wsize != 0 */ var hold; /* local strm.hold */ var bits; /* local strm.bits */ var lcode; /* local strm.lencode */ @@ -268,7 +269,7 @@ module.exports = function inflate_fast(strm, start) { wsize = state.wsize; whave = state.whave; wnext = state.wnext; - window = state.window; + s_window = state.window; hold = state.hold; bits = state.bits; lcode = state.lencode; @@ -386,13 +387,13 @@ module.exports = function inflate_fast(strm, start) { //#endif } from = 0; // window index - from_source = window; + from_source = s_window; if (wnext === 0) { /* very common case */ from += wsize - op; if (op < len) { /* some from window */ len -= op; do { - output[_out++] = window[from++]; + output[_out++] = s_window[from++]; } while (--op); from = _out - dist; /* rest from output */ from_source = output; @@ -404,14 +405,14 @@ module.exports = function inflate_fast(strm, start) { if (op < len) { /* some from end of window */ len -= op; do { - output[_out++] = window[from++]; + output[_out++] = s_window[from++]; } while (--op); from = 0; if (wnext < len) { /* some from start of window */ op = wnext; len -= op; do { - output[_out++] = window[from++]; + output[_out++] = s_window[from++]; } while (--op); from = _out - dist; /* rest from output */ from_source = output; @@ -423,7 +424,7 @@ module.exports = function inflate_fast(strm, start) { if (op < len) { /* some from window */ len -= op; do { - output[_out++] = window[from++]; + output[_out++] = s_window[from++]; } while (--op); from = _out - dist; /* rest from output */ from_source = output; @@ -2371,9 +2372,9 @@ function ZStream() { module.exports = ZStream; -},{}],"/partial_inflator.js":[function(require,module,exports){ -var zlib = require('./lib/zlib/inflate.js'); -var ZStream = require('./lib/zlib/zstream.js'); +},{}],8:[function(require,module,exports){ +var zlib = require('../node_modules/pako/lib/zlib/inflate.js'); +var ZStream = require('../node_modules/pako/lib/zlib/zstream.js'); var Inflate = function () { this.strm = new ZStream(); @@ -2385,12 +2386,20 @@ var Inflate = function () { }; Inflate.prototype = { - inflate: function (data, flush) { + inflate: function (data, flush, expected) { this.strm.input = data; this.strm.avail_in = this.strm.input.length; this.strm.next_in = 0; this.strm.next_out = 0; + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + this.strm.avail_out = this.chunkSize; zlib.inflate(this.strm, flush); @@ -2405,5 +2414,5 @@ Inflate.prototype = { module.exports = {Inflate: Inflate}; -},{"./lib/zlib/inflate.js":5,"./lib/zlib/zstream.js":7}]},{},[])("/partial_inflator.js") -}); +},{"../node_modules/pako/lib/zlib/inflate.js":5,"../node_modules/pako/lib/zlib/zstream.js":7}]},{},[8])(8) +}); \ No newline at end of file diff --git a/include/rfb.js b/include/rfb.js index b45537c7..4ea5fd07 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1688,16 +1688,17 @@ var RFB; var resetStreams = 0; var streamId = -1; - var decompress = function (data) { + var decompress = function (data, expected) { for (var i = 0; i < 4; i++) { if ((resetStreams >> i) & 1) { this._FBU.zlibs[i].reset(); + console.debug('RESET!'); Util.Info("Reset zlib stream " + i); } } //var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); - var uncompressed = this._FBU.zlibs[streamId].inflate(data, true); + var uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); /*if (uncompressed.status !== 0) { Util.Error("Invalid data in zlib stream"); }*/ @@ -1830,7 +1831,7 @@ var RFB; if (raw) { data = this._sock.rQshiftBytes(cl_data); } else { - data = decompress(this._sock.rQshiftBytes(cl_data)); + data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); } // Convert indexed (palette based) image data to RGB @@ -1879,7 +1880,7 @@ var RFB; if (raw) { data = this._sock.rQshiftBytes(cl_data); } else { - data = decompress(this._sock.rQshiftBytes(cl_data)); + data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); } this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); diff --git a/utils/inflator.partial.js b/utils/inflator.partial.js index 2522d781..8b6d1095 100644 --- a/utils/inflator.partial.js +++ b/utils/inflator.partial.js @@ -1,5 +1,5 @@ -var zlib = require('./lib/zlib/inflate.js'); -var ZStream = require('./lib/zlib/zstream.js'); +var zlib = require('../node_modules/pako/lib/zlib/inflate.js'); +var ZStream = require('../node_modules/pako/lib/zlib/zstream.js'); var Inflate = function () { this.strm = new ZStream(); @@ -11,12 +11,20 @@ var Inflate = function () { }; Inflate.prototype = { - inflate: function (data, flush) { + inflate: function (data, flush, expected) { this.strm.input = data; this.strm.avail_in = this.strm.input.length; this.strm.next_in = 0; this.strm.next_out = 0; + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); + } + this.strm.avail_out = this.chunkSize; zlib.inflate(this.strm, flush); From 3e3df4dbb11b008d1a629dea1e576f976b4f8fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20de=20Giessen?= Date: Fri, 16 Oct 2015 16:38:35 +0200 Subject: [PATCH 107/527] Added return to correctly wait for password input before proceeding. Fixes #542 --- include/rfb.js | 1 + 1 file changed, 1 insertion(+) diff --git a/include/rfb.js b/include/rfb.js index 4ea5fd07..dfd9269c 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -739,6 +739,7 @@ var RFB; // an RFB state change and a UI interface issue this._updateState('password', "Password Required"); this._onPasswordRequired(this); + return false; } if (this._sock.rQwait("auth challenge", 16)) { return false; } From 494b407a0a54c43a395110f99141e8123c96e2a4 Mon Sep 17 00:00:00 2001 From: Ganesh Varadarajan Date: Thu, 22 Oct 2015 19:57:07 +0530 Subject: [PATCH 108/527] Add hash fragment as an optional method to supply config variables. Any config variable like host, port, password, token may be specified either in the query string (like now), or in the URL hash fragment. In case a given variable is present in both, the value in the fragment takes precedence. Supplying variables in the fragment avoids leaking them to the web server hosting the noVNC viewer HTML. --- include/ui.js | 4 ++-- include/webutil.js | 23 +++++++++++++++++++++++ vnc.html | 2 ++ vnc_auto.html | 30 ++++++++++++++++-------------- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/include/ui.js b/include/ui.js index cb5717cb..38fbe4bd 100644 --- a/include/ui.js +++ b/include/ui.js @@ -97,7 +97,7 @@ var UI; UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); - var autoconnect = WebUtil.getQueryVar('autoconnect', false); + var autoconnect = WebUtil.getConfigVar('autoconnect', false); if (autoconnect === 'true' || autoconnect == '1') { autoconnect = true; UI.connect(); @@ -355,7 +355,7 @@ var UI; // Initial page load read/initialization of settings initSetting: function(name, defVal) { // Check Query string followed by cookie - var val = WebUtil.getQueryVar(name); + var val = WebUtil.getConfigVar(name); if (val === null) { val = WebUtil.readSetting(name, defVal); } diff --git a/include/webutil.js b/include/webutil.js index e674bf94..f10aa0d7 100644 --- a/include/webutil.js +++ b/include/webutil.js @@ -90,6 +90,29 @@ WebUtil.getQueryVar = function (name, defVal) { } }; +// Read a hash fragment variable +WebUtil.getHashVar = function (name, defVal) { + "use strict"; + var re = new RegExp('.*[&#]' + name + '=([^&]*)'), + match = document.location.hash.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + if (match) { + return decodeURIComponent(match[1]); + } else { + return defVal; + } +}; + +// Read a variable from the fragment or the query string +// Fragment takes precedence +WebUtil.getConfigVar = function (name, defVal) { + "use strict"; + var val = WebUtil.getHashVar(name); + if (val === null) { + val = WebUtil.getQueryVar(name, defVal); + } + return val; +}; /* * Cookie handling. Dervied from: http://www.quirksmode.org/js/cookies.html diff --git a/vnc.html b/vnc.html index 1a293d09..e2250f5d 100644 --- a/vnc.html +++ b/vnc.html @@ -11,6 +11,8 @@ Connect parameters are provided in query string: http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + or the fragment: + http://example.com/#host=HOST&port=PORT&encrypt=1&true_color=1 --> noVNC diff --git a/vnc_auto.html b/vnc_auto.html index 86cfde75..04803223 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -11,6 +11,8 @@ Connect parameters are provided in query string: http://example.com/?host=HOST&port=PORT&encrypt=1&true_color=1 + or the fragment: + http://example.com/#host=HOST&port=PORT&encrypt=1&true_color=1 --> noVNC @@ -84,7 +86,7 @@ function UIresize() { - if (WebUtil.getQueryVar('resize', false)) { + if (WebUtil.getConfigVar('resize', false)) { var innerW = window.innerWidth; var innerH = window.innerHeight; var controlbarH = $D('noVNC_status_bar').offsetHeight; @@ -183,11 +185,11 @@ $D('xvpRebootButton').onclick = xvpReboot; $D('xvpResetButton').onclick = xvpReset; - WebUtil.init_logging(WebUtil.getQueryVar('logging', 'warn')); - document.title = unescape(WebUtil.getQueryVar('title', 'noVNC')); + WebUtil.init_logging(WebUtil.getConfigVar('logging', 'warn')); + document.title = unescape(WebUtil.getConfigVar('title', 'noVNC')); // By default, use the host and port of server that served this file - host = WebUtil.getQueryVar('host', window.location.hostname); - port = WebUtil.getQueryVar('port', window.location.port); + host = WebUtil.getConfigVar('host', window.location.hostname); + port = WebUtil.getConfigVar('port', window.location.port); // if port == 80 (or 443) then it won't be present and should be // set manually @@ -202,13 +204,13 @@ // If a token variable is passed in, set the parameter in a cookie. // This is used by nova-novncproxy. - token = WebUtil.getQueryVar('token', null); + token = WebUtil.getConfigVar('token', null); if (token) { WebUtil.createCookie('token', token, 1) } - password = WebUtil.getQueryVar('password', ''); - path = WebUtil.getQueryVar('path', 'websockify'); + password = WebUtil.getConfigVar('password', ''); + path = WebUtil.getConfigVar('path', 'websockify'); if ((!host) || (!port)) { updateState(null, 'fatal', null, 'Must specify host and port in URL'); @@ -217,13 +219,13 @@ try { rfb = new RFB({'target': $D('noVNC_canvas'), - 'encrypt': WebUtil.getQueryVar('encrypt', + 'encrypt': WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:")), - 'repeaterID': WebUtil.getQueryVar('repeaterID', ''), - 'true_color': WebUtil.getQueryVar('true_color', true), - 'local_cursor': WebUtil.getQueryVar('cursor', true), - 'shared': WebUtil.getQueryVar('shared', true), - 'view_only': WebUtil.getQueryVar('view_only', false), + 'repeaterID': WebUtil.getConfigVar('repeaterID', ''), + 'true_color': WebUtil.getConfigVar('true_color', true), + 'local_cursor': WebUtil.getConfigVar('cursor', true), + 'shared': WebUtil.getConfigVar('shared', true), + 'view_only': WebUtil.getConfigVar('view_only', false), 'onUpdateState': updateState, 'onXvpInit': xvpInit, 'onPasswordRequired': passwordRequired, From 54e835eeac811ef826fea810378c7f1a98797a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sarrailh=20R=C3=A9mi?= Date: Mon, 23 Nov 2015 13:08:05 +0100 Subject: [PATCH 109/527] Small typo in launch.sh --- utils/launch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/launch.sh b/utils/launch.sh index 2a0106de..ecee06cd 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -108,7 +108,7 @@ if [[ -e ${HERE}/websockify ]]; then if [[ ! -x $WEBSOCKIFY ]]; then echo "The path ${HERE}/websockify exists, but $WEBSOCKIFY either does not exist or is not executable." - echo "If you inteded to use an installed websockify package, please remove ${HERE}/websockify." + echo "If you intended to use an installed websockify package, please remove ${HERE}/websockify." exit 1 fi From c8f14d175bbfe00f69fde268bd10de2b80a089d9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 22 Dec 2015 13:51:30 -0500 Subject: [PATCH 110/527] Disable copyWithin Use in Websock.js the `copyWithin` function of typed arrays has performance issues in some versions of Chromium, and doesn't doesn't have enough of a performance impact to justify leaving it enabled. --- include/websock.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/include/websock.js b/include/websock.js index 892238b5..7c27255e 100644 --- a/include/websock.js +++ b/include/websock.js @@ -67,6 +67,11 @@ function Websock() { (function () { "use strict"; + // this has performance issues in some versions Chromium, and + // doesn't gain a tremendous amount of performance increase in Firefox + // at the moment. It may be valuable to turn it on in the future. + var ENABLE_COPYWITHIN = false; + var typedArrayToString = (function () { // This is only for PhantomJS, which doesn't like apply-ing // with Typed Arrays @@ -364,8 +369,7 @@ function Websock() { this._rQ = new Uint8Array(this._rQbufferSize); this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); } else { - if (this._rQ.copyWithin) { - // Firefox only, ATM + if (ENABLE_COPYWITHIN) { this._rQ.copyWithin(0, this._rQi); } else { this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); From 40037b6a29d3f372b16073f7b2d06850c32a1cd9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Thu, 3 Dec 2015 21:30:47 -0500 Subject: [PATCH 111/527] On-Demand Dynamic Receive Queue Resizing This commit causes the receive queue to dynamically resize to fit incoming messages. Fixes #557 --- include/websock.js | 60 +++++++++++++++++++++++++++++++------------ tests/test.websock.js | 14 ++++++++++ 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/include/websock.js b/include/websock.js index 7c27255e..f3336a0a 100644 --- a/include/websock.js +++ b/include/websock.js @@ -66,12 +66,13 @@ function Websock() { (function () { "use strict"; - // this has performance issues in some versions Chromium, and // doesn't gain a tremendous amount of performance increase in Firefox // at the moment. It may be valuable to turn it on in the future. var ENABLE_COPYWITHIN = false; + var MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB + var typedArrayToString = (function () { // This is only for PhantomJS, which doesn't like apply-ing // with Typed Arrays @@ -345,9 +346,49 @@ function Websock() { return new Uint8Array(this._sQ.buffer, 0, this._sQlen); }, + _expand_compact_rQ: function (min_fit) { + var resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; + if (resizeNeeded) { + if (!min_fit) { + // just double the size if we need to do compaction + this._rQbufferSize *= 2; + } else { + // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 + this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; + } + } + + // we don't want to grow unboundedly + if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { + this._rQbufferSize = MAX_RQ_GROW_SIZE; + if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { + throw new Exception("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); + } + } + + if (resizeNeeded) { + var old_rQbuffer = this._rQ.buffer; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else { + if (ENABLE_COPYWITHIN) { + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); + } + } + + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + }, + _decode_message: function (data) { // push arraybuffer values onto the end var u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expand_compact_rQ(u8.length); + } this._rQ.set(u8, this._rQlen); this._rQlen += u8.length; }, @@ -362,22 +403,7 @@ function Websock() { this._rQlen = 0; this._rQi = 0; } else if (this._rQlen > this._rQmax) { - if (this._rQlen - this._rQi > 0.5 * this._rQbufferSize) { - var old_rQbuffer = this._rQ.buffer; - this._rQbufferSize *= 2; - this._rQmax = this._rQbufferSize / 8; - this._rQ = new Uint8Array(this._rQbufferSize); - this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); - } else { - if (ENABLE_COPYWITHIN) { - this._rQ.copyWithin(0, this._rQi); - } else { - this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); - } - } - - this._rQlen = this._rQlen - this._rQi; - this._rQi = 0; + this._expand_compact_rQ(); } } else { Util.Debug("Ignoring empty message"); diff --git a/tests/test.websock.js b/tests/test.websock.js index 14d57832..953a5268 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -404,6 +404,20 @@ describe('Websock', function() { expect(sock.get_rQi()).to.equal(0); }); + it('should automatically resize the receive queue if the incoming message is too large', function () { + sock._rQ = new Uint8Array(20); + sock._rQlen = 0; + sock.set_rQi(0); + sock._rQbufferSize = 20; + sock._rQmax = 2; + var msg = { data: new Uint8Array(30).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._rQlen).to.equal(30); + expect(sock.get_rQi()).to.equal(0); + expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen + }); + it('should call the error event handler on an exception', function () { sock._eventHandlers.error = sinon.spy(); sock._eventHandlers.message = sinon.stub().throws(); From 0252c7f7664b5c8848aecd55b14f743122656a92 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 6 Jan 2016 11:09:34 -0500 Subject: [PATCH 112/527] Remove remaining references to jsunzip.js This commit removes remaining references to jsunzip.js, which is no longer actually used in noVNC. Closes #436. --- LICENSE.txt | 3 - include/jsunzip.js | 676 -------------------------------------------- include/rfb.js | 2 - tests/vnc_perf.html | 2 +- 4 files changed, 1 insertion(+), 682 deletions(-) delete mode 100755 include/jsunzip.js diff --git a/LICENSE.txt b/LICENSE.txt index 82e8a6a1..924d2b0c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -9,7 +9,6 @@ is not limited to): include/des.js include/display.js include/input.js - include/jsunzip.js include/keysym.js include/logo.js include/playback.js @@ -49,8 +48,6 @@ licenses (all MPL 2.0 compatible): include/des.js : Various BSD style licenses - include/jsunzip.js : zlib/libpng license - include/chrome-app/tcp-stream.js : Apache 2.0 license diff --git a/include/jsunzip.js b/include/jsunzip.js deleted file mode 100755 index 8968f866..00000000 --- a/include/jsunzip.js +++ /dev/null @@ -1,676 +0,0 @@ -/* - * JSUnzip - * - * Copyright (c) 2011 by Erik Moller - * All Rights Reserved - * - * This software is provided 'as-is', without any express - * or implied warranty. In no event will the authors be - * held liable for any damages arising from the use of - * this software. - * - * Permission is granted to anyone to use this software - * for any purpose, including commercial applications, - * and to alter it and redistribute it freely, subject to - * the following restrictions: - * - * 1. The origin of this software must not be - * misrepresented; you must not claim that you - * wrote the original software. If you use this - * software in a product, an acknowledgment in - * the product documentation would be appreciated - * but is not required. - * - * 2. Altered source versions must be plainly marked - * as such, and must not be misrepresented as - * being the original software. - * - * 3. This notice may not be removed or altered from - * any source distribution. - */ - -var tinf; - -function JSUnzip() { - - this.getInt = function(offset, size) { - switch (size) { - case 4: - return (this.data.charCodeAt(offset + 3) & 0xff) << 24 | - (this.data.charCodeAt(offset + 2) & 0xff) << 16 | - (this.data.charCodeAt(offset + 1) & 0xff) << 8 | - (this.data.charCodeAt(offset + 0) & 0xff); - break; - case 2: - return (this.data.charCodeAt(offset + 1) & 0xff) << 8 | - (this.data.charCodeAt(offset + 0) & 0xff); - break; - default: - return this.data.charCodeAt(offset) & 0xff; - break; - } - }; - - this.getDOSDate = function(dosdate, dostime) { - var day = dosdate & 0x1f; - var month = ((dosdate >> 5) & 0xf) - 1; - var year = 1980 + ((dosdate >> 9) & 0x7f) - var second = (dostime & 0x1f) * 2; - var minute = (dostime >> 5) & 0x3f; - hour = (dostime >> 11) & 0x1f; - return new Date(year, month, day, hour, minute, second); - } - - this.open = function(data) { - this.data = data; - this.files = []; - - if (this.data.length < 22) - return { 'status' : false, 'error' : 'Invalid data' }; - var endOfCentralDirectory = this.data.length - 22; - while (endOfCentralDirectory >= 0 && this.getInt(endOfCentralDirectory, 4) != 0x06054b50) - --endOfCentralDirectory; - if (endOfCentralDirectory < 0) - return { 'status' : false, 'error' : 'Invalid data' }; - if (this.getInt(endOfCentralDirectory + 4, 2) != 0 || this.getInt(endOfCentralDirectory + 6, 2) != 0) - return { 'status' : false, 'error' : 'No multidisk support' }; - - var entriesInThisDisk = this.getInt(endOfCentralDirectory + 8, 2); - var centralDirectoryOffset = this.getInt(endOfCentralDirectory + 16, 4); - var globalCommentLength = this.getInt(endOfCentralDirectory + 20, 2); - this.comment = this.data.slice(endOfCentralDirectory + 22, endOfCentralDirectory + 22 + globalCommentLength); - - var fileOffset = centralDirectoryOffset; - - for (var i = 0; i < entriesInThisDisk; ++i) { - if (this.getInt(fileOffset + 0, 4) != 0x02014b50) - return { 'status' : false, 'error' : 'Invalid data' }; - if (this.getInt(fileOffset + 6, 2) > 20) - return { 'status' : false, 'error' : 'Unsupported version' }; - if (this.getInt(fileOffset + 8, 2) & 1) - return { 'status' : false, 'error' : 'Encryption not implemented' }; - - var compressionMethod = this.getInt(fileOffset + 10, 2); - if (compressionMethod != 0 && compressionMethod != 8) - return { 'status' : false, 'error' : 'Unsupported compression method' }; - - var lastModFileTime = this.getInt(fileOffset + 12, 2); - var lastModFileDate = this.getInt(fileOffset + 14, 2); - var lastModifiedDate = this.getDOSDate(lastModFileDate, lastModFileTime); - - var crc = this.getInt(fileOffset + 16, 4); - // TODO: crc - - var compressedSize = this.getInt(fileOffset + 20, 4); - var uncompressedSize = this.getInt(fileOffset + 24, 4); - - var fileNameLength = this.getInt(fileOffset + 28, 2); - var extraFieldLength = this.getInt(fileOffset + 30, 2); - var fileCommentLength = this.getInt(fileOffset + 32, 2); - - var relativeOffsetOfLocalHeader = this.getInt(fileOffset + 42, 4); - - var fileName = this.data.slice(fileOffset + 46, fileOffset + 46 + fileNameLength); - var fileComment = this.data.slice(fileOffset + 46 + fileNameLength + extraFieldLength, fileOffset + 46 + fileNameLength + extraFieldLength + fileCommentLength); - - if (this.getInt(relativeOffsetOfLocalHeader + 0, 4) != 0x04034b50) - return { 'status' : false, 'error' : 'Invalid data' }; - var localFileNameLength = this.getInt(relativeOffsetOfLocalHeader + 26, 2); - var localExtraFieldLength = this.getInt(relativeOffsetOfLocalHeader + 28, 2); - var localFileContent = relativeOffsetOfLocalHeader + 30 + localFileNameLength + localExtraFieldLength; - - this.files[fileName] = - { - 'fileComment' : fileComment, - 'compressionMethod' : compressionMethod, - 'compressedSize' : compressedSize, - 'uncompressedSize' : uncompressedSize, - 'localFileContent' : localFileContent, - 'lastModifiedDate' : lastModifiedDate - }; - - fileOffset += 46 + fileNameLength + extraFieldLength + fileCommentLength; - } - return { 'status' : true } - }; - - - this.read = function(fileName) { - var fileInfo = this.files[fileName]; - if (fileInfo) { - if (fileInfo.compressionMethod == 8) { - if (!tinf) { - tinf = new TINF(); - tinf.init(); - } - var result = tinf.uncompress(this.data, fileInfo.localFileContent); - if (result.status == tinf.OK) - return { 'status' : true, 'data' : result.data }; - else - return { 'status' : false, 'error' : result.error }; - } else { - return { 'status' : true, 'data' : this.data.slice(fileInfo.localFileContent, fileInfo.localFileContent + fileInfo.uncompressedSize) }; - } - } - return { 'status' : false, 'error' : "File '" + fileName + "' doesn't exist in zip" }; - }; - -}; - - - -/* - * tinflate - tiny inflate - * - * Copyright (c) 2003 by Joergen Ibsen / Jibz - * All Rights Reserved - * - * http://www.ibsensoftware.com/ - * - * This software is provided 'as-is', without any express - * or implied warranty. In no event will the authors be - * held liable for any damages arising from the use of - * this software. - * - * Permission is granted to anyone to use this software - * for any purpose, including commercial applications, - * and to alter it and redistribute it freely, subject to - * the following restrictions: - * - * 1. The origin of this software must not be - * misrepresented; you must not claim that you - * wrote the original software. If you use this - * software in a product, an acknowledgment in - * the product documentation would be appreciated - * but is not required. - * - * 2. Altered source versions must be plainly marked - * as such, and must not be misrepresented as - * being the original software. - * - * 3. This notice may not be removed or altered from - * any source distribution. - */ - -/* - * tinflate javascript port by Erik Moller in May 2011. - * emoller@opera.com - * - * read_bits() patched by mike@imidio.com to allow - * reading more then 8 bits (needed in some zlib streams) - */ - -"use strict"; - -function TINF() { - -this.OK = 0; -this.DATA_ERROR = (-3); -this.WINDOW_SIZE = 32768; - -/* ------------------------------ * - * -- internal data structures -- * - * ------------------------------ */ - -this.TREE = function() { - this.table = new Array(16); /* table of code length counts */ - this.trans = new Array(288); /* code -> symbol translation table */ -}; - -this.DATA = function(that) { - this.source = ''; - this.sourceIndex = 0; - this.tag = 0; - this.bitcount = 0; - - this.dest = []; - - this.history = []; - - this.ltree = new that.TREE(); /* dynamic length/symbol tree */ - this.dtree = new that.TREE(); /* dynamic distance tree */ -}; - -/* --------------------------------------------------- * - * -- uninitialized global data (static structures) -- * - * --------------------------------------------------- */ - -this.sltree = new this.TREE(); /* fixed length/symbol tree */ -this.sdtree = new this.TREE(); /* fixed distance tree */ - -/* extra bits and base tables for length codes */ -this.length_bits = new Array(30); -this.length_base = new Array(30); - -/* extra bits and base tables for distance codes */ -this.dist_bits = new Array(30); -this.dist_base = new Array(30); - -/* special ordering of code length codes */ -this.clcidx = [ - 16, 17, 18, 0, 8, 7, 9, 6, - 10, 5, 11, 4, 12, 3, 13, 2, - 14, 1, 15 -]; - -/* ----------------------- * - * -- utility functions -- * - * ----------------------- */ - -/* build extra bits and base tables */ -this.build_bits_base = function(bits, base, delta, first) -{ - var i, sum; - - /* build bits table */ - for (i = 0; i < delta; ++i) bits[i] = 0; - for (i = 0; i < 30 - delta; ++i) bits[i + delta] = Math.floor(i / delta); - - /* build base table */ - for (sum = first, i = 0; i < 30; ++i) - { - base[i] = sum; - sum += 1 << bits[i]; - } -} - -/* build the fixed huffman trees */ -this.build_fixed_trees = function(lt, dt) -{ - var i; - - /* build fixed length tree */ - for (i = 0; i < 7; ++i) lt.table[i] = 0; - - lt.table[7] = 24; - lt.table[8] = 152; - lt.table[9] = 112; - - for (i = 0; i < 24; ++i) lt.trans[i] = 256 + i; - for (i = 0; i < 144; ++i) lt.trans[24 + i] = i; - for (i = 0; i < 8; ++i) lt.trans[24 + 144 + i] = 280 + i; - for (i = 0; i < 112; ++i) lt.trans[24 + 144 + 8 + i] = 144 + i; - - /* build fixed distance tree */ - for (i = 0; i < 5; ++i) dt.table[i] = 0; - - dt.table[5] = 32; - - for (i = 0; i < 32; ++i) dt.trans[i] = i; -} - -/* given an array of code lengths, build a tree */ -this.build_tree = function(t, lengths, loffset, num) -{ - var offs = new Array(16); - var i, sum; - - /* clear code length count table */ - for (i = 0; i < 16; ++i) t.table[i] = 0; - - /* scan symbol lengths, and sum code length counts */ - for (i = 0; i < num; ++i) t.table[lengths[loffset + i]]++; - - t.table[0] = 0; - - /* compute offset table for distribution sort */ - for (sum = 0, i = 0; i < 16; ++i) - { - offs[i] = sum; - sum += t.table[i]; - } - - /* create code->symbol translation table (symbols sorted by code) */ - for (i = 0; i < num; ++i) - { - if (lengths[loffset + i]) t.trans[offs[lengths[loffset + i]]++] = i; - } -} - -/* ---------------------- * - * -- decode functions -- * - * ---------------------- */ - -/* get one bit from source stream */ -this.getbit = function(d) -{ - var bit; - - /* check if tag is empty */ - if (!d.bitcount--) - { - /* load next tag */ - d.tag = d.source[d.sourceIndex++] & 0xff; - d.bitcount = 7; - } - - /* shift bit out of tag */ - bit = d.tag & 0x01; - d.tag >>= 1; - - return bit; -} - -/* read a num bit value from a stream and add base */ -function read_bits_direct(source, bitcount, tag, idx, num) -{ - var val = 0; - while (bitcount < 24) { - tag = tag | (source[idx++] & 0xff) << bitcount; - bitcount += 8; - } - val = tag & (0xffff >> (16 - num)); - tag >>= num; - bitcount -= num; - return [bitcount, tag, idx, val]; -} -this.read_bits = function(d, num, base) -{ - if (!num) - return base; - - var ret = read_bits_direct(d.source, d.bitcount, d.tag, d.sourceIndex, num); - d.bitcount = ret[0]; - d.tag = ret[1]; - d.sourceIndex = ret[2]; - return ret[3] + base; -} - -/* given a data stream and a tree, decode a symbol */ -this.decode_symbol = function(d, t) -{ - while (d.bitcount < 16) { - d.tag = d.tag | (d.source[d.sourceIndex++] & 0xff) << d.bitcount; - d.bitcount += 8; - } - - var sum = 0, cur = 0, len = 0; - do { - cur = 2 * cur + ((d.tag & (1 << len)) >> len); - - ++len; - - sum += t.table[len]; - cur -= t.table[len]; - - } while (cur >= 0); - - d.tag >>= len; - d.bitcount -= len; - - return t.trans[sum + cur]; -} - -/* given a data stream, decode dynamic trees from it */ -this.decode_trees = function(d, lt, dt) -{ - var code_tree = new this.TREE(); - var lengths = new Array(288+32); - var hlit, hdist, hclen; - var i, num, length; - - /* get 5 bits HLIT (257-286) */ - hlit = this.read_bits(d, 5, 257); - - /* get 5 bits HDIST (1-32) */ - hdist = this.read_bits(d, 5, 1); - - /* get 4 bits HCLEN (4-19) */ - hclen = this.read_bits(d, 4, 4); - - for (i = 0; i < 19; ++i) lengths[i] = 0; - - /* read code lengths for code length alphabet */ - for (i = 0; i < hclen; ++i) - { - /* get 3 bits code length (0-7) */ - var clen = this.read_bits(d, 3, 0); - - lengths[this.clcidx[i]] = clen; - } - - /* build code length tree */ - this.build_tree(code_tree, lengths, 0, 19); - - /* decode code lengths for the dynamic trees */ - for (num = 0; num < hlit + hdist; ) - { - var sym = this.decode_symbol(d, code_tree); - - switch (sym) - { - case 16: - /* copy previous code length 3-6 times (read 2 bits) */ - { - var prev = lengths[num - 1]; - for (length = this.read_bits(d, 2, 3); length; --length) - { - lengths[num++] = prev; - } - } - break; - case 17: - /* repeat code length 0 for 3-10 times (read 3 bits) */ - for (length = this.read_bits(d, 3, 3); length; --length) - { - lengths[num++] = 0; - } - break; - case 18: - /* repeat code length 0 for 11-138 times (read 7 bits) */ - for (length = this.read_bits(d, 7, 11); length; --length) - { - lengths[num++] = 0; - } - break; - default: - /* values 0-15 represent the actual code lengths */ - lengths[num++] = sym; - break; - } - } - - /* build dynamic trees */ - this.build_tree(lt, lengths, 0, hlit); - this.build_tree(dt, lengths, hlit, hdist); -} - -/* ----------------------------- * - * -- block inflate functions -- * - * ----------------------------- */ - -/* given a stream and two trees, inflate a block of data */ -this.inflate_block_data = function(d, lt, dt) -{ - // js optimization. - var ddest = d.dest; - var ddestlength = ddest.length; - - while (1) - { - var sym = this.decode_symbol(d, lt); - - /* check for end of block */ - if (sym == 256) - { - return this.OK; - } - - if (sym < 256) - { - ddest[ddestlength++] = sym; // ? String.fromCharCode(sym); - d.history.push(sym); - } else { - - var length, dist, offs; - var i; - - sym -= 257; - - /* possibly get more bits from length code */ - length = this.read_bits(d, this.length_bits[sym], this.length_base[sym]); - - dist = this.decode_symbol(d, dt); - - /* possibly get more bits from distance code */ - offs = d.history.length - this.read_bits(d, this.dist_bits[dist], this.dist_base[dist]); - - if (offs < 0) - throw ("Invalid zlib offset " + offs); - - /* copy match */ - for (i = offs; i < offs + length; ++i) { - //ddest[ddestlength++] = ddest[i]; - ddest[ddestlength++] = d.history[i]; - d.history.push(d.history[i]); - } - } - } -} - -/* inflate an uncompressed block of data */ -this.inflate_uncompressed_block = function(d) -{ - var length, invlength; - var i; - - if (d.bitcount > 7) { - var overflow = Math.floor(d.bitcount / 8); - d.sourceIndex -= overflow; - d.bitcount = 0; - d.tag = 0; - } - - /* get length */ - length = d.source[d.sourceIndex+1]; - length = 256*length + d.source[d.sourceIndex]; - - /* get one's complement of length */ - invlength = d.source[d.sourceIndex+3]; - invlength = 256*invlength + d.source[d.sourceIndex+2]; - - /* check length */ - if (length != (~invlength & 0x0000ffff)) return this.DATA_ERROR; - - d.sourceIndex += 4; - - /* copy block */ - for (i = length; i; --i) { - d.history.push(d.source[d.sourceIndex]); - d.dest[d.dest.length] = d.source[d.sourceIndex++]; - } - - /* make sure we start next block on a byte boundary */ - d.bitcount = 0; - - return this.OK; -} - -/* inflate a block of data compressed with fixed huffman trees */ -this.inflate_fixed_block = function(d) -{ - /* decode block using fixed trees */ - return this.inflate_block_data(d, this.sltree, this.sdtree); -} - -/* inflate a block of data compressed with dynamic huffman trees */ -this.inflate_dynamic_block = function(d) -{ - /* decode trees from stream */ - this.decode_trees(d, d.ltree, d.dtree); - - /* decode block using decoded trees */ - return this.inflate_block_data(d, d.ltree, d.dtree); -} - -/* ---------------------- * - * -- public functions -- * - * ---------------------- */ - -/* initialize global (static) data */ -this.init = function() -{ - /* build fixed huffman trees */ - this.build_fixed_trees(this.sltree, this.sdtree); - - /* build extra bits and base tables */ - this.build_bits_base(this.length_bits, this.length_base, 4, 3); - this.build_bits_base(this.dist_bits, this.dist_base, 2, 1); - - /* fix a special case */ - this.length_bits[28] = 0; - this.length_base[28] = 258; - - this.reset(); -} - -this.reset = function() -{ - this.d = new this.DATA(this); - delete this.header; -} - -/* inflate stream from source to dest */ -this.uncompress = function(source, offset) -{ - - var d = this.d; - var bfinal; - - /* initialise data */ - d.source = source; - d.sourceIndex = offset; - d.bitcount = 0; - - d.dest = []; - - // Skip zlib header at start of stream - if (typeof this.header == 'undefined') { - this.header = this.read_bits(d, 16, 0); - /* byte 0: 0x78, 7 = 32k window size, 8 = deflate */ - /* byte 1: check bits for header and other flags */ - } - - var blocks = 0; - - do { - - var btype; - var res; - - /* read final block flag */ - bfinal = this.getbit(d); - - /* read block type (2 bits) */ - btype = this.read_bits(d, 2, 0); - - /* decompress block */ - switch (btype) - { - case 0: - /* decompress uncompressed block */ - res = this.inflate_uncompressed_block(d); - break; - case 1: - /* decompress block with fixed huffman trees */ - res = this.inflate_fixed_block(d); - break; - case 2: - /* decompress block with dynamic huffman trees */ - res = this.inflate_dynamic_block(d); - break; - default: - return { 'status' : this.DATA_ERROR }; - } - - if (res != this.OK) return { 'status' : this.DATA_ERROR }; - blocks++; - - } while (!bfinal && d.sourceIndex < d.source.length); - - d.history = d.history.slice(-this.WINDOW_SIZE); - - return { 'status' : this.OK, 'data' : d.dest }; -} - -}; diff --git a/include/rfb.js b/include/rfb.js index dfd9269c..0aa320d3 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -374,8 +374,6 @@ var RFB; } for (i = 0; i < 4; i++) { - //this._FBU.zlibs[i] = new TINF(); - //this._FBU.zlibs[i].init(); this._FBU.zlibs[i] = new inflator.Inflate(); } }, diff --git a/tests/vnc_perf.html b/tests/vnc_perf.html index c439e955..51295789 100644 --- a/tests/vnc_perf.html +++ b/tests/vnc_perf.html @@ -48,7 +48,7 @@ // Load supporting scripts Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", "keysymdef.js", "keyboard.js", "input.js", "display.js", - "jsunzip.js", "rfb.js"]); + "rfb.js"]); var start_time, VNC_frame_data, pass, passes, encIdx, encOrder = ['raw', 'rre', 'hextile', 'tightpng', 'copyrect'], From 464944844f039eea05fa19b0d1747d7ac28c4ac3 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 6 Jan 2016 11:18:47 -0500 Subject: [PATCH 113/527] Remove unused variables in the TIGHT server init This commit removes some unused variables in the TIGHT server init section of the server init handler, replacing them with documenting comments and calls to rQskipBytes. Fixes #440. --- include/rfb.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 0aa320d3..5d98dbd2 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -920,18 +920,17 @@ var RFB; var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } - var i; - for (i = 0; i < numServerMessages; i++) { - var srvMsg = this._sock.rQshiftStr(16); - } + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. - for (i = 0; i < numClientMessages; i++) { - var clientMsg = this._sock.rQshiftStr(16); - } + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); - for (i = 0; i < numEncodings; i++) { - var encoding = this._sock.rQshiftStr(16); - } + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); + + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); } // NB(directxman12): these are down here so that we don't run them multiple times From 4ee55e01266769ea0711f74464162ba190a7e3a9 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 6 Jan 2016 13:26:26 -0500 Subject: [PATCH 114/527] Fixed vnc_perf.html This commit updates vnc_perf.html so that it works with the current version of noVNC. It also introduces a utility to convert noVNC session recordings recorded in base64-mode to binary-mode recordings, since noVNC no longer supports base64-mode. Fixes #479. --- tests/vnc_perf.html | 24 ++++++++++++++---------- tests/vnc_playback.html | 2 +- utils/b64-to-binary.pl | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100755 utils/b64-to-binary.pl diff --git a/tests/vnc_perf.html b/tests/vnc_perf.html index 51295789..9acea882 100644 --- a/tests/vnc_perf.html +++ b/tests/vnc_perf.html @@ -38,17 +38,23 @@ - - + diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index f36f1e65..cfc5953b 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -40,7 +40,7 @@ diff --git a/utils/b64-to-binary.pl b/utils/b64-to-binary.pl new file mode 100755 index 00000000..280e28c9 --- /dev/null +++ b/utils/b64-to-binary.pl @@ -0,0 +1,17 @@ +#!/usr/bin/env perl +use MIME::Base64; + +for (<>) { + unless (/^'([{}])(\d+)\1(.+?)',$/) { + print; + next; + } + + my ($dir, $amt, $b64) = ($1, $2, $3); + + my $decoded = MIME::Base64::decode($b64) or die "Could not base64-decode line `$_`"; + + my $decoded_escaped = join "", map { "\\x$_" } unpack("(H2)*", $decoded); + + print "'${dir}${amt}${dir}${decoded_escaped}',\n"; +} From c55f05f61936ac978a8b94cf8d5f8011a217c822 Mon Sep 17 00:00:00 2001 From: Miguel Xavier Penha Neto Date: Thu, 1 Oct 2015 17:26:44 -0300 Subject: [PATCH 115/527] Pass token into the path variable If a token is already present in the path, the new variable is ignored. In order to properly manipulate the path, a new method, `WebUtil.injectParamIfMissing` was introduced. Fixes #536 [@directxman12: fix up path manipulation logic] --- include/ui.js | 9 +++++++++ include/webutil.js | 24 ++++++++++++++++++++++++ vnc.html | 1 + vnc_auto.html | 10 +++++++--- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/include/ui.js b/include/ui.js index 38fbe4bd..1a7f803a 100644 --- a/include/ui.js +++ b/include/ui.js @@ -96,6 +96,7 @@ var UI; UI.initSetting('view_only', false); UI.initSetting('path', 'websockify'); UI.initSetting('repeaterID', ''); + UI.initSetting('token', ''); var autoconnect = WebUtil.getConfigVar('autoconnect', false); if (autoconnect === 'true' || autoconnect == '1') { @@ -519,6 +520,7 @@ var UI; UI.connSettingsOpen = false; UI.saveSetting('host'); UI.saveSetting('port'); + UI.saveSetting('token'); //UI.saveSetting('password'); } else { $D('noVNC_controls').style.display = "block"; @@ -810,7 +812,14 @@ var UI; var host = $D('noVNC_host').value; var port = $D('noVNC_port').value; var password = $D('noVNC_password').value; + var token = $D('noVNC_token').value; var path = $D('noVNC_path').value; + + //if token is in path then ignore the new token variable + if (token) { + path = WebUtil.injectParamIfMissing(path, "token", token); + } + if ((!host) || (!port)) { throw new Error("Must set host and port"); } diff --git a/include/webutil.js b/include/webutil.js index f10aa0d7..4289aa6b 100644 --- a/include/webutil.js +++ b/include/webutil.js @@ -260,3 +260,27 @@ WebUtil.selectStylesheet = function (sheet) { } return sheet; }; + +WebUtil.injectParamIfMissing = function (path, param, value) { + // force pretend that we're dealing with a relative path + // (assume that we wanted an extra if we pass one in) + path = "/" + path; + + var elem = document.createElement('a'); + elem.href = path; + + var param_eq = encodeURIComponent(param) + "="; + var query; + if (elem.search) { + query = elem.search.slice(1).split('&'); + } else { + query = []; + } + + if (!query.some(function (v) { return v.startsWith(param_eq); })) { + query.push(param_eq + encodeURIComponent(value)); + elem.search = "?" + query.join("&"); + } + + return elem.pathname.slice(1) + elem.search + elem.hash; +}; diff --git a/vnc.html b/vnc.html index e2250f5d..f64c750c 100644 --- a/vnc.html +++ b/vnc.html @@ -199,6 +199,7 @@
  • +
  • diff --git a/vnc_auto.html b/vnc_auto.html index 04803223..73174713 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -202,16 +202,20 @@ } } + password = WebUtil.getConfigVar('password', ''); + path = WebUtil.getConfigVar('path', 'websockify'); + // If a token variable is passed in, set the parameter in a cookie. // This is used by nova-novncproxy. token = WebUtil.getConfigVar('token', null); if (token) { + + // if token is already present in the path we should use it + path = WebUtil.injectParamIfMissing(path, "token", token); + WebUtil.createCookie('token', token, 1) } - password = WebUtil.getConfigVar('password', ''); - path = WebUtil.getConfigVar('path', 'websockify'); - if ((!host) || (!port)) { updateState(null, 'fatal', null, 'Must specify host and port in URL'); return; From a4ffd2d49ae7c4d364a43b2a4e6161efdc3f84a2 Mon Sep 17 00:00:00 2001 From: "Tyler G. Hicks-Wright" Date: Fri, 22 Jan 2016 17:16:01 -0700 Subject: [PATCH 116/527] Map Util.[Debug|Info|Warn|Error] to console.[debug|info|warn|error]. This change will preserve file and line number reporting when Util.[Debug|Info|Warn|Error] is called, which should make development and debugging a lot easier. --- include/util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/util.js b/include/util.js index ed0e3cde..b3ee36ad 100644 --- a/include/util.js +++ b/include/util.js @@ -211,13 +211,13 @@ Util.init_logging = function (level) { /* jshint -W086 */ switch (level) { case 'debug': - Util.Debug = function (msg) { console.log(msg); }; + Util.Debug = console.debug.bind(window.console); case 'info': - Util.Info = function (msg) { console.log(msg); }; + Util.Info = console.info.bind(window.console); case 'warn': - Util.Warn = function (msg) { console.warn(msg); }; + Util.Warn = console.warn.bind(window.console); case 'error': - Util.Error = function (msg) { console.error(msg); }; + Util.Error = console.error.bind(window.console); case 'none': break; default: From 7bc383e8b6f6da10cb3f335123fe561674a6723d Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 24 Feb 2016 15:59:58 -0500 Subject: [PATCH 117/527] Make sure to copy arrays when using render queue This commit ensures that input arrays are copied to new storage when they are pushed onto the render queue. This ensures that they are not overwritten before they are eventually used. Fixes #571 --- include/display.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/include/display.js b/include/display.js index 6bf89bd6..a492817d 100644 --- a/include/display.js +++ b/include/display.js @@ -471,9 +471,14 @@ var Display; blitImage: function (x, y, width, height, arr, offset, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); this.renderQ_push({ 'type': 'blit', - 'data': arr, + 'data': new_arr, 'x': x, 'y': y, 'width': width, @@ -488,9 +493,14 @@ var Display; blitRgbImage: function (x, y , width, height, arr, offset, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + var new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); this.renderQ_push({ 'type': 'blitRgb', - 'data': arr, + 'data': new_arr, 'x': x, 'y': y, 'width': width, @@ -506,7 +516,7 @@ var Display; blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) { if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, but it + // NB(directxman12): it's technically more performant here to use preallocated arrays, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // this probably isn't getting called *nearly* as much var new_arr = new Uint8Array(width * height * 4); From c77d9fcc4e752047388a404d7eeed6db6af39b2a Mon Sep 17 00:00:00 2001 From: Ward Fisher Date: Mon, 11 Apr 2016 14:32:14 -0600 Subject: [PATCH 118/527] Added an '--ssl-only' option to noVNC launch script. This will pass through to Websockify, allowing a user to specify that only ssl-secured connections will be allowed. --- utils/launch.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/utils/launch.sh b/utils/launch.sh index ecee06cd..bbce453e 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -5,7 +5,7 @@ usage() { echo "$*" echo fi - echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT]" + echo "Usage: ${NAME} [--listen PORT] [--vnc VNC_HOST:PORT] [--cert CERT] [--ssl-only]" echo echo "Starts the WebSockets proxy and a mini-webserver and " echo "provides a cut-and-paste URL to go to." @@ -18,6 +18,8 @@ usage() { echo " Default: self.pem" echo " --web WEB Path to web files (e.g. vnc.html)" echo " Default: ./" + echo " --ssl-only Disable non-https connections." + echo " " exit 2 } @@ -29,6 +31,7 @@ VNC_DEST="localhost:5900" CERT="" WEB="" proxy_pid="" +SSLONLY="" die() { echo "$*" @@ -55,6 +58,7 @@ while [ "$*" ]; do --vnc) VNC_DEST="${OPTARG}"; shift ;; --cert) CERT="${OPTARG}"; shift ;; --web) WEB="${OPTARG}"; shift ;; + --ssl-only) SSLONLY="--ssl-only" ;; -h|--help) usage ;; -*) usage "Unknown chrooter option: ${param}" ;; *) break ;; @@ -134,7 +138,7 @@ fi echo "Starting webserver and WebSockets proxy on port ${PORT}" #${HERE}/websockify --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & -${WEBSOCKIFY} --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & +${WEBSOCKIFY} ${SSLONLY} --web ${WEB} ${CERT:+--cert ${CERT}} ${PORT} ${VNC_DEST} & proxy_pid="$!" sleep 1 if ! ps -p ${proxy_pid} >/dev/null; then From 27a1f6cb9505171fef9f604941bf54a2e1f0abcb Mon Sep 17 00:00:00 2001 From: Ward Fisher Date: Mon, 11 Apr 2016 14:42:42 -0600 Subject: [PATCH 119/527] Tweaked message printed based on whether or not http is available. --- utils/launch.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils/launch.sh b/utils/launch.sh index bbce453e..db610662 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -148,7 +148,12 @@ if ! ps -p ${proxy_pid} >/dev/null; then fi echo -e "\n\nNavigate to this URL:\n" -echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +if [ "x$SSLONLY" == "x" ]; then + echo -e " http://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +else + echo -e " https://$(hostname):${PORT}/vnc.html?host=$(hostname)&port=${PORT}\n" +fi + echo -e "Press Ctrl-C to exit\n\n" wait ${proxy_pid} From c8294760b12fe5231e0c56b8ed1dc88d29668d57 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 11 Apr 2016 13:01:28 -0500 Subject: [PATCH 120/527] Handle missing leading slash in elem.pathname IE11 with compat mode turned off has been observed displaying this behavior. This commit checks for and corrects this broken behavior. Fixes #591 --- include/webutil.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/webutil.js b/include/webutil.js index 4289aa6b..abb180a8 100644 --- a/include/webutil.js +++ b/include/webutil.js @@ -282,5 +282,10 @@ WebUtil.injectParamIfMissing = function (path, param, value) { elem.search = "?" + query.join("&"); } - return elem.pathname.slice(1) + elem.search + elem.hash; + // some browsers (e.g. IE11) may occasionally omit the leading slash + // in the elem.pathname string. Handle that case gracefully. + if (elem.pathname.charAt(0) == "/") { + return elem.pathname.slice(1) + elem.search + elem.hash; + } else { + return elem.pathname + elem.search + elem.hash; }; From 3c9558444cf2fe76175b47a27cb9cbae3d6900f7 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Wed, 13 Apr 2016 12:53:37 -0500 Subject: [PATCH 121/527] Adds missed line from PR 597 --- include/webutil.js | 1 + 1 file changed, 1 insertion(+) diff --git a/include/webutil.js b/include/webutil.js index abb180a8..9ee34730 100644 --- a/include/webutil.js +++ b/include/webutil.js @@ -288,4 +288,5 @@ WebUtil.injectParamIfMissing = function (path, param, value) { return elem.pathname.slice(1) + elem.search + elem.hash; } else { return elem.pathname + elem.search + elem.hash; + } }; From 529c64e1036b622ff971b1a0cd13856837d3a9eb Mon Sep 17 00:00:00 2001 From: samhed Date: Mon, 25 Apr 2016 16:35:49 +0200 Subject: [PATCH 122/527] Re-arrange global UI variables --- include/ui.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/include/ui.js b/include/ui.js index 1a7f803a..425e5a3b 100644 --- a/include/ui.js +++ b/include/ui.js @@ -24,20 +24,25 @@ var UI; UI = { rfb_state: 'loaded', + resizeTimeout: null, + popupStatusTimeout: null, + hideKeyboardTimeout: null, + settingsOpen: false, connSettingsOpen: false, - popupStatusTimeout: null, clipboardOpen: false, keyboardVisible: false, - hideKeyboardTimeout: null, - lastKeyboardinput: null, - defaultKeyboardinputLen: 100, - extraKeysVisible: false, - ctrlOn: false, - altOn: false, + isTouchDevice: false, rememberedClipSetting: null, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, + + shiftDown: false, + ctrlDown: false, + altDown: false, + altGrDown: false, // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus From f620259bc59dc18e91653918cdb513626a3af8dd Mon Sep 17 00:00:00 2001 From: samhed Date: Mon, 25 Apr 2016 16:38:23 +0200 Subject: [PATCH 123/527] Force clipping mode in Safari on all platforms --- include/ui.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/include/ui.js b/include/ui.js index 425e5a3b..cfdedb3a 100644 --- a/include/ui.js +++ b/include/ui.js @@ -35,6 +35,7 @@ var UI; keyboardVisible: false, isTouchDevice: false, + isSafari: false, rememberedClipSetting: null, lastKeyboardinput: null, defaultKeyboardinputLen: 100, @@ -137,12 +138,12 @@ var UI; UI.setBarPosition(); } ); - var isSafari = (navigator.userAgent.indexOf('Safari') != -1 && - navigator.userAgent.indexOf('Chrome') == -1); + UI.isSafari = (navigator.userAgent.indexOf('Safari') != -1 && + navigator.userAgent.indexOf('Chrome') == -1); // Only show the button if fullscreen is properly supported // * Safari doesn't support alphanumerical input while in fullscreen - if (!isSafari && + if (!UI.isSafari && (document.documentElement.requestFullscreen || document.documentElement.mozRequestFullScreen || document.documentElement.webkitRequestFullscreen || @@ -942,7 +943,12 @@ var UI; var resizeElem = $D('noVNC_resize'); var connected = UI.rfb && UI.rfb_state === 'normal'; - if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { + if (UI.isSafari) { + // Safari auto-hides the scrollbars which makes them + // impossible to use in most cases + UI.setViewClip(true); + $D('noVNC_clip').disabled = true; + } else if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { // Disable clipping if we are scaling UI.setViewClip(false); $D('noVNC_clip').disabled = true; From 27e77d468f041e91c717be37d48242d392e2a81a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 28 Apr 2016 06:46:40 -0500 Subject: [PATCH 124/527] Adds tap-to-click in viewport drag mode (#600) --- include/rfb.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/rfb.js b/include/rfb.js index 5d98dbd2..e409217a 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -119,6 +119,7 @@ var RFB; this._mouse_arr = []; this._viewportDragging = false; this._viewportDragPos = {}; + this._viewportHasMoved = false; // set the default value on user-facing properties Util.set_defaults(this, defaults, { @@ -593,6 +594,13 @@ var RFB; return; } else { this._viewportDragging = false; + + // If the viewport didn't actually move, then treat as a mouse click event + // Send the button down event here, as the button up event is sent at the end of this function + if (!this._viewportHasMoved && !this._view_only) { + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); + } + this._viewportHasMoved = false; } } @@ -608,6 +616,11 @@ var RFB; var deltaY = this._viewportDragPos.y - y; this._viewportDragPos = {'x': x, 'y': y}; + // if there is actually viewport move, set the HasMoved flag to true + if (deltaX != 0 || deltaY != 0) { + this._viewportHasMoved = true; + } + this._display.viewportChangePos(deltaX, deltaY); // Skip sending mouse events From 32df3fdbe1b289b06344902b72537d6dc3671afb Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 28 Apr 2016 17:41:48 +0200 Subject: [PATCH 125/527] Add a threshold for viewport dragging (#600) --- include/rfb.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index e409217a..3e9344bb 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -614,14 +614,18 @@ var RFB; if (this._viewportDragging) { var deltaX = this._viewportDragPos.x - x; var deltaY = this._viewportDragPos.y - y; - this._viewportDragPos = {'x': x, 'y': y}; - // if there is actually viewport move, set the HasMoved flag to true - if (deltaX != 0 || deltaY != 0) { - this._viewportHasMoved = true; - } + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var dragThreshold = 10 * window.devicePixelRatio; - this._display.viewportChangePos(deltaX, deltaY); + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || + Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = {'x': x, 'y': y}; + this._display.viewportChangePos(deltaX, deltaY); + } // Skip sending mouse events return; From f6a29ddeeb5557d8f20f998038a065d04509fd9d Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 30 Jul 2015 10:13:35 -0400 Subject: [PATCH 126/527] Fix occasional error with sending while readyState !== 1 Under certain conditions, noVNC would attempt to flush the web socket while it was disconnected, before the disconnected state was picked up. This casues noVNC to crash ungracefully and the parent window is not notified - leading to no chance at recovery without a page refresh. --- include/websock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/websock.js b/include/websock.js index f3336a0a..4d7a4f9a 100644 --- a/include/websock.js +++ b/include/websock.js @@ -197,7 +197,7 @@ function Websock() { } if (this._websocket.bufferedAmount < this.maxBufferedAmount) { - if (this._sQlen > 0) { + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { this._websocket.send(this._encode_message()); this._sQlen = 0; } From cf0623fffa6db5a27782998a610690f9b27605bc Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Thu, 30 Jul 2015 10:32:25 -0400 Subject: [PATCH 127/527] Fix failing test It only makes sense to send data on a websocket if the readyState is equal to 1. --- tests/test.websock.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test.websock.js b/tests/test.websock.js index 953a5268..f708e04b 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -173,6 +173,7 @@ describe('Websock', function() { it('should actually send on the websocket if the websocket does not have too much buffered', function () { sock.maxBufferedAmount = 10; sock._websocket.bufferedAmount = 8; + sock._websocket.readyState = WebSocket.OPEN sock._sQ = new Uint8Array([1, 2, 3]); sock._sQlen = 3; var encoded = sock._encode_message(); From 12ae8b3d504c2bb263274aecb0c91d75a76d2e71 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 30 Apr 2016 04:07:33 +0200 Subject: [PATCH 128/527] Respect the threshold in the viewdrag test (#600) --- tests/test.rfb.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 961d9eb5..a49335ea 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1803,15 +1803,17 @@ describe('Remote Frame Buffer Protocol Client', function() { it('if enabled, viewportDragging should occur on mouse movement while a button is down', function () { client._viewportDrag = true; client._viewportDragging = true; - client._viewportDragPos = { x: 13, y: 9 }; + client._viewportHasMoved = false; + client._viewportDragPos = { x: 23, y: 9 }; client._display.viewportChangePos = sinon.spy(); client._mouse._onMouseMove(10, 4); expect(client._viewportDragging).to.be.true; + expect(client._viewportHasMoved.to.be.true; expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 }); expect(client._display.viewportChangePos).to.have.been.calledOnce; - expect(client._display.viewportChangePos).to.have.been.calledWith(3, 5); + expect(client._display.viewportChangePos).to.have.been.calledWith(13, 5); }); }); From 057cfc7cb4dae8c0efaf31c23e6500d903842c22 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 30 Apr 2016 04:26:30 +0200 Subject: [PATCH 129/527] Add missing parenthesis (#600) --- tests/test.rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index a49335ea..1b73986f 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1810,7 +1810,7 @@ describe('Remote Frame Buffer Protocol Client', function() { client._mouse._onMouseMove(10, 4); expect(client._viewportDragging).to.be.true; - expect(client._viewportHasMoved.to.be.true; + expect(client._viewportHasMoved).to.be.true; expect(client._viewportDragPos).to.deep.equal({ x: 10, y: 4 }); expect(client._display.viewportChangePos).to.have.been.calledOnce; expect(client._display.viewportChangePos).to.have.been.calledWith(13, 5); From f52105bc88ebd18d5cb3fba817173e99600cdc3f Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 12 May 2016 16:43:19 +0200 Subject: [PATCH 130/527] Add fallback value for devicePixelRatio In IE 10 for example, devicePixelRatio doesn't exist which caused the code to fail by setting the thresholds to zero. --- include/input.js | 5 +++-- include/rfb.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/input.js b/include/input.js index 5d9e209e..fa6ba44a 100644 --- a/include/input.js +++ b/include/input.js @@ -212,7 +212,7 @@ var Keyboard, Mouse; // Touch device // When two touches occur within 500 ms of each other and are - // closer than 20 pixels together a double click is triggered. + // close enough together a double click is triggered. if (down == 1) { if (this._doubleClickTimer === null) { this._lastTouchPos = pos; @@ -229,7 +229,8 @@ var Keyboard, Mouse; // The goal is to trigger on a certain physical width, the // devicePixelRatio brings us a bit closer but is not optimal. - if (d < 20 * window.devicePixelRatio) { + var threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { pos = this._lastTouchPos; } } diff --git a/include/rfb.js b/include/rfb.js index 3e9344bb..48fa5a8c 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -617,7 +617,7 @@ var RFB; // The goal is to trigger on a certain physical width, the // devicePixelRatio brings us a bit closer but is not optimal. - var dragThreshold = 10 * window.devicePixelRatio; + var dragThreshold = 10 * (window.devicePixelRatio || 1); if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold)) { From 553864e85813f886b9c60b4825c8d4b714cae695 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 17:27:17 +0200 Subject: [PATCH 131/527] Switch names between the container and the screen The noVNC_container now contains the logo and the screen. While the noVNC_screen in turn contains the canvas. --- include/base.css | 9 +++++---- include/ui.js | 28 +++++++++++++++------------- vnc.html | 4 ++-- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/include/base.css b/include/base.css index 2769357e..af284717 100644 --- a/include/base.css +++ b/include/base.css @@ -113,9 +113,7 @@ html { float:right; } -/* Do not set width/height for VNC_screen or VNC_canvas or incorrect - * scaling will occur. Canvas resizes to remote VNC settings */ -#noVNC_screen { +#noVNC_container { display: table; width:100%; height:100%; @@ -124,7 +122,7 @@ html { /*border-top-left-radius: 800px 600px;*/ } -#noVNC_container { +#noVNC_screen { display: none; position: absolute; margin: 0px; @@ -137,6 +135,9 @@ html { height: auto; } +/* Do not set width/height for VNC_canvas or incorrect + * scaling will occur. Canvas size depends on remote VNC + * settings and noVNC settings. */ #noVNC_canvas { position: absolute; left: 0; diff --git a/include/ui.js b/include/ui.js index cfdedb3a..066a35a7 100644 --- a/include/ui.js +++ b/include/ui.js @@ -248,7 +248,7 @@ var UI; onresize: function (callback) { if (!UI.rfb) return; - var size = UI.getCanvasLimit(); + var size = UI.screenSize(); if (size && UI.rfb_state === 'normal' && UI.rfb.get_display()) { var display = UI.rfb.get_display(); @@ -278,17 +278,19 @@ var UI; } }, - getCanvasLimit: function () { - var container = $D('noVNC_container'); + // The screen is always the same size as the available + // viewport minus the height of the control bar + screenSize: function () { + var screen = $D('noVNC_screen'); // Hide the scrollbars until the size is calculated - container.style.overflow = "hidden"; + screen.style.overflow = "hidden"; - var pos = Util.getPosition(container); + var pos = Util.getPosition(screen); var w = pos.width; var h = pos.height; - container.style.overflow = "visible"; + screen.style.overflow = "visible"; if (isNaN(w) || isNaN(h)) { return false; @@ -687,7 +689,7 @@ var UI; break; case 'disconnected': $D('noVNC_logo').style.display = "block"; - $D('noVNC_container').style.display = "none"; + $D('noVNC_screen').style.display = "none"; /* falls through */ case 'loaded': klass = "noVNC_status_normal"; @@ -844,7 +846,7 @@ var UI; //Close dialog. setTimeout(UI.setBarPosition, 100); $D('noVNC_logo').style.display = "none"; - $D('noVNC_container').style.display = "inline"; + $D('noVNC_screen').style.display = "inline"; }, disconnect: function() { @@ -855,7 +857,7 @@ var UI; UI.rfb.set_onFBUComplete(UI.FBUComplete); $D('noVNC_logo').style.display = "block"; - $D('noVNC_container').style.display = "none"; + $D('noVNC_screen').style.display = "none"; // Don't display the connection settings until we're actually disconnected }, @@ -919,19 +921,19 @@ var UI; // If clipping, update clipping settings display.set_viewport(true); - var size = UI.getCanvasLimit(); + var size = UI.screenSize(); if (size) { display.set_maxWidth(size.w); display.set_maxHeight(size.h); // Hide potential scrollbars that can skew the position - $D('noVNC_container').style.overflow = "hidden"; + $D('noVNC_screen').style.overflow = "hidden"; // The x position marks the left margin of the canvas, // remove the margin from both sides to keep it centered var new_w = size.w - (2 * Util.getPosition($D('noVNC_canvas')).x); - $D('noVNC_container').style.overflow = "visible"; + $D('noVNC_screen').style.overflow = "visible"; display.viewportChangeSize(new_w, size.h); } @@ -1218,7 +1220,7 @@ var UI; $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; - var vncwidth = $D('noVNC_screen').style.offsetWidth; + var vncwidth = $D('noVNC_container').style.offsetWidth; $D('noVNC-control-bar').style.width = vncwidth + 'px'; } diff --git a/vnc.html b/vnc.html index f64c750c..c0f242c0 100644 --- a/vnc.html +++ b/vnc.html @@ -207,11 +207,11 @@
    -
    +

    no
    VNC

    -
    +
    Canvas not supported. From 777cb7a0c573e16656a30b538e8d0e280cc69975 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 18:01:15 +0200 Subject: [PATCH 132/527] Cleanup for the resize related functions Renamed functions, added clarifying comments and moved the resize related functions closer to the other viewport functions. --- include/rfb.js | 2 + include/ui.js | 147 ++++++++++++++++++++++++++----------------------- 2 files changed, 81 insertions(+), 68 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 48fa5a8c..9e4525b5 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -311,6 +311,8 @@ var RFB; this._sock.flush(); }, + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message setDesktopSize: function (width, height) { if (this._rfb_state !== "normal") { return; } diff --git a/include/ui.js b/include/ui.js index 066a35a7..6a6b6f40 100644 --- a/include/ui.js +++ b/include/ui.js @@ -1,7 +1,7 @@ /* * noVNC: HTML5 VNC client * Copyright (C) 2012 Joel Martin - * Copyright (C) 2015 Samuel Mannehed for Cendio AB + * Copyright (C) 2016 Samuel Mannehed for Cendio AB * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. @@ -132,7 +132,7 @@ var UI; UI.setBarPosition(); Util.addEvent(window, 'resize', function () { - UI.onresize(); + UI.applyResizeMode(); UI.setViewClip(); UI.updateViewDrag(); UI.setBarPosition(); @@ -188,7 +188,7 @@ var UI; 'onUpdateState': UI.updateState, 'onXvpInit': UI.updateXvpVisualState, 'onClipboard': UI.clipReceive, - 'onFBUComplete': UI.FBUComplete, + 'onFBUComplete': UI.initialResize, 'onFBResize': UI.updateViewDrag, 'onDesktopName': UI.updateDocumentTitle}); return true; @@ -245,60 +245,6 @@ var UI; $D("noVNC_resize").onchange = UI.enableDisableViewClip; }, - onresize: function (callback) { - if (!UI.rfb) return; - - var size = UI.screenSize(); - - if (size && UI.rfb_state === 'normal' && UI.rfb.get_display()) { - var display = UI.rfb.get_display(); - var scaleType = UI.getSetting('resize'); - if (scaleType === 'remote') { - // use remote resizing - - // When the local window has been resized, wait until the size remains - // the same for 0.5 seconds before sending the request for changing - // the resolution of the session - clearTimeout(UI.resizeTimeout); - UI.resizeTimeout = setTimeout(function(){ - display.set_maxWidth(size.w); - display.set_maxHeight(size.h); - Util.Debug('Attempting setDesktopSize(' + - size.w + ', ' + size.h + ')'); - UI.rfb.setDesktopSize(size.w, size.h); - }, 500); - } else if (scaleType === 'scale' || scaleType === 'downscale') { - // use local scaling - - var downscaleOnly = scaleType === 'downscale'; - var scaleRatio = display.autoscale(size.w, size.h, downscaleOnly); - UI.rfb.get_mouse().set_scale(scaleRatio); - Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); - } - } - }, - - // The screen is always the same size as the available - // viewport minus the height of the control bar - screenSize: function () { - var screen = $D('noVNC_screen'); - - // Hide the scrollbars until the size is calculated - screen.style.overflow = "hidden"; - - var pos = Util.getPosition(screen); - var w = pos.width; - var h = pos.height; - - screen.style.overflow = "visible"; - - if (isNaN(w) || isNaN(h)) { - return false; - } else { - return {w: w, h: h}; - } - }, - // Read form control compatible setting from cookie getSetting: function(name) { var ctrl = $D('noVNC_' + name); @@ -792,16 +738,6 @@ var UI; } }, - // This resize can not be done until we know from the first Frame Buffer Update - // if it is supported or not. - // The resize is needed to make sure the server desktop size is updated to the - // corresponding size of the current local window when reconnecting to an - // existing session. - FBUComplete: function(rfb, fbu) { - UI.onresize(); - UI.rfb.set_onFBUComplete(function() { }); - }, - // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; @@ -854,7 +790,7 @@ var UI; UI.rfb.disconnect(); // Restore the callback used for initial resize - UI.rfb.set_onFBUComplete(UI.FBUComplete); + UI.rfb.set_onFBUComplete(UI.initialResize); $D('noVNC_logo').style.display = "block"; $D('noVNC_screen').style.display = "none"; @@ -888,6 +824,81 @@ var UI; Util.Debug("<< UI.clipSend"); }, + + // Apply remote resizing or local scaling + applyResizeMode: function () { + if (!UI.rfb) return; + + var screen = UI.screenSize(); + + if (screen && UI.rfb_state === 'normal' && UI.rfb.get_display()) { + + var display = UI.rfb.get_display(); + var resizeMode = UI.getSetting('resize'); + + if (resizeMode === 'remote') { + + // Request changing the resolution of the remote display to + // the size of the local browser viewport. + + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(UI.resizeTimeout); + UI.resizeTimeout = setTimeout(function(){ + + // Limit the viewport to the size of the browser window + display.set_maxWidth(screen.w); + display.set_maxHeight(screen.h); + + Util.Debug('Attempting setDesktopSize(' + + screen.w + ', ' + screen.h + ')'); + + // Request a remote size covering the viewport + UI.rfb.setDesktopSize(screen.w, screen.h); + }, 500); + + } else if (resizeMode === 'scale' || resizeMode === 'downscale') { + var downscaleOnly = resizeMode === 'downscale'; + var scaleRatio = display.autoscale(screen.w, screen.h, downscaleOnly); + UI.rfb.get_mouse().set_scale(scaleRatio); + Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); + } + } + }, + + // The screen is always the same size as the available viewport + // in the browser window minus the height of the control bar + screenSize: function () { + var screen = $D('noVNC_screen'); + + // Hide the scrollbars until the size is calculated + screen.style.overflow = "hidden"; + + var pos = Util.getPosition(screen); + var w = pos.width; + var h = pos.height; + + screen.style.overflow = "visible"; + + if (isNaN(w) || isNaN(h)) { + return false; + } else { + return {w: w, h: h}; + } + }, + + // Normally we only apply the current resize mode after a window resize + // event. This means that when a new connection is opened, there is no + // resize mode active. + // We have to wait until the first FBU because this is where the client + // will find the supported encodings of the server. Some calls later in + // the chain is dependant on knowing the server-capabilities. + initialResize: function(rfb, fbu) { + UI.applyResizeMode(); + // After doing this once, we remove the callback. + UI.rfb.set_onFBUComplete(function() { }); + }, + // Set and configure viewport clipping setViewClip: function(clip) { var display; From 29475d772847013b53878fef22103d1538477395 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 18:40:13 +0200 Subject: [PATCH 133/527] Moved UpdateState and UpdateVisualState --- include/ui.js | 207 +++++++++++++++++++++++++------------------------- 1 file changed, 103 insertions(+), 104 deletions(-) diff --git a/include/ui.js b/include/ui.js index 6a6b6f40..1c87438e 100644 --- a/include/ui.js +++ b/include/ui.js @@ -245,6 +245,109 @@ var UI; $D("noVNC_resize").onchange = UI.enableDisableViewClip; }, + updateState: function(rfb, state, oldstate, msg) { + UI.rfb_state = state; + var klass; + switch (state) { + case 'failed': + case 'fatal': + klass = "noVNC_status_error"; + break; + case 'normal': + klass = "noVNC_status_normal"; + break; + case 'disconnected': + $D('noVNC_logo').style.display = "block"; + $D('noVNC_screen').style.display = "none"; + /* falls through */ + case 'loaded': + klass = "noVNC_status_normal"; + break; + case 'password': + UI.toggleConnectPanel(); + + $D('noVNC_connect_button').value = "Send Password"; + $D('noVNC_connect_button').onclick = UI.setPassword; + $D('noVNC_password').focus(); + + klass = "noVNC_status_warn"; + break; + default: + klass = "noVNC_status_warn"; + break; + } + + if (typeof(msg) !== 'undefined') { + $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_status').innerHTML = msg; + } + + UI.updateVisualState(); + }, + + // Disable/enable controls depending on connection state + updateVisualState: function() { + var connected = UI.rfb && UI.rfb_state === 'normal'; + + //Util.Debug(">> updateVisualState"); + $D('noVNC_encrypt').disabled = connected; + $D('noVNC_true_color').disabled = connected; + if (Util.browserSupportsCursorURIs()) { + $D('noVNC_cursor').disabled = connected; + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + + UI.enableDisableViewClip(); + $D('noVNC_resize').disabled = connected; + $D('noVNC_shared').disabled = connected; + $D('noVNC_view_only').disabled = connected; + $D('noVNC_path').disabled = connected; + $D('noVNC_repeaterID').disabled = connected; + + if (connected) { + UI.setViewClip(); + UI.setMouseButton(1); + $D('clipboardButton').style.display = "inline"; + $D('showKeyboard').style.display = "inline"; + $D('noVNC_extra_keys').style.display = ""; + $D('sendCtrlAltDelButton').style.display = "inline"; + } else { + UI.setMouseButton(); + $D('clipboardButton').style.display = "none"; + $D('showKeyboard').style.display = "none"; + $D('noVNC_extra_keys').style.display = "none"; + $D('sendCtrlAltDelButton').style.display = "none"; + UI.updateXvpVisualState(0); + } + + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.updateViewDrag(false); + + switch (UI.rfb_state) { + case 'fatal': + case 'failed': + case 'disconnected': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + UI.connSettingsOpen = false; + UI.toggleConnectPanel(); + break; + case 'loaded': + $D('connectButton').style.display = ""; + $D('disconnectButton').style.display = "none"; + break; + default: + $D('connectButton').style.display = "none"; + $D('disconnectButton').style.display = ""; + break; + } + + //Util.Debug("<< updateVisualState"); + }, + // Read form control compatible setting from cookie getSetting: function(name) { var ctrl = $D('noVNC_' + name); @@ -575,7 +678,6 @@ var UI; }, - setPassword: function() { UI.rfb.sendPassword($D('noVNC_password').value); //Reset connect button. @@ -622,109 +724,6 @@ var UI; } }, - updateState: function(rfb, state, oldstate, msg) { - UI.rfb_state = state; - var klass; - switch (state) { - case 'failed': - case 'fatal': - klass = "noVNC_status_error"; - break; - case 'normal': - klass = "noVNC_status_normal"; - break; - case 'disconnected': - $D('noVNC_logo').style.display = "block"; - $D('noVNC_screen').style.display = "none"; - /* falls through */ - case 'loaded': - klass = "noVNC_status_normal"; - break; - case 'password': - UI.toggleConnectPanel(); - - $D('noVNC_connect_button').value = "Send Password"; - $D('noVNC_connect_button').onclick = UI.setPassword; - $D('noVNC_password').focus(); - - klass = "noVNC_status_warn"; - break; - default: - klass = "noVNC_status_warn"; - break; - } - - if (typeof(msg) !== 'undefined') { - $D('noVNC-control-bar').setAttribute("class", klass); - $D('noVNC_status').innerHTML = msg; - } - - UI.updateVisualState(); - }, - - // Disable/enable controls depending on connection state - updateVisualState: function() { - var connected = UI.rfb && UI.rfb_state === 'normal'; - - //Util.Debug(">> updateVisualState"); - $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; - if (Util.browserSupportsCursorURIs()) { - $D('noVNC_cursor').disabled = connected; - } else { - UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; - } - - UI.enableDisableViewClip(); - $D('noVNC_resize').disabled = connected; - $D('noVNC_shared').disabled = connected; - $D('noVNC_view_only').disabled = connected; - $D('noVNC_path').disabled = connected; - $D('noVNC_repeaterID').disabled = connected; - - if (connected) { - UI.setViewClip(); - UI.setMouseButton(1); - $D('clipboardButton').style.display = "inline"; - $D('showKeyboard').style.display = "inline"; - $D('noVNC_extra_keys').style.display = ""; - $D('sendCtrlAltDelButton').style.display = "inline"; - } else { - UI.setMouseButton(); - $D('clipboardButton').style.display = "none"; - $D('showKeyboard').style.display = "none"; - $D('noVNC_extra_keys').style.display = "none"; - $D('sendCtrlAltDelButton').style.display = "none"; - UI.updateXvpVisualState(0); - } - - // State change disables viewport dragging. - // It is enabled (toggled) by direct click on the button - UI.updateViewDrag(false); - - switch (UI.rfb_state) { - case 'fatal': - case 'failed': - case 'disconnected': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; - UI.connSettingsOpen = false; - UI.toggleConnectPanel(); - break; - case 'loaded': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; - break; - default: - $D('connectButton').style.display = "none"; - $D('disconnectButton').style.display = ""; - break; - } - - //Util.Debug("<< updateVisualState"); - }, - // Disable/enable XVP button updateXvpVisualState: function(ver) { if (ver >= 1) { From 0bd2cbacaae3e6c211d48dc6e6d75268a17acc95 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 18:42:10 +0200 Subject: [PATCH 134/527] Remove whitespace from function definitions --- include/ui.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/include/ui.js b/include/ui.js index 1c87438e..f73e6e41 100644 --- a/include/ui.js +++ b/include/ui.js @@ -47,7 +47,7 @@ var UI; // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus - load: function (callback) { + load: function(callback) { WebUtil.initSettings(UI.start, callback); }, @@ -182,7 +182,7 @@ var UI; } }, - initRFB: function () { + initRFB: function() { try { UI.rfb = new RFB({'target': $D('noVNC_canvas'), 'onUpdateState': UI.updateState, @@ -825,7 +825,7 @@ var UI; // Apply remote resizing or local scaling - applyResizeMode: function () { + applyResizeMode: function() { if (!UI.rfb) return; var screen = UI.screenSize(); @@ -867,7 +867,7 @@ var UI; // The screen is always the same size as the available viewport // in the browser window minus the height of the control bar - screenSize: function () { + screenSize: function() { var screen = $D('noVNC_screen'); // Hide the scrollbars until the size is calculated @@ -951,7 +951,7 @@ var UI; }, // Handle special cases where clipping is forced on/off or locked - enableDisableViewClip: function () { + enableDisableViewClip: function() { var resizeElem = $D('noVNC_resize'); var connected = UI.rfb && UI.rfb_state === 'normal'; From 45c70c9e23567a5b1d6985668211c227459b2af9 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 18:47:55 +0200 Subject: [PATCH 135/527] Moved and changed order of the setting-functions --- include/ui.js | 160 +++++++++++++++++++++++++------------------------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/include/ui.js b/include/ui.js index f73e6e41..c67077f4 100644 --- a/include/ui.js +++ b/include/ui.js @@ -348,17 +348,14 @@ var UI; //Util.Debug("<< updateVisualState"); }, - // Read form control compatible setting from cookie - getSetting: function(name) { - var ctrl = $D('noVNC_' + name); - var val = WebUtil.readSetting(name); - if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { - if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { - val = false; - } else { - val = true; - } + // Initial page load read/initialization of settings + initSetting: function(name, defVal) { + // Check Query string followed by cookie + var val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); } + UI.updateSetting(name, val); return val; }, @@ -410,23 +407,85 @@ var UI; return val; }, - // Initial page load read/initialization of settings - initSetting: function(name, defVal) { - // Check Query string followed by cookie - var val = WebUtil.getConfigVar(name); - if (val === null) { - val = WebUtil.readSetting(name, defVal); - } - UI.updateSetting(name, val); - return val; - }, - // Force a setting to be a certain value forceSetting: function(name, val) { UI.updateSetting(name, val); return val; }, + // Read form control compatible setting from cookie + getSetting: function(name) { + var ctrl = $D('noVNC_' + name); + var val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { + val = false; + } else { + val = true; + } + } + return val; + }, + + // Open menu + openSettingsMenu: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close connection settings if open + if (UI.connSettingsOpen === true) { + UI.toggleConnectPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + $D('noVNC_settings').style.display = "block"; + $D('settingsButton').className = "noVNC_status_button_selected"; + UI.settingsOpen = true; + }, + + // Close menu (without applying settings) + closeSettingsMenu: function() { + $D('noVNC_settings').style.display = "none"; + $D('settingsButton').className = "noVNC_status_button"; + UI.settingsOpen = false; + }, + + // Toggle the settings menu: + // On open, settings are refreshed from saved cookies. + // On close, settings are applied + toggleSettingsPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + if (UI.settingsOpen) { + UI.settingsApply(); + UI.closeSettingsMenu(); + } else { + UI.updateSetting('encrypt'); + UI.updateSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.updateSetting('cursor'); + } else { + UI.updateSetting('cursor', !UI.isTouchDevice); + $D('noVNC_cursor').disabled = true; + } + UI.updateSetting('clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('stylesheet'); + UI.updateSetting('logging'); + + UI.openSettingsMenu(); + } + }, + // Show the popup status togglePopupStatus: function(text) { @@ -587,65 +646,6 @@ var UI; } }, - // Toggle the settings menu: - // On open, settings are refreshed from saved cookies. - // On close, settings are applied - toggleSettingsPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - if (UI.settingsOpen) { - UI.settingsApply(); - UI.closeSettingsMenu(); - } else { - UI.updateSetting('encrypt'); - UI.updateSetting('true_color'); - if (Util.browserSupportsCursorURIs()) { - UI.updateSetting('cursor'); - } else { - UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; - } - UI.updateSetting('clip'); - UI.updateSetting('resize'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('stylesheet'); - UI.updateSetting('logging'); - - UI.openSettingsMenu(); - } - }, - - // Open menu - openSettingsMenu: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close clipboard panel if open - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - // Close connection settings if open - if (UI.connSettingsOpen === true) { - UI.toggleConnectPanel(); - } - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } - $D('noVNC_settings').style.display = "block"; - $D('settingsButton').className = "noVNC_status_button_selected"; - UI.settingsOpen = true; - }, - - // Close menu (without applying settings) - closeSettingsMenu: function() { - $D('noVNC_settings').style.display = "none"; - $D('settingsButton').className = "noVNC_status_button"; - UI.settingsOpen = false; - }, - // Save/apply settings when 'Apply' button is pressed settingsApply: function() { //Util.Debug(">> settingsApply"); From 4e471b5b55fa2272e3c72b0df8848dabc3196965 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 19:04:32 +0200 Subject: [PATCH 136/527] Moved and split the popupStatus function Now if popupStatus is called twice it will refresh the duration of the popup. If you want to wait 1.5 seconds and close the popup you can click the popup itself. --- include/ui.js | 56 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/include/ui.js b/include/ui.js index c67077f4..72878376 100644 --- a/include/ui.js +++ b/include/ui.js @@ -221,8 +221,8 @@ var UI; $D("xvpShutdownButton").onclick = UI.xvpShutdown; $D("xvpRebootButton").onclick = UI.xvpReboot; $D("xvpResetButton").onclick = UI.xvpReset; - $D("noVNC_status").onclick = UI.togglePopupStatus; - $D("noVNC_popup_status").onclick = UI.togglePopupStatus; + $D("noVNC_status").onclick = UI.popupStatus; + $D("noVNC_popup_status").onclick = UI.closePopup; $D("xvpButton").onclick = UI.toggleXvpPanel; $D("clipboardButton").onclick = UI.toggleClipboardPanel; $D("fullscreenButton").onclick = UI.toggleFullscreen; @@ -348,6 +348,31 @@ var UI; //Util.Debug("<< updateVisualState"); }, + popupStatus: function(text) { + var psp = $D('noVNC_popup_status'); + + clearTimeout(UI.popupStatusTimeout); + + if (typeof text === 'string') { + psp.innerHTML = text; + } else { + psp.innerHTML = $D('noVNC_status').innerHTML; + } + psp.style.display = "block"; + psp.style.left = window.innerWidth/2 - + parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; + + // Show the popup for a maximum of 1.5 seconds + UI.popupStatusTimeout = setTimeout(function() { + UI.closePopup(); + }, 1500); + }, + + closePopup: function() { + clearTimeout(UI.popupStatusTimeout); + $D('noVNC_popup_status').style.display = "none"; + }, + // Initial page load read/initialization of settings initSetting: function(name, defVal) { // Check Query string followed by cookie @@ -486,31 +511,6 @@ var UI; } }, - - // Show the popup status - togglePopupStatus: function(text) { - var psp = $D('noVNC_popup_status'); - - var closePopup = function() { psp.style.display = "none"; }; - - if (window.getComputedStyle(psp).display === 'none') { - if (typeof text === 'string') { - psp.innerHTML = text; - } else { - psp.innerHTML = $D('noVNC_status').innerHTML; - } - psp.style.display = "block"; - psp.style.left = window.innerWidth/2 - - parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; - - // Show the popup for a maximum of 1.5 seconds - UI.popupStatusTimeout = setTimeout(function() { closePopup(); }, 1500); - } else { - clearTimeout(UI.popupStatusTimeout); - closePopup(); - } - }, - // Show the XVP panel toggleXvpPanel: function() { // Close the description panel @@ -968,7 +968,7 @@ var UI; // The browser is IE and we are in fullscreen mode. // - We need to force clipping while in fullscreen since // scrollbars doesn't work. - UI.togglePopupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); + UI.popupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); UI.rememberedClipSetting = UI.getSetting('clip'); UI.setViewClip(true); $D('noVNC_clip').disabled = true; From cd611a5326ed9053b94501c453f99fd24c7dfb11 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 19:13:35 +0200 Subject: [PATCH 137/527] Renamed showExtraKeys to toggleExtraKeys --- images/{showextrakeys.png => toggleextrakeys.png} | Bin include/base.css | 4 ++-- include/ui.js | 8 ++++---- vnc.html | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename images/{showextrakeys.png => toggleextrakeys.png} (100%) diff --git a/images/showextrakeys.png b/images/toggleextrakeys.png similarity index 100% rename from images/showextrakeys.png rename to images/toggleextrakeys.png diff --git a/include/base.css b/include/base.css index af284717..59e90c5b 100644 --- a/include/base.css +++ b/include/base.css @@ -442,7 +442,7 @@ html { margin-left: 0px; } -#showExtraKeysButton { display: none; } +#toggleExtraKeysButton { display: none; } #toggleCtrlButton { display: inline; } #toggleAltButton { display: inline; } #sendTabButton { display: inline; } @@ -476,7 +476,7 @@ html { padding-right: 0px; } /* collapse the extra keys on lower resolutions */ - #showExtraKeysButton { + #toggleExtraKeysButton { display: inline; } #toggleCtrlButton { diff --git a/include/ui.js b/include/ui.js index 72878376..843f38f0 100644 --- a/include/ui.js +++ b/include/ui.js @@ -211,7 +211,7 @@ var UI; $D("keyboardinput").onblur = UI.keyInputBlur; $D("keyboardinput").onsubmit = function () { return false; }; - $D("showExtraKeysButton").onclick = UI.showExtraKeys; + $D("toggleExtraKeysButton").onclick = UI.toggleExtraKeys; $D("toggleCtrlButton").onclick = UI.toggleCtrl; $D("toggleAltButton").onclick = UI.toggleAlt; $D("sendTabButton").onclick = UI.sendTab; @@ -1159,21 +1159,21 @@ var UI; UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); }, - showExtraKeys: function() { + toggleExtraKeys: function() { UI.keepKeyboard(); if(UI.extraKeysVisible === false) { $D('toggleCtrlButton').style.display = "inline"; $D('toggleAltButton').style.display = "inline"; $D('sendTabButton').style.display = "inline"; $D('sendEscButton').style.display = "inline"; - $D('showExtraKeysButton').className = "noVNC_status_button_selected"; + $D('toggleExtraKeysButton').className = "noVNC_status_button_selected"; UI.extraKeysVisible = true; } else if(UI.extraKeysVisible === true) { $D('toggleCtrlButton').style.display = ""; $D('toggleAltButton').style.display = ""; $D('sendTabButton').style.display = ""; $D('sendEscButton').style.display = ""; - $D('showExtraKeysButton').className = "noVNC_status_button"; + $D('toggleExtraKeysButton').className = "noVNC_status_button"; UI.extraKeysVisible = false; } }, diff --git a/vnc.html b/vnc.html index c0f242c0..cdf45dc9 100644 --- a/vnc.html +++ b/vnc.html @@ -74,8 +74,8 @@ autocorrect="off" autocomplete="off" spellcheck="false" mozactionhint="Enter">
    - + Date: Tue, 26 Apr 2016 23:21:32 +0200 Subject: [PATCH 138/527] Clarify comments and variable names for viewDrag --- include/ui.js | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/include/ui.js b/include/ui.js index 843f38f0..872b9e97 100644 --- a/include/ui.js +++ b/include/ui.js @@ -984,38 +984,36 @@ var UI; } }, - // Update the viewport drag/move button + // Update the viewport drag state updateViewDrag: function(drag) { if (!UI.rfb) return; - var vmb = $D('noVNC_view_drag_button'); + var viewDragButton = $D('noVNC_view_drag_button'); - // Check if viewport drag is possible + // Check if viewport drag is possible. It is only possible + // if the remote display is clipping the client display. if (UI.rfb_state === 'normal' && UI.rfb.get_display().get_viewport() && UI.rfb.get_display().clippingDisplay()) { - // Show and enable the drag button - vmb.style.display = "inline"; - vmb.disabled = false; + viewDragButton.style.display = "inline"; + viewDragButton.disabled = false; } else { - // The VNC content is the same size as - // or smaller than the display - + // The size of the remote display is the same or smaller + // than the client display. Make sure viewport drag isn't + // active when it can't be used. if (UI.rfb.get_viewportDrag) { - // Turn off viewport drag when it's - // active since it can't be used here - vmb.className = "noVNC_status_button"; + viewDragButton.className = "noVNC_status_button"; UI.rfb.set_viewportDrag(false); } - // Disable or hide the drag button + // The button is disabled instead of hidden on touch devices if (UI.rfb_state === 'normal' && UI.isTouchDevice) { - vmb.style.display = "inline"; - vmb.disabled = true; + viewDragButton.style.display = "inline"; + viewDragButton.disabled = true; } else { - vmb.style.display = "none"; + viewDragButton.style.display = "none"; } return; } @@ -1023,10 +1021,10 @@ var UI; if (typeof(drag) !== "undefined" && typeof(drag) !== "object") { if (drag) { - vmb.className = "noVNC_status_button_selected"; + viewDragButton.className = "noVNC_status_button_selected"; UI.rfb.set_viewportDrag(true); } else { - vmb.className = "noVNC_status_button"; + viewDragButton.className = "noVNC_status_button"; UI.rfb.set_viewportDrag(false); } } @@ -1035,12 +1033,12 @@ var UI; toggleViewDrag: function() { if (!UI.rfb) return; - var vmb = $D('noVNC_view_drag_button'); + var viewDragButton = $D('noVNC_view_drag_button'); if (UI.rfb.get_viewportDrag()) { - vmb.className = "noVNC_status_button"; + viewDragButton.className = "noVNC_status_button"; UI.rfb.set_viewportDrag(false); } else { - vmb.className = "noVNC_status_button_selected"; + viewDragButton.className = "noVNC_status_button_selected"; UI.rfb.set_viewportDrag(true); } }, From ab81ddf5d48e83bde15c272eea9f993300ea7c21 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 23:26:13 +0200 Subject: [PATCH 139/527] Move the connect and disconnect functions --- include/ui.js | 171 +++++++++++++++++++++++++------------------------- 1 file changed, 85 insertions(+), 86 deletions(-) diff --git a/include/ui.js b/include/ui.js index 872b9e97..60fb49f5 100644 --- a/include/ui.js +++ b/include/ui.js @@ -569,7 +569,91 @@ var UI; } }, - // Toggle fullscreen mode + // Show the connection settings panel/menu + toggleConnectPanel: function() { + // Close the description panel + $D('noVNC_description').style.display = "none"; + // Close connection settings if open + if (UI.settingsOpen === true) { + UI.settingsApply(); + UI.closeSettingsMenu(); + $D('connectButton').className = "noVNC_status_button"; + } + // Close clipboard panel if open + if (UI.clipboardOpen === true) { + UI.toggleClipboardPanel(); + } + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + + // Toggle Connection Panel + if (UI.connSettingsOpen === true) { + $D('noVNC_controls').style.display = "none"; + $D('connectButton').className = "noVNC_status_button"; + UI.connSettingsOpen = false; + UI.saveSetting('host'); + UI.saveSetting('port'); + UI.saveSetting('token'); + //UI.saveSetting('password'); + } else { + $D('noVNC_controls').style.display = "block"; + $D('connectButton').className = "noVNC_status_button_selected"; + UI.connSettingsOpen = true; + $D('noVNC_host').focus(); + } + }, + + connect: function() { + UI.closeSettingsMenu(); + UI.toggleConnectPanel(); + + var host = $D('noVNC_host').value; + var port = $D('noVNC_port').value; + var password = $D('noVNC_password').value; + var token = $D('noVNC_token').value; + var path = $D('noVNC_path').value; + + //if token is in path then ignore the new token variable + if (token) { + path = WebUtil.injectParamIfMissing(path, "token", token); + } + + if ((!host) || (!port)) { + throw new Error("Must set host and port"); + } + + if (!UI.initRFB()) return; + + UI.rfb.set_encrypt(UI.getSetting('encrypt')); + UI.rfb.set_true_color(UI.getSetting('true_color')); + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + UI.rfb.set_shared(UI.getSetting('shared')); + UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + + UI.rfb.connect(host, port, password, path); + + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + $D('noVNC_screen').style.display = "inline"; + }, + + disconnect: function() { + UI.closeSettingsMenu(); + UI.rfb.disconnect(); + + // Restore the callback used for initial resize + UI.rfb.set_onFBUComplete(UI.initialResize); + + $D('noVNC_logo').style.display = "block"; + $D('noVNC_screen').style.display = "none"; + + // Don't display the connection settings until we're actually disconnected + }, + toggleFullscreen: function() { if (document.fullscreenElement || // alternative standard method document.mozFullScreenElement || // currently working methods @@ -610,42 +694,6 @@ var UI; } }, - // Show the connection settings panel/menu - toggleConnectPanel: function() { - // Close the description panel - $D('noVNC_description').style.display = "none"; - // Close connection settings if open - if (UI.settingsOpen === true) { - UI.settingsApply(); - UI.closeSettingsMenu(); - $D('connectButton').className = "noVNC_status_button"; - } - // Close clipboard panel if open - if (UI.clipboardOpen === true) { - UI.toggleClipboardPanel(); - } - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } - - // Toggle Connection Panel - if (UI.connSettingsOpen === true) { - $D('noVNC_controls').style.display = "none"; - $D('connectButton').className = "noVNC_status_button"; - UI.connSettingsOpen = false; - UI.saveSetting('host'); - UI.saveSetting('port'); - UI.saveSetting('token'); - //UI.saveSetting('password'); - } else { - $D('noVNC_controls').style.display = "block"; - $D('connectButton').className = "noVNC_status_button_selected"; - UI.connSettingsOpen = true; - $D('noVNC_host').focus(); - } - }, - // Save/apply settings when 'Apply' button is pressed settingsApply: function() { //Util.Debug(">> settingsApply"); @@ -748,55 +796,6 @@ var UI; Util.Debug("<< UI.clipReceive"); }, - connect: function() { - UI.closeSettingsMenu(); - UI.toggleConnectPanel(); - - var host = $D('noVNC_host').value; - var port = $D('noVNC_port').value; - var password = $D('noVNC_password').value; - var token = $D('noVNC_token').value; - var path = $D('noVNC_path').value; - - //if token is in path then ignore the new token variable - if (token) { - path = WebUtil.injectParamIfMissing(path, "token", token); - } - - if ((!host) || (!port)) { - throw new Error("Must set host and port"); - } - - if (!UI.initRFB()) return; - - UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_true_color(UI.getSetting('true_color')); - UI.rfb.set_local_cursor(UI.getSetting('cursor')); - UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_view_only(UI.getSetting('view_only')); - UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - - UI.rfb.connect(host, port, password, path); - - //Close dialog. - setTimeout(UI.setBarPosition, 100); - $D('noVNC_logo').style.display = "none"; - $D('noVNC_screen').style.display = "inline"; - }, - - disconnect: function() { - UI.closeSettingsMenu(); - UI.rfb.disconnect(); - - // Restore the callback used for initial resize - UI.rfb.set_onFBUComplete(UI.initialResize); - - $D('noVNC_logo').style.display = "block"; - $D('noVNC_screen').style.display = "none"; - - // Don't display the connection settings until we're actually disconnected - }, - displayBlur: function() { if (!UI.rfb) return; From 9e45354efcc4a7519f213fc9f68165c77d07f431 Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 23:29:15 +0200 Subject: [PATCH 140/527] Renamed and moved updateXvpButton --- include/ui.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/include/ui.js b/include/ui.js index 60fb49f5..23157728 100644 --- a/include/ui.js +++ b/include/ui.js @@ -186,7 +186,7 @@ var UI; try { UI.rfb = new RFB({'target': $D('noVNC_canvas'), 'onUpdateState': UI.updateState, - 'onXvpInit': UI.updateXvpVisualState, + 'onXvpInit': UI.updateXvpButton, 'onClipboard': UI.clipReceive, 'onFBUComplete': UI.initialResize, 'onFBResize': UI.updateViewDrag, @@ -319,7 +319,7 @@ var UI; $D('showKeyboard').style.display = "none"; $D('noVNC_extra_keys').style.display = "none"; $D('sendCtrlAltDelButton').style.display = "none"; - UI.updateXvpVisualState(0); + UI.updateXvpButton(0); } // State change disables viewport dragging. @@ -540,6 +540,19 @@ var UI; } }, + // Disable/enable XVP button + updateXvpButton: function(ver) { + if (ver >= 1) { + $D('xvpButton').style.display = 'inline'; + } else { + $D('xvpButton').style.display = 'none'; + // Close XVP panel if open + if (UI.xvpOpen === true) { + UI.toggleXvpPanel(); + } + } + }, + // Show the clipboard panel toggleClipboardPanel: function() { // Close the description panel @@ -772,19 +785,6 @@ var UI; } }, - // Disable/enable XVP button - updateXvpVisualState: function(ver) { - if (ver >= 1) { - $D('xvpButton').style.display = 'inline'; - } else { - $D('xvpButton').style.display = 'none'; - // Close XVP panel if open - if (UI.xvpOpen === true) { - UI.toggleXvpPanel(); - } - } - }, - // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; From fdf21468d3a69fd0a2189317a03a11db135b40ed Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 23:41:58 +0200 Subject: [PATCH 141/527] Renamed and moved keyboard and mouse functions --- include/ui.js | 88 +++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 45 deletions(-) diff --git a/include/ui.js b/include/ui.js index 23157728..db0b3c17 100644 --- a/include/ui.js +++ b/include/ui.js @@ -208,7 +208,7 @@ var UI; $D("showKeyboard").onclick = UI.showKeyboard; $D("keyboardinput").oninput = UI.keyInput; - $D("keyboardinput").onblur = UI.keyInputBlur; + $D("keyboardinput").onblur = UI.hideKeyboard; $D("keyboardinput").onsubmit = function () { return false; }; $D("toggleExtraKeysButton").onclick = UI.toggleExtraKeys; @@ -749,10 +749,6 @@ var UI; return false; }, - sendCtrlAltDel: function() { - UI.rfb.sendCtrlAltDel(); - }, - xvpShutdown: function() { UI.rfb.xvpShutdown(); }, @@ -765,26 +761,6 @@ var UI; UI.rfb.xvpReset(); }, - setMouseButton: function(num) { - if (typeof num === 'undefined') { - // Disable mouse buttons - num = -1; - } - if (UI.rfb) { - UI.rfb.get_mouse().set_touchButton(num); - } - - var blist = [0, 1,2,4]; - for (var b = 0; b < blist.length; b++) { - var button = $D('noVNC_mouse_button' + blist[b]); - if (blist[b] === num) { - button.style.display = ""; - } else { - button.style.display = "none"; - } - } - }, - // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; @@ -1042,6 +1018,26 @@ var UI; } }, + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } + + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, + // On touch devices, show the OS keyboard showKeyboard: function() { var kbi = $D('keyboardinput'); @@ -1060,6 +1056,16 @@ var UI; } }, + hideKeyboard: function() { + $D('showKeyboard').className = "noVNC_status_button"; + //Weird bug in iOS if you change keyboardVisible + //here it does not actually occur so next time + //you click keyboard icon it doesnt work. + UI.hideKeyboardTimeout = setTimeout(function() { + UI.keyboardVisible = false; + },100); + }, + keepKeyboard: function() { clearTimeout(UI.hideKeyboardTimeout); if(UI.keyboardVisible === true) { @@ -1148,14 +1154,6 @@ var UI; } }, - keyInputBlur: function() { - $D('showKeyboard').className = "noVNC_status_button"; - //Weird bug in iOS if you change keyboardVisible - //here it does not actually occur so next time - //you click keyboard icon it doesnt work. - UI.hideKeyboardTimeout = setTimeout(function() { UI.setKeyboard(); },100); - }, - toggleExtraKeys: function() { UI.keepKeyboard(); if(UI.extraKeysVisible === false) { @@ -1175,6 +1173,16 @@ var UI; } }, + sendEsc: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Escape); + }, + + sendTab: function() { + UI.keepKeyboard(); + UI.rfb.sendKey(XK_Tab); + }, + toggleCtrl: function() { UI.keepKeyboard(); if(UI.ctrlOn === false) { @@ -1201,18 +1209,8 @@ var UI; } }, - sendTab: function() { - UI.keepKeyboard(); - UI.rfb.sendKey(XK_Tab); - }, - - sendEsc: function() { - UI.keepKeyboard(); - UI.rfb.sendKey(XK_Escape); - }, - - setKeyboard: function() { - UI.keyboardVisible = false; + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); }, //Helper to add options to dropdown. From afcf031a682e32c37a1bad24b1bd61bf02c3525e Mon Sep 17 00:00:00 2001 From: samhed Date: Tue, 26 Apr 2016 23:59:44 +0200 Subject: [PATCH 142/527] Send the xvp keys from anonymous functions --- include/ui.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/include/ui.js b/include/ui.js index db0b3c17..0e35f87f 100644 --- a/include/ui.js +++ b/include/ui.js @@ -218,9 +218,9 @@ var UI; $D("sendEscButton").onclick = UI.sendEsc; $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; - $D("xvpShutdownButton").onclick = UI.xvpShutdown; - $D("xvpRebootButton").onclick = UI.xvpReboot; - $D("xvpResetButton").onclick = UI.xvpReset; + $D("xvpShutdownButton").onclick = function() { UI.rfb.xvpShutdown(); }, + $D("xvpRebootButton").onclick = function() { UI.rfb.xvpReboot(); }, + $D("xvpResetButton").onclick = function() { UI.rfb.xvpReset(); }, $D("noVNC_status").onclick = UI.popupStatus; $D("noVNC_popup_status").onclick = UI.closePopup; $D("xvpButton").onclick = UI.toggleXvpPanel; @@ -749,18 +749,6 @@ var UI; return false; }, - xvpShutdown: function() { - UI.rfb.xvpShutdown(); - }, - - xvpReboot: function() { - UI.rfb.xvpReboot(); - }, - - xvpReset: function() { - UI.rfb.xvpReset(); - }, - // Display the desktop name in the document title updateDocumentTitle: function(rfb, name) { document.title = name + " - noVNC"; From 95dd60011c4097a0b7408a429fe68518576d24d2 Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 27 Apr 2016 00:29:25 +0200 Subject: [PATCH 143/527] Group together related functions and add dividers Dividers between the different parts of the UI has been added in order to easier get an overview. --- include/ui.js | 264 +++++++++++++++++++++++++++++++------------------- 1 file changed, 166 insertions(+), 98 deletions(-) diff --git a/include/ui.js b/include/ui.js index 0e35f87f..e04c0f50 100644 --- a/include/ui.js +++ b/include/ui.js @@ -245,6 +245,12 @@ var UI; $D("noVNC_resize").onchange = UI.enableDisableViewClip; }, +/* ------^------- + * /INIT + * ============== + * VISUAL + * ------v------*/ + updateState: function(rfb, state, oldstate, msg) { UI.rfb_state = state; var klass; @@ -373,6 +379,12 @@ var UI; $D('noVNC_popup_status').style.display = "none"; }, +/* ------^------- + * /VISUAL + * ============== + * SETTINGS + * ------v------*/ + // Initial page load read/initialization of settings initSetting: function(name, defVal) { // Check Query string followed by cookie @@ -452,6 +464,37 @@ var UI; return val; }, + // Save/apply settings when 'Apply' button is pressed + settingsApply: function() { + //Util.Debug(">> settingsApply"); + UI.saveSetting('encrypt'); + UI.saveSetting('true_color'); + if (Util.browserSupportsCursorURIs()) { + UI.saveSetting('cursor'); + } + + UI.saveSetting('resize'); + + if (UI.getSetting('resize') === 'downscale' || UI.getSetting('resize') === 'scale') { + UI.forceSetting('clip', false); + } + + UI.saveSetting('clip'); + UI.saveSetting('shared'); + UI.saveSetting('view_only'); + UI.saveSetting('path'); + UI.saveSetting('repeaterID'); + UI.saveSetting('stylesheet'); + UI.saveSetting('logging'); + + // Settings with immediate (non-connected related) effect + WebUtil.selectStylesheet(UI.getSetting('stylesheet')); + WebUtil.init_logging(UI.getSetting('logging')); + UI.setViewClip(); + UI.updateViewDrag(); + //Util.Debug("<< settingsApply"); + }, + // Open menu openSettingsMenu: function() { // Close the description panel @@ -511,6 +554,12 @@ var UI; } }, +/* ------^------- + * /SETTINGS + * ============== + * XVP + * ------v------*/ + // Show the XVP panel toggleXvpPanel: function() { // Close the description panel @@ -553,6 +602,12 @@ var UI; } }, +/* ------^------- + * /XVP + * ============== + * CLIPBOARD + * ------v------*/ + // Show the clipboard panel toggleClipboardPanel: function() { // Close the description panel @@ -582,6 +637,30 @@ var UI; } }, + clipReceive: function(rfb, text) { + Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + $D('noVNC_clipboard_text').value = text; + Util.Debug("<< UI.clipReceive"); + }, + + clipClear: function() { + $D('noVNC_clipboard_text').value = ""; + UI.rfb.clipboardPasteFrom(""); + }, + + clipSend: function() { + var text = $D('noVNC_clipboard_text').value; + Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + UI.rfb.clipboardPasteFrom(text); + Util.Debug("<< UI.clipSend"); + }, + +/* ------^------- + * /CLIPBOARD + * ============== + * CONNECTION + * ------v------*/ + // Show the connection settings panel/menu toggleConnectPanel: function() { // Close the description panel @@ -667,6 +746,22 @@ var UI; // Don't display the connection settings until we're actually disconnected }, + setPassword: function() { + UI.rfb.sendPassword($D('noVNC_password').value); + //Reset connect button. + $D('noVNC_connect_button').value = "Connect"; + $D('noVNC_connect_button').onclick = UI.connect; + //Hide connection panel. + UI.toggleConnectPanel(); + return false; + }, + +/* ------^------- + * /CONNECTION + * ============== + * FULLSCREEN + * ------v------*/ + toggleFullscreen: function() { if (document.fullscreenElement || // alternative standard method document.mozFullScreenElement || // currently working methods @@ -707,85 +802,11 @@ var UI; } }, - // Save/apply settings when 'Apply' button is pressed - settingsApply: function() { - //Util.Debug(">> settingsApply"); - UI.saveSetting('encrypt'); - UI.saveSetting('true_color'); - if (Util.browserSupportsCursorURIs()) { - UI.saveSetting('cursor'); - } - - UI.saveSetting('resize'); - - if (UI.getSetting('resize') === 'downscale' || UI.getSetting('resize') === 'scale') { - UI.forceSetting('clip', false); - } - - UI.saveSetting('clip'); - UI.saveSetting('shared'); - UI.saveSetting('view_only'); - UI.saveSetting('path'); - UI.saveSetting('repeaterID'); - UI.saveSetting('stylesheet'); - UI.saveSetting('logging'); - - // Settings with immediate (non-connected related) effect - WebUtil.selectStylesheet(UI.getSetting('stylesheet')); - WebUtil.init_logging(UI.getSetting('logging')); - UI.setViewClip(); - UI.updateViewDrag(); - //Util.Debug("<< settingsApply"); - }, - - - setPassword: function() { - UI.rfb.sendPassword($D('noVNC_password').value); - //Reset connect button. - $D('noVNC_connect_button').value = "Connect"; - $D('noVNC_connect_button').onclick = UI.connect; - //Hide connection panel. - UI.toggleConnectPanel(); - return false; - }, - - // Display the desktop name in the document title - updateDocumentTitle: function(rfb, name) { - document.title = name + " - noVNC"; - }, - - clipReceive: function(rfb, text) { - Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); - $D('noVNC_clipboard_text').value = text; - Util.Debug("<< UI.clipReceive"); - }, - - displayBlur: function() { - if (!UI.rfb) return; - - UI.rfb.get_keyboard().set_focused(false); - UI.rfb.get_mouse().set_focused(false); - }, - - displayFocus: function() { - if (!UI.rfb) return; - - UI.rfb.get_keyboard().set_focused(true); - UI.rfb.get_mouse().set_focused(true); - }, - - clipClear: function() { - $D('noVNC_clipboard_text').value = ""; - UI.rfb.clipboardPasteFrom(""); - }, - - clipSend: function() { - var text = $D('noVNC_clipboard_text').value; - Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Util.Debug("<< UI.clipSend"); - }, - +/* ------^------- + * /FULLSCREEN + * ============== + * RESIZE + * ------v------*/ // Apply remote resizing or local scaling applyResizeMode: function() { @@ -861,6 +882,12 @@ var UI; UI.rfb.set_onFBUComplete(function() { }); }, +/* ------^------- + * /RESIZE + * ============== + * CLIPPING + * ------v------*/ + // Set and configure viewport clipping setViewClip: function(clip) { var display; @@ -947,6 +974,12 @@ var UI; } }, +/* ------^------- + * /CLIPPING + * ============== + * VIEWDRAG + * ------v------*/ + // Update the viewport drag state updateViewDrag: function(drag) { if (!UI.rfb) return; @@ -1006,25 +1039,11 @@ var UI; } }, - setMouseButton: function(num) { - if (typeof num === 'undefined') { - // Disable mouse buttons - num = -1; - } - if (UI.rfb) { - UI.rfb.get_mouse().set_touchButton(num); - } - - var blist = [0, 1,2,4]; - for (var b = 0; b < blist.length; b++) { - var button = $D('noVNC_mouse_button' + blist[b]); - if (blist[b] === num) { - button.style.display = ""; - } else { - button.style.display = "none"; - } - } - }, +/* ------^------- + * /VIEWDRAG + * ============== + * KEYBOARD + * ------v------*/ // On touch devices, show the OS keyboard showKeyboard: function() { @@ -1201,6 +1220,51 @@ var UI; UI.rfb.sendCtrlAltDel(); }, +/* ------^------- + * /KEYBOARD + * ============== + * MISC + * ------v------*/ + + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; + } + if (UI.rfb) { + UI.rfb.get_mouse().set_touchButton(num); + } + + var blist = [0, 1,2,4]; + for (var b = 0; b < blist.length; b++) { + var button = $D('noVNC_mouse_button' + blist[b]); + if (blist[b] === num) { + button.style.display = ""; + } else { + button.style.display = "none"; + } + } + }, + + displayBlur: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(false); + UI.rfb.get_mouse().set_focused(false); + }, + + displayFocus: function() { + if (!UI.rfb) return; + + UI.rfb.get_keyboard().set_focused(true); + UI.rfb.get_mouse().set_focused(true); + }, + + // Display the desktop name in the document title + updateDocumentTitle: function(rfb, name) { + document.title = name + " - noVNC"; + }, + //Helper to add options to dropdown. addOption: function(selectbox, text, value) { var optn = document.createElement("OPTION"); @@ -1217,5 +1281,9 @@ var UI; $D('noVNC-control-bar').style.width = vncwidth + 'px'; } +/* ------^------- + * /MISC + * ============== + */ }; })(); From 4d26f58e0d711144ec6bcdabf26811db1c457238 Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 27 Apr 2016 00:31:50 +0200 Subject: [PATCH 144/527] Change name of clipboard functions There were a high risk of confusion between clipboard functions and clipping functions. --- include/ui.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/include/ui.js b/include/ui.js index e04c0f50..7a8600a9 100644 --- a/include/ui.js +++ b/include/ui.js @@ -187,7 +187,7 @@ var UI; UI.rfb = new RFB({'target': $D('noVNC_canvas'), 'onUpdateState': UI.updateState, 'onXvpInit': UI.updateXvpButton, - 'onClipboard': UI.clipReceive, + 'onClipboard': UI.clipboardReceive, 'onFBUComplete': UI.initialResize, 'onFBResize': UI.updateViewDrag, 'onDesktopName': UI.updateDocumentTitle}); @@ -233,8 +233,8 @@ var UI; $D("noVNC_clipboard_text").onfocus = UI.displayBlur; $D("noVNC_clipboard_text").onblur = UI.displayFocus; - $D("noVNC_clipboard_text").onchange = UI.clipSend; - $D("noVNC_clipboard_clear_button").onclick = UI.clipClear; + $D("noVNC_clipboard_text").onchange = UI.clipboardSend; + $D("noVNC_clipboard_clear_button").onclick = UI.clipboardClear; $D("noVNC_settings_menu").onmouseover = UI.displayBlur; $D("noVNC_settings_menu").onmouseover = UI.displayFocus; @@ -637,22 +637,22 @@ var UI; } }, - clipReceive: function(rfb, text) { - Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); + clipboardReceive: function(rfb, text) { + Util.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "..."); $D('noVNC_clipboard_text').value = text; - Util.Debug("<< UI.clipReceive"); + Util.Debug("<< UI.clipboardReceive"); }, - clipClear: function() { + clipboardClear: function() { $D('noVNC_clipboard_text').value = ""; UI.rfb.clipboardPasteFrom(""); }, - clipSend: function() { + clipboardSend: function() { var text = $D('noVNC_clipboard_text').value; - Util.Debug(">> UI.clipSend: " + text.substr(0,40) + "..."); + Util.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "..."); UI.rfb.clipboardPasteFrom(text); - Util.Debug("<< UI.clipSend"); + Util.Debug("<< UI.clipboardSend"); }, /* ------^------- From a20a8987650a7a3080706fe1c3678cf4f2e855ad Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 27 Apr 2016 00:41:16 +0200 Subject: [PATCH 145/527] Timeouts should always use anonymous functions While you CAN pass functions in strings, it is not the correct way of doing it. The browsers are just being nice. --- include/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index 7a8600a9..3146ae6c 100644 --- a/include/ui.js +++ b/include/ui.js @@ -728,7 +728,7 @@ var UI; UI.rfb.connect(host, port, password, path); //Close dialog. - setTimeout(UI.setBarPosition, 100); + setTimeout(function () { UI.setBarPosition; } ); $D('noVNC_logo').style.display = "none"; $D('noVNC_screen').style.display = "inline"; }, From 682fd02be65107ed9267969ff21e3661bddda5ba Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 30 Apr 2016 04:35:19 +0200 Subject: [PATCH 146/527] Change the names of settings-elements This change was made in order to make it easier to distinguish settings-elements from other elements. One example of the problem that was solved is the two elements "noVNC_clip" and "noVNC_clipboard" where the first is the setting for clipping mode. That element was now renamed to "noVNC_setting_clip". --- include/base.css | 14 +++++----- include/ui.js | 66 ++++++++++++++++++++++++------------------------ vnc.html | 34 ++++++++++++------------- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/include/base.css b/include/base.css index 59e90c5b..56e03c3a 100644 --- a/include/base.css +++ b/include/base.css @@ -1,7 +1,7 @@ /* * noVNC base CSS * Copyright (C) 2012 Joel Martin - * Copyright (C) 2013 Samuel Mannehed for Cendio AB + * Copyright (C) 2016 Samuel Mannehed for Cendio AB * noVNC is licensed under the MPL 2.0 (see LICENSE.txt) * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ @@ -30,18 +30,18 @@ html { padding-bottom:8px; } -#noVNC_host { +#noVNC_setting_host { width:150px; } -#noVNC_port { +#noVNC_setting_port { width: 80px; } -#noVNC_password { +#noVNC_setting_password { width: 150px; } -#noVNC_encrypt { +#noVNC_setting_encrypt { } -#noVNC_path { +#noVNC_setting_path { width: 100px; } #noVNC_connect_button { @@ -109,7 +109,7 @@ html { padding: 0px; } -#noVNC_apply { +#noVNC_settings_apply { float:right; } diff --git a/include/ui.js b/include/ui.js index 3146ae6c..53c6e560 100644 --- a/include/ui.js +++ b/include/ui.js @@ -60,13 +60,13 @@ var UI; var sheets = WebUtil.getStylesheets(); var i; for (i = 0; i < sheets.length; i += 1) { - UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); + UI.addOption($D('noVNC_setting_stylesheet'),sheets[i].title, sheets[i].title); } // Logging selection dropdown var llevels = ['error', 'warn', 'info', 'debug']; for (i = 0; i < llevels.length; i += 1) { - UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); + UI.addOption($D('noVNC_setting_logging'),llevels[i], llevels[i]); } // Settings with immediate effects @@ -114,7 +114,7 @@ var UI; UI.updateVisualState(); - $D('noVNC_host').focus(); + $D('noVNC_setting_host').focus(); // Show mouse selector buttons on touch screen devices if (UI.isTouchDevice) { @@ -238,11 +238,11 @@ var UI; $D("noVNC_settings_menu").onmouseover = UI.displayBlur; $D("noVNC_settings_menu").onmouseover = UI.displayFocus; - $D("noVNC_apply").onclick = UI.settingsApply; + $D("noVNC_settings_apply").onclick = UI.settingsApply; $D("noVNC_connect_button").onclick = UI.connect; - $D("noVNC_resize").onchange = UI.enableDisableViewClip; + $D("noVNC_setting_resize").onchange = UI.enableDisableViewClip; }, /* ------^------- @@ -274,7 +274,7 @@ var UI; $D('noVNC_connect_button').value = "Send Password"; $D('noVNC_connect_button').onclick = UI.setPassword; - $D('noVNC_password').focus(); + $D('noVNC_setting_password').focus(); klass = "noVNC_status_warn"; break; @@ -296,21 +296,21 @@ var UI; var connected = UI.rfb && UI.rfb_state === 'normal'; //Util.Debug(">> updateVisualState"); - $D('noVNC_encrypt').disabled = connected; - $D('noVNC_true_color').disabled = connected; + $D('noVNC_setting_encrypt').disabled = connected; + $D('noVNC_setting_true_color').disabled = connected; if (Util.browserSupportsCursorURIs()) { - $D('noVNC_cursor').disabled = connected; + $D('noVNC_setting_cursor').disabled = connected; } else { UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; + $D('noVNC_setting_cursor').disabled = true; } UI.enableDisableViewClip(); - $D('noVNC_resize').disabled = connected; - $D('noVNC_shared').disabled = connected; - $D('noVNC_view_only').disabled = connected; - $D('noVNC_path').disabled = connected; - $D('noVNC_repeaterID').disabled = connected; + $D('noVNC_setting_resize').disabled = connected; + $D('noVNC_setting_shared').disabled = connected; + $D('noVNC_setting_view_only').disabled = connected; + $D('noVNC_setting_path').disabled = connected; + $D('noVNC_setting_repeaterID').disabled = connected; if (connected) { UI.setViewClip(); @@ -408,7 +408,7 @@ var UI; // Update the settings control value = UI.getSetting(name); - var ctrl = $D('noVNC_' + name); + var ctrl = $D('noVNC_setting_' + name); if (ctrl.type === 'checkbox') { ctrl.checked = value; @@ -431,7 +431,7 @@ var UI; // Save control setting to cookie saveSetting: function(name) { - var val, ctrl = $D('noVNC_' + name); + var val, ctrl = $D('noVNC_setting_' + name); if (ctrl.type === 'checkbox') { val = ctrl.checked; } else if (typeof ctrl.options !== 'undefined') { @@ -452,7 +452,7 @@ var UI; // Read form control compatible setting from cookie getSetting: function(name) { - var ctrl = $D('noVNC_' + name); + var ctrl = $D('noVNC_setting_' + name); var val = WebUtil.readSetting(name); if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { @@ -539,7 +539,7 @@ var UI; UI.updateSetting('cursor'); } else { UI.updateSetting('cursor', !UI.isTouchDevice); - $D('noVNC_cursor').disabled = true; + $D('noVNC_setting_cursor').disabled = true; } UI.updateSetting('clip'); UI.updateSetting('resize'); @@ -693,7 +693,7 @@ var UI; $D('noVNC_controls').style.display = "block"; $D('connectButton').className = "noVNC_status_button_selected"; UI.connSettingsOpen = true; - $D('noVNC_host').focus(); + $D('noVNC_setting_host').focus(); } }, @@ -701,11 +701,11 @@ var UI; UI.closeSettingsMenu(); UI.toggleConnectPanel(); - var host = $D('noVNC_host').value; - var port = $D('noVNC_port').value; - var password = $D('noVNC_password').value; - var token = $D('noVNC_token').value; - var path = $D('noVNC_path').value; + var host = $D('noVNC_setting_host').value; + var port = $D('noVNC_setting_port').value; + var password = $D('noVNC_setting_password').value; + var token = $D('noVNC_setting_token').value; + var path = $D('noVNC_setting_path').value; //if token is in path then ignore the new token variable if (token) { @@ -747,7 +747,7 @@ var UI; }, setPassword: function() { - UI.rfb.sendPassword($D('noVNC_password').value); + UI.rfb.sendPassword($D('noVNC_setting_password').value); //Reset connect button. $D('noVNC_connect_button').value = "Connect"; $D('noVNC_connect_button').onclick = UI.connect; @@ -942,18 +942,18 @@ var UI; // Handle special cases where clipping is forced on/off or locked enableDisableViewClip: function() { - var resizeElem = $D('noVNC_resize'); + var resizeSetting = $D('noVNC_setting_resize'); var connected = UI.rfb && UI.rfb_state === 'normal'; if (UI.isSafari) { // Safari auto-hides the scrollbars which makes them // impossible to use in most cases UI.setViewClip(true); - $D('noVNC_clip').disabled = true; - } else if (resizeElem.value === 'downscale' || resizeElem.value === 'scale') { + $D('noVNC_setting_clip').disabled = true; + } else if (resizeSetting.value === 'downscale' || resizeSetting.value === 'scale') { // Disable clipping if we are scaling UI.setViewClip(false); - $D('noVNC_clip').disabled = true; + $D('noVNC_setting_clip').disabled = true; } else if (document.msFullscreenElement) { // The browser is IE and we are in fullscreen mode. // - We need to force clipping while in fullscreen since @@ -961,13 +961,13 @@ var UI; UI.popupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); UI.rememberedClipSetting = UI.getSetting('clip'); UI.setViewClip(true); - $D('noVNC_clip').disabled = true; + $D('noVNC_setting_clip').disabled = true; } else if (document.body.msRequestFullscreen && UI.rememberedClip !== null) { // Restore view clip to what it was before fullscreen on IE UI.setViewClip(UI.rememberedClipSetting); - $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + $D('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; } else { - $D('noVNC_clip').disabled = connected || UI.isTouchDevice; + $D('noVNC_setting_clip').disabled = connected || UI.isTouchDevice; if (UI.isTouchDevice) { UI.setViewClip(true); } diff --git a/vnc.html b/vnc.html index cdf45dc9..9d583738 100644 --- a/vnc.html +++ b/vnc.html @@ -5,7 +5,7 @@

  • -
  • +
  • @@ -196,10 +196,10 @@
      -
    • -
    • -
    • -
    • +
    • +
    • +
    • +
    From 3f2c25a60fc08514b6f5c9ccdfb5dab14fc59b80 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 26 May 2016 23:15:39 +0200 Subject: [PATCH 147/527] Use a consistent naming convention for elements * Element names we use "_" as word-delimiter, not "-" * Element names use less camel-case * Element names end with the type * Element names always start with noVNC_ --- include/base.css | 40 ++++++------- include/black.css | 2 +- include/blue.css | 2 +- include/ui.js | 142 +++++++++++++++++++++++----------------------- vnc.html | 44 +++++++------- 5 files changed, 115 insertions(+), 115 deletions(-) diff --git a/include/base.css b/include/base.css index 56e03c3a..8d88c0f5 100644 --- a/include/base.css +++ b/include/base.css @@ -56,10 +56,10 @@ html { #noVNC_view_drag_button { display: none; } -#sendCtrlAltDelButton { +#noVNC_sendCtrlAltDel_button { display: none; } -#fullscreenButton { +#noVNC_fullscreen_button { display: none; } #noVNC_xvp_buttons { @@ -77,13 +77,13 @@ html { position: relative; } -.noVNC-buttons-left { +.noVNC_buttons_left { float: left; z-index: 1; position: relative; } -.noVNC-buttons-right { +.noVNC_buttons_right { float:right; right: 0px; z-index: 2; @@ -233,7 +233,7 @@ html { right:85px; } -#keyboardinput { +#noVNC_keyboardinput { width:1px; height:1px; background-color:#fff; @@ -278,7 +278,7 @@ html { } /* Control bar */ -#noVNC-control-bar { +#noVNC_control_bar { position:fixed; display:block; @@ -427,11 +427,11 @@ html { font-size: 180px; } -.noVNC-buttons-left { +.noVNC_buttons_left { padding-left: 10px; } -.noVNC-buttons-right { +.noVNC_buttons_right { padding-right: 10px; } @@ -442,11 +442,11 @@ html { margin-left: 0px; } -#toggleExtraKeysButton { display: none; } -#toggleCtrlButton { display: inline; } -#toggleAltButton { display: inline; } -#sendTabButton { display: inline; } -#sendEscButton { display: inline; } +#noVNC_toggleExtraKeys_button { display: none; } +#noVNC_toggleCtrl_button { display: inline; } +#noVNC_toggleAlt_button { display: inline; } +#noVNC_sendTab_button { display: inline; } +#noVNC_sendEsc_button { display: inline; } /* left-align the status text on lower resolutions */ @media screen and (max-width: 800px){ @@ -469,35 +469,35 @@ html { .noVNC_status_button { font-size: 10px; } - .noVNC-buttons-left { + .noVNC_buttons_left { padding-left: 0px; } - .noVNC-buttons-right { + .noVNC_buttons_right { padding-right: 0px; } /* collapse the extra keys on lower resolutions */ - #toggleExtraKeysButton { + #noVNC_toggleExtraKeys_button { display: inline; } - #toggleCtrlButton { + #noVNC_toggleCtrl_button { display: none; position: absolute; top: 30px; left: 0px; } - #toggleAltButton { + #noVNC_toggleAlt_button { display: none; position: absolute; top: 65px; left: 0px; } - #sendTabButton { + #noVNC_sendTab_button { display: none; position: absolute; top: 100px; left: 0px; } - #sendEscButton { + #noVNC_sendEsc_button { display: none; position: absolute; top: 135px; diff --git a/include/black.css b/include/black.css index 7d940c5a..5c4558dc 100644 --- a/include/black.css +++ b/include/black.css @@ -6,7 +6,7 @@ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ -#keyboardinput { +#noVNC_keyboardinput { background-color:#000; } diff --git a/include/blue.css b/include/blue.css index b2a0adcc..4ab53bde 100644 --- a/include/blue.css +++ b/include/blue.css @@ -58,7 +58,7 @@ color:#fff; } -#keyboardinput { +#noVNC_keyboardinput { background-color:#04073d; } diff --git a/include/ui.js b/include/ui.js index 53c6e560..37de7b6e 100644 --- a/include/ui.js +++ b/include/ui.js @@ -148,7 +148,7 @@ var UI; document.documentElement.mozRequestFullScreen || document.documentElement.webkitRequestFullscreen || document.body.msRequestFullscreen)) { - $D('fullscreenButton').style.display = "inline"; + $D('noVNC_fullscreen_button').style.display = "inline"; Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); @@ -205,31 +205,31 @@ var UI; $D("noVNC_mouse_button1").onclick = function () { UI.setMouseButton(2); }; $D("noVNC_mouse_button2").onclick = function () { UI.setMouseButton(4); }; $D("noVNC_mouse_button4").onclick = function () { UI.setMouseButton(0); }; - $D("showKeyboard").onclick = UI.showKeyboard; + $D("noVNC_keyboard_button").onclick = UI.showKeyboard; - $D("keyboardinput").oninput = UI.keyInput; - $D("keyboardinput").onblur = UI.hideKeyboard; - $D("keyboardinput").onsubmit = function () { return false; }; + $D("noVNC_keyboardinput").oninput = UI.keyInput; + $D("noVNC_keyboardinput").onblur = UI.hideKeyboard; + $D("noVNC_keyboardinput").onsubmit = function () { return false; }; - $D("toggleExtraKeysButton").onclick = UI.toggleExtraKeys; - $D("toggleCtrlButton").onclick = UI.toggleCtrl; - $D("toggleAltButton").onclick = UI.toggleAlt; - $D("sendTabButton").onclick = UI.sendTab; - $D("sendEscButton").onclick = UI.sendEsc; + $D("noVNC_toggleExtraKeys_button").onclick = UI.toggleExtraKeys; + $D("noVNC_toggleCtrl_button").onclick = UI.toggleCtrl; + $D("noVNC_toggleAlt_button").onclick = UI.toggleAlt; + $D("noVNC_sendTab_button").onclick = UI.sendTab; + $D("noVNC_sendEsc_button").onclick = UI.sendEsc; - $D("sendCtrlAltDelButton").onclick = UI.sendCtrlAltDel; - $D("xvpShutdownButton").onclick = function() { UI.rfb.xvpShutdown(); }, - $D("xvpRebootButton").onclick = function() { UI.rfb.xvpReboot(); }, - $D("xvpResetButton").onclick = function() { UI.rfb.xvpReset(); }, + $D("noVNC_sendCtrlAltDel_button").onclick = UI.sendCtrlAltDel; + $D("noVNC_xvpShutdown_button").onclick = function() { UI.rfb.xvpShutdown(); }, + $D("noVNC_xvpReboot_button").onclick = function() { UI.rfb.xvpReboot(); }, + $D("noVNC_xvpReset_button").onclick = function() { UI.rfb.xvpReset(); }, $D("noVNC_status").onclick = UI.popupStatus; $D("noVNC_popup_status").onclick = UI.closePopup; - $D("xvpButton").onclick = UI.toggleXvpPanel; - $D("clipboardButton").onclick = UI.toggleClipboardPanel; - $D("fullscreenButton").onclick = UI.toggleFullscreen; - $D("settingsButton").onclick = UI.toggleSettingsPanel; - $D("connectButton").onclick = UI.toggleConnectPanel; - $D("disconnectButton").onclick = UI.disconnect; - $D("descriptionButton").onclick = UI.toggleConnectPanel; + $D("noVNC_toggleXvp_button").onclick = UI.toggleXvpPanel; + $D("noVNC_clipboard_button").onclick = UI.toggleClipboardPanel; + $D("noVNC_fullscreen_button").onclick = UI.toggleFullscreen; + $D("noVNC_settings_button").onclick = UI.toggleSettingsPanel; + $D("noVNC_connectPanel_button").onclick = UI.toggleConnectPanel; + $D("noVNC_disconnect_button").onclick = UI.disconnect; + $D("noVNC_description_button").onclick = UI.toggleConnectPanel; $D("noVNC_clipboard_text").onfocus = UI.displayBlur; $D("noVNC_clipboard_text").onblur = UI.displayFocus; @@ -284,7 +284,7 @@ var UI; } if (typeof(msg) !== 'undefined') { - $D('noVNC-control-bar').setAttribute("class", klass); + $D('noVNC_control_bar').setAttribute("class", klass); $D('noVNC_status').innerHTML = msg; } @@ -315,16 +315,16 @@ var UI; if (connected) { UI.setViewClip(); UI.setMouseButton(1); - $D('clipboardButton').style.display = "inline"; - $D('showKeyboard').style.display = "inline"; + $D('noVNC_clipboard_button').style.display = "inline"; + $D('noVNC_keyboard_button').style.display = "inline"; $D('noVNC_extra_keys').style.display = ""; - $D('sendCtrlAltDelButton').style.display = "inline"; + $D('noVNC_sendCtrlAltDel_button').style.display = "inline"; } else { UI.setMouseButton(); - $D('clipboardButton').style.display = "none"; - $D('showKeyboard').style.display = "none"; + $D('noVNC_clipboard_button').style.display = "none"; + $D('noVNC_keyboard_button').style.display = "none"; $D('noVNC_extra_keys').style.display = "none"; - $D('sendCtrlAltDelButton').style.display = "none"; + $D('noVNC_sendCtrlAltDel_button').style.display = "none"; UI.updateXvpButton(0); } @@ -336,18 +336,18 @@ var UI; case 'fatal': case 'failed': case 'disconnected': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; + $D('noVNC_connectPanel_button').style.display = ""; + $D('noVNC_disconnect_button').style.display = "none"; UI.connSettingsOpen = false; UI.toggleConnectPanel(); break; case 'loaded': - $D('connectButton').style.display = ""; - $D('disconnectButton').style.display = "none"; + $D('noVNC_connectPanel_button').style.display = ""; + $D('noVNC_disconnect_button').style.display = "none"; break; default: - $D('connectButton').style.display = "none"; - $D('disconnectButton').style.display = ""; + $D('noVNC_connectPanel_button').style.display = "none"; + $D('noVNC_disconnect_button').style.display = ""; break; } @@ -512,14 +512,14 @@ var UI; UI.toggleXvpPanel(); } $D('noVNC_settings').style.display = "block"; - $D('settingsButton').className = "noVNC_status_button_selected"; + $D('noVNC_settings_button').className = "noVNC_status_button_selected"; UI.settingsOpen = true; }, // Close menu (without applying settings) closeSettingsMenu: function() { $D('noVNC_settings').style.display = "none"; - $D('settingsButton').className = "noVNC_status_button"; + $D('noVNC_settings_button').className = "noVNC_status_button"; UI.settingsOpen = false; }, @@ -580,11 +580,11 @@ var UI; // Toggle XVP panel if (UI.xvpOpen === true) { $D('noVNC_xvp').style.display = "none"; - $D('xvpButton').className = "noVNC_status_button"; + $D('noVNC_toggleXvp_button').className = "noVNC_status_button"; UI.xvpOpen = false; } else { $D('noVNC_xvp').style.display = "block"; - $D('xvpButton').className = "noVNC_status_button_selected"; + $D('noVNC_toggleXvp_button').className = "noVNC_status_button_selected"; UI.xvpOpen = true; } }, @@ -592,9 +592,9 @@ var UI; // Disable/enable XVP button updateXvpButton: function(ver) { if (ver >= 1) { - $D('xvpButton').style.display = 'inline'; + $D('noVNC_toggleXvp_button').style.display = 'inline'; } else { - $D('xvpButton').style.display = 'none'; + $D('noVNC_toggleXvp_button').style.display = 'none'; // Close XVP panel if open if (UI.xvpOpen === true) { UI.toggleXvpPanel(); @@ -628,11 +628,11 @@ var UI; // Toggle Clipboard Panel if (UI.clipboardOpen === true) { $D('noVNC_clipboard').style.display = "none"; - $D('clipboardButton').className = "noVNC_status_button"; + $D('noVNC_clipboard_button').className = "noVNC_status_button"; UI.clipboardOpen = false; } else { $D('noVNC_clipboard').style.display = "block"; - $D('clipboardButton').className = "noVNC_status_button_selected"; + $D('noVNC_clipboard_button').className = "noVNC_status_button_selected"; UI.clipboardOpen = true; } }, @@ -669,7 +669,7 @@ var UI; if (UI.settingsOpen === true) { UI.settingsApply(); UI.closeSettingsMenu(); - $D('connectButton').className = "noVNC_status_button"; + $D('noVNC_connectPanel_button').className = "noVNC_status_button"; } // Close clipboard panel if open if (UI.clipboardOpen === true) { @@ -683,7 +683,7 @@ var UI; // Toggle Connection Panel if (UI.connSettingsOpen === true) { $D('noVNC_controls').style.display = "none"; - $D('connectButton').className = "noVNC_status_button"; + $D('noVNC_connectPanel_button').className = "noVNC_status_button"; UI.connSettingsOpen = false; UI.saveSetting('host'); UI.saveSetting('port'); @@ -691,7 +691,7 @@ var UI; //UI.saveSetting('password'); } else { $D('noVNC_controls').style.display = "block"; - $D('connectButton').className = "noVNC_status_button_selected"; + $D('noVNC_connectPanel_button').className = "noVNC_status_button_selected"; UI.connSettingsOpen = true; $D('noVNC_setting_host').focus(); } @@ -796,9 +796,9 @@ var UI; document.mozFullScreenElement || // currently working methods document.webkitFullscreenElement || document.msFullscreenElement ) { - $D('fullscreenButton').className = "noVNC_status_button_selected"; + $D('noVNC_fullscreen_button').className = "noVNC_status_button_selected"; } else { - $D('fullscreenButton').className = "noVNC_status_button"; + $D('noVNC_fullscreen_button').className = "noVNC_status_button"; } }, @@ -1047,8 +1047,8 @@ var UI; // On touch devices, show the OS keyboard showKeyboard: function() { - var kbi = $D('keyboardinput'); - var skb = $D('showKeyboard'); + var kbi = $D('noVNC_keyboardinput'); + var skb = $D('noVNC_keyboard_button'); var l = kbi.value.length; if(UI.keyboardVisible === false) { kbi.focus(); @@ -1064,7 +1064,7 @@ var UI; }, hideKeyboard: function() { - $D('showKeyboard').className = "noVNC_status_button"; + $D('noVNC_keyboard_button').className = "noVNC_status_button"; //Weird bug in iOS if you change keyboardVisible //here it does not actually occur so next time //you click keyboard icon it doesnt work. @@ -1076,16 +1076,16 @@ var UI; keepKeyboard: function() { clearTimeout(UI.hideKeyboardTimeout); if(UI.keyboardVisible === true) { - $D('keyboardinput').focus(); - $D('showKeyboard').className = "noVNC_status_button_selected"; + $D('noVNC_keyboardinput').focus(); + $D('noVNC_keyboard_button').className = "noVNC_status_button_selected"; } else if(UI.keyboardVisible === false) { - $D('keyboardinput').blur(); - $D('showKeyboard').className = "noVNC_status_button"; + $D('noVNC_keyboardinput').blur(); + $D('noVNC_keyboard_button').className = "noVNC_status_button"; } }, keyboardinputReset: function() { - var kbi = $D('keyboardinput'); + var kbi = $D('noVNC_keyboardinput'); kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); UI.lastKeyboardinput = kbi.value; }, @@ -1164,18 +1164,18 @@ var UI; toggleExtraKeys: function() { UI.keepKeyboard(); if(UI.extraKeysVisible === false) { - $D('toggleCtrlButton').style.display = "inline"; - $D('toggleAltButton').style.display = "inline"; - $D('sendTabButton').style.display = "inline"; - $D('sendEscButton').style.display = "inline"; - $D('toggleExtraKeysButton').className = "noVNC_status_button_selected"; + $D('noVNC_toggleCtrl_button').style.display = "inline"; + $D('noVNC_toggleAlt_button').style.display = "inline"; + $D('noVNC_sendTab_button').style.display = "inline"; + $D('noVNC_sendEsc_button').style.display = "inline"; + $D('noVNC_toggleExtraKeys_button').className = "noVNC_status_button_selected"; UI.extraKeysVisible = true; } else if(UI.extraKeysVisible === true) { - $D('toggleCtrlButton').style.display = ""; - $D('toggleAltButton').style.display = ""; - $D('sendTabButton').style.display = ""; - $D('sendEscButton').style.display = ""; - $D('toggleExtraKeysButton').className = "noVNC_status_button"; + $D('noVNC_toggleCtrl_button').style.display = ""; + $D('noVNC_toggleAlt_button').style.display = ""; + $D('noVNC_sendTab_button').style.display = ""; + $D('noVNC_sendEsc_button').style.display = ""; + $D('noVNC_toggleExtraKeys_button').className = "noVNC_status_button"; UI.extraKeysVisible = false; } }, @@ -1194,11 +1194,11 @@ var UI; UI.keepKeyboard(); if(UI.ctrlOn === false) { UI.rfb.sendKey(XK_Control_L, true); - $D('toggleCtrlButton').className = "noVNC_status_button_selected"; + $D('noVNC_toggleCtrl_button').className = "noVNC_status_button_selected"; UI.ctrlOn = true; } else if(UI.ctrlOn === true) { UI.rfb.sendKey(XK_Control_L, false); - $D('toggleCtrlButton').className = "noVNC_status_button"; + $D('noVNC_toggleCtrl_button').className = "noVNC_status_button"; UI.ctrlOn = false; } }, @@ -1207,11 +1207,11 @@ var UI; UI.keepKeyboard(); if(UI.altOn === false) { UI.rfb.sendKey(XK_Alt_L, true); - $D('toggleAltButton').className = "noVNC_status_button_selected"; + $D('noVNC_toggleAlt_button').className = "noVNC_status_button_selected"; UI.altOn = true; } else if(UI.altOn === true) { UI.rfb.sendKey(XK_Alt_L, false); - $D('toggleAltButton').className = "noVNC_status_button"; + $D('noVNC_toggleAlt_button').className = "noVNC_status_button"; UI.altOn = false; } }, @@ -1274,11 +1274,11 @@ var UI; }, setBarPosition: function() { - $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; + $D('noVNC_control_bar').style.top = (window.pageYOffset) + 'px'; $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; var vncwidth = $D('noVNC_container').style.offsetWidth; - $D('noVNC-control-bar').style.width = vncwidth + 'px'; + $D('noVNC_control_bar').style.width = vncwidth + 'px'; } /* ------^------- diff --git a/vnc.html b/vnc.html index 9d583738..2990aacf 100644 --- a/vnc.html +++ b/vnc.html @@ -48,9 +48,9 @@ -
    +
    -
    +
    @@ -64,26 +64,26 @@ -
    + id="noVNC_toggleExtraKeys_button" class="noVNC_status_button"> + id="noVNC_toggleCtrl_button" class="noVNC_status_button"> + id="noVNC_toggleAlt_button" class="noVNC_status_button"> + id="noVNC_sendTab_button" class="noVNC_status_button"> + id="noVNC_sendEsc_button" class="noVNC_status_button">
    @@ -91,27 +91,27 @@
    -
    +
    @@ -128,7 +128,7 @@ and website for more information.
    - +
    @@ -147,9 +147,9 @@
    - - - + + +
    @@ -204,7 +204,7 @@
    -
    +
    From a1dbbcc1a71ca88a7e3e2fd0e47362744b69c265 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 27 May 2016 09:36:00 +0200 Subject: [PATCH 148/527] Consistent closing tags for void elements --- vnc.html | 56 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/vnc.html b/vnc.html index 2990aacf..0b653065 100644 --- a/vnc.html +++ b/vnc.html @@ -16,20 +16,20 @@ --> noVNC - + - + - + - + @@ -53,19 +53,19 @@
    + title="Move/Drag Viewport" />
    + id="noVNC_mouse_button0" class="noVNC_status_button" /> + id="noVNC_mouse_button1" class="noVNC_status_button" /> + id="noVNC_mouse_button2" class="noVNC_status_button" /> + id="noVNC_mouse_button4" class="noVNC_status_button" /> + value="Keyboard" title="Show Keyboard" /> @@ -141,7 +141,7 @@
    + value="Clear" />
    @@ -157,14 +157,14 @@
      -
    • Encrypt
    • -
    • True Color
    • -
    • Local Cursor
    • -
    • Clip to Window
    • -
    • Shared Mode
    • -
    • View Only
    • +
    • Encrypt
    • +
    • True Color
    • +
    • Local Cursor
    • +
    • Clip to Window
    • +
    • Shared Mode
    • +
    • View Only

    • -
    • Path
    • +
    • Path
    • -
    • Repeater ID
    • +
    • Repeater ID


    • -
    • +
    @@ -199,8 +199,8 @@
  • -
  • -
  • +
  • +
  • From ae11605141f345d38bf1e5fcf8fb85c42c771145 Mon Sep 17 00:00:00 2001 From: Samuel Date: Thu, 2 Jun 2016 22:37:52 +0200 Subject: [PATCH 149/527] Split the setDesktopSize function (#618) In order to follow the surrounding coding-standards, the setDesktopSize client message is split from the public function which now is called requestDesktopSize(). --- include/rfb.js | 58 +++++++++++++++++++++++++++++++---------------- include/ui.js | 4 ++-- tests/test.rfb.js | 8 +++---- vnc_auto.html | 2 +- 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 48fa5a8c..7340fad2 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -311,28 +311,13 @@ var RFB; this._sock.flush(); }, - setDesktopSize: function (width, height) { + requestDesktopSize: function (width, height) { if (this._rfb_state !== "normal") { return; } if (this._supportsSetDesktopSize) { - - var arr = [251]; // msg-type - arr.push8(0); // padding - arr.push16(width); // width - arr.push16(height); // height - - arr.push8(1); // number-of-screens - arr.push8(0); // padding - - // screen array - arr.push32(this._screen_id); // id - arr.push16(0); // x-position - arr.push16(0); // y-position - arr.push16(width); // width - arr.push16(height); // height - arr.push32(this._screen_flags); // flags - - this._sock.send(arr); + RFB.messages.setDesktopSize(this._sock, width, height, + this._screen_id, this._screen_flags); + this._sock.flush(); } }, @@ -1340,6 +1325,41 @@ var RFB; sock._sQlen += 8 + n; }, + setDesktopSize: function (sock, width, height, id, flags) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + }, + pixelFormat: function (sock, bpp, depth, true_color) { var buff = sock._sQ; var offset = sock._sQlen; diff --git a/include/ui.js b/include/ui.js index cfdedb3a..95bc1356 100644 --- a/include/ui.js +++ b/include/ui.js @@ -263,9 +263,9 @@ var UI; UI.resizeTimeout = setTimeout(function(){ display.set_maxWidth(size.w); display.set_maxHeight(size.h); - Util.Debug('Attempting setDesktopSize(' + + Util.Debug('Attempting requestDesktopSize(' + size.w + ', ' + size.h + ')'); - UI.rfb.setDesktopSize(size.w, size.h); + UI.rfb.requestDesktopSize(size.w, size.h); }, 500); } else if (scaleType === 'scale' || scaleType === 'downscale') { // use local scaling diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 1b73986f..a0f2fa70 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -219,7 +219,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); - describe("#setDesktopSize", function () { + describe("#requestDesktopSize", function () { beforeEach(function() { client._sock = new Websock(); client._sock.open('ws://', 'binary'); @@ -244,19 +244,19 @@ describe('Remote Frame Buffer Protocol Client', function() { expected.push16(2); // height expected.push32(0); // flags - client.setDesktopSize(1, 2); + client.requestDesktopSize(1, 2); expect(client._sock).to.have.sent(new Uint8Array(expected)); }); it('should not send the request if the client has not recieved a ExtendedDesktopSize rectangle', function () { client._supportsSetDesktopSize = false; - client.setDesktopSize(1,2); + client.requestDesktopSize(1,2); expect(client._sock.flush).to.not.have.been.called; }); it('should not send the request if we are not in a normal state', function () { client._rfb_state = "broken"; - client.setDesktopSize(1,2); + client.requestDesktopSize(1,2); expect(client._sock.flush).to.not.have.been.called; }); }); diff --git a/vnc_auto.html b/vnc_auto.html index 73174713..bbf94d7b 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -92,7 +92,7 @@ var controlbarH = $D('noVNC_status_bar').offsetHeight; var padding = 5; if (innerW !== undefined && innerH !== undefined) - rfb.setDesktopSize(innerW, innerH - controlbarH - padding); + rfb.requestDesktopSize(innerW, innerH - controlbarH - padding); } } function FBUComplete(rfb, fbu) { From 5a20f425d4fddf67aef14c25ccea7d3fc2315677 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 2 Jun 2016 14:53:22 +0200 Subject: [PATCH 150/527] Clean up encodings array List pseudo-encodings seperately and sorted by encoding number. --- include/rfb.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index c022969b..f426c15e 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -43,18 +43,20 @@ var RFB; ['HEXTILE', 0x05 ], ['RRE', 0x02 ], ['RAW', 0x00 ], - ['DesktopSize', -223 ], - ['Cursor', -239 ], // Psuedo-encoding settings + //['JPEG_quality_lo', -32 ], ['JPEG_quality_med', -26 ], //['JPEG_quality_hi', -23 ], //['compress_lo', -255 ], ['compress_hi', -247 ], + + ['DesktopSize', -223 ], ['last_rect', -224 ], - ['xvp', -309 ], - ['ExtendedDesktopSize', -308 ] + ['Cursor', -239 ], + ['ExtendedDesktopSize', -308 ], + ['xvp', -309 ] ]; this._encHandlers = {}; From 3af1a3a05b8d251faaa552c8dee2b3294fc9d357 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 2 Jun 2016 14:57:44 +0200 Subject: [PATCH 151/527] Avoid unnecessary delays We only use setTimeout() to avoid hanging the browser, not because we actually want a delay. So let's use the smallest delay there is. --- include/rfb.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index f426c15e..fa2401dd 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -255,7 +255,7 @@ var RFB; sendPassword: function (passwd) { this._rfb_password = passwd; this._rfb_state = 'Authentication'; - setTimeout(this._init_msg.bind(this), 1); + setTimeout(this._init_msg.bind(this), 0); }, sendCtrlAltDel: function () { @@ -549,7 +549,7 @@ var RFB; this._msgTimer = setTimeout(function () { this._msgTimer = null; this._handle_message(); - }.bind(this), 10); + }.bind(this), 0); } else { Util.Debug("More data to process, existing timer"); } From cf0236de18a3790badd0892ed4de4c2dc95ce91d Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 3 Jun 2016 14:13:15 +0200 Subject: [PATCH 152/527] Fix typo in pointer event test --- tests/test.rfb.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index a0f2fa70..08159040 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1741,7 +1741,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send a mask of 1 on mousedown', function () { client._mouse._onMouseButton(10, 12, 1, 0x001); var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; - RFB.messages.pointerEvent(pointer_msg, 0, 10, 12, 0x001); + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); expect(client._sock).to.have.sent(pointer_msg._sQ); }); From b1538a0fa4d9e6e98fdd06eed1a8699d11c8fdc0 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 3 Jun 2016 14:13:35 +0200 Subject: [PATCH 153/527] Fix 'sent' assertion We were completely mishandling the length of the data. Make sure we look at the length of the websocket rather than the websock object, and also compare with the expected length. --- tests/assertions.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/assertions.js b/tests/assertions.js index 4bd0cf40..fa122dc3 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -37,10 +37,14 @@ chai.use(function (_chai, utils) { }; var data = obj._websocket._get_sent_data(); var same = true; - for (var i = 0; i < obj.length; i++) { - if (data[i] != target_data[i]) { - same = false; - break; + if (data.length != target_data.length) { + same = false; + } else { + for (var i = 0; i < data.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } } } if (!same) { From 89d2837fa8e3f0498bbac7b7194f453d88bcbc1f Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 2 Jun 2016 15:09:00 +0200 Subject: [PATCH 154/527] Always flush socket after each message Make sure our messages go away right away, rather than having to remember to call flush from the caller, or causing extra delays by waiting for the send timer. This should result in a more responsive system. --- include/rfb.js | 25 +++++++------------------ tests/test.rfb.js | 35 +++++++++++++++-------------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index fa2401dd..14e0aa62 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -67,7 +67,6 @@ var RFB; this._display = null; // Display object this._keyboard = null; // Keyboard input handler object this._mouse = null; // Mouse input handler object - this._sendTimer = null; // Send Queue check timer this._disconnTimer = null; // disconnection timer this._msgTimer = null; // queued handle_msg timer @@ -268,8 +267,6 @@ var RFB; RFB.messages.keyEvent(this._sock, XK_Delete, 0); RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); RFB.messages.keyEvent(this._sock, XK_Control_L, 0); - - this._sock.flush(); }, xvpOp: function (ver, op) { @@ -303,14 +300,11 @@ var RFB; RFB.messages.keyEvent(this._sock, code, 1); RFB.messages.keyEvent(this._sock, code, 0); } - - this._sock.flush(); }, clipboardPasteFrom: function (text) { if (this._rfb_state !== 'normal') { return; } RFB.messages.clientCutText(this._sock, text); - this._sock.flush(); }, // Requests a change of remote desktop size. This message is an extension @@ -386,11 +380,6 @@ var RFB; }, _cleanupSocket: function (state) { - if (this._sendTimer) { - clearInterval(this._sendTimer); - this._sendTimer = null; - } - if (this._msgTimer) { clearInterval(this._msgTimer); this._msgTimer = null; @@ -564,7 +553,6 @@ var RFB; _handleKeyPress: function (keysym, down) { if (this._view_only) { return; } // View only, skip keyboard, events RFB.messages.keyEvent(this._sock, keysym, down); - this._sock.flush(); }, _handleMouseButton: function (x, y, down, bmask) { @@ -670,10 +658,6 @@ var RFB; this._rfb_version = this._rfb_max_version; } - // Send updates either at a rate of 1 update per 50ms, or - // whatever slower rate the network can handle - this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50); - var cversion = "00" + parseInt(this._rfb_version, 10) + ".00" + ((this._rfb_version * 10) % 10); this._sock.send_string("RFB " + cversion + "\n"); @@ -992,7 +976,6 @@ var RFB; this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; - this._sock.flush(); if (this._encrypt) { this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name); @@ -1095,7 +1078,6 @@ var RFB; var ret = this._framebufferUpdate(); if (ret) { RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); - this._sock.flush(); } return ret; @@ -1285,6 +1267,7 @@ var RFB; buff[offset + 7] = keysym; sock._sQlen += 8; + sock.flush(); }, pointerEvent: function (sock, x, y, mask) { @@ -1302,6 +1285,7 @@ var RFB; buff[offset + 5] = y; sock._sQlen += 6; + sock.flush(); }, // TODO(directxman12): make this unicode compatible? @@ -1327,6 +1311,7 @@ var RFB; } sock._sQlen += 8 + n; + sock.flush(); }, setDesktopSize: function (sock, width, height, id, flags) { @@ -1362,6 +1347,7 @@ var RFB; buff[offset + 23] = flags; sock._sQlen += 24; + sock.flush(); }, pixelFormat: function (sock, bpp, depth, true_color) { @@ -1397,6 +1383,7 @@ var RFB; buff[offset + 19] = 0; // padding sock._sQlen += 20; + sock.flush(); }, clientEncodings: function (sock, encodings, local_cursor, true_color) { @@ -1431,6 +1418,7 @@ var RFB; buff[offset + 3] = cnt; sock._sQlen += j - offset; + sock.flush(); }, fbUpdateRequests: function (sock, cleanDirty, fb_width, fb_height) { @@ -1477,6 +1465,7 @@ var RFB; buff[offset + 9] = h & 0xFF; sock._sQlen += 10; + sock.flush(); } }; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 08159040..aed339c8 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -132,7 +132,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { - var expected = {_sQ: new Uint8Array(48), _sQlen: 0}; + var expected = {_sQ: new Uint8Array(48), _sQlen: 0, flush: function () {}}; RFB.messages.keyEvent(expected, 0xFFE3, 1); RFB.messages.keyEvent(expected, 0xFFE9, 1); RFB.messages.keyEvent(expected, 0xFFFF, 1); @@ -168,14 +168,14 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should send a single key with the given code and state (down = true)', function () { - var expected = {_sQ: new Uint8Array(8), _sQlen: 0}; + var expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: function () {}}; RFB.messages.keyEvent(expected, 123, 1); client.sendKey(123, true); expect(client._sock).to.have.sent(expected._sQ); }); it('should send both a down and up event if the state is not specified', function () { - var expected = {_sQ: new Uint8Array(16), _sQlen: 0}; + var expected = {_sQ: new Uint8Array(16), _sQlen: 0, flush: function () {}}; RFB.messages.keyEvent(expected, 123, 1); RFB.messages.keyEvent(expected, 123, 0); client.sendKey(123); @@ -206,7 +206,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should send the given text in a paste event', function () { - var expected = {_sQ: new Uint8Array(11), _sQlen: 0}; + var expected = {_sQ: new Uint8Array(11), _sQlen: 0, flush: function () {}}; RFB.messages.clientCutText(expected, 'abc'); client.clipboardPasteFrom('abc'); expect(client._sock).to.have.sent(expected._sQ); @@ -571,13 +571,6 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_version).to.equal(3.8); }); - it('should initialize the flush interval', function () { - client._sock.flush = sinon.spy(); - send_ver('003.008', client); - this.clock.tick(100); - expect(client._sock.flush).to.have.been.calledThrice; - }); - it('should send back the interpreted version', function () { send_ver('004.000', client); @@ -1070,7 +1063,9 @@ describe('Remote Frame Buffer Protocol Client', function() { client.set_true_color(true); client.set_local_cursor(false); // we skip the cursor encoding - var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), _sQlen: 0}; + var expected = {_sQ: new Uint8Array(34 + 4 * (client._encodings.length - 1)), + _sQlen: 0, + flush: function () {}}; RFB.messages.pixelFormat(expected, 4, 3, true); RFB.messages.clientEncodings(expected, client._encodings, false, true); var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, @@ -1161,7 +1156,7 @@ describe('Remote Frame Buffer Protocol Client', function() { } it('should send an update request if there is sufficient data', function () { - var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); @@ -1178,7 +1173,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should resume receiving an update if we previously did not have enough data', function () { - var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0}; + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); @@ -1733,14 +1728,14 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send a pointer event on mouse button presses', function () { client._mouse._onMouseButton(10, 12, 1, 0x001); - var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: function () {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a mask of 1 on mousedown', function () { client._mouse._onMouseButton(10, 12, 1, 0x001); - var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: function () {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); expect(client._sock).to.have.sent(pointer_msg._sQ); }); @@ -1748,14 +1743,14 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send a mask of 0 on mouseup', function () { client._mouse_buttonMask = 0x001; client._mouse._onMouseButton(10, 12, 0, 0x001); - var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: function () {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a pointer event on mouse movement', function () { client._mouse._onMouseMove(10, 12); - var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0}; + var pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: function () {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); expect(client._sock).to.have.sent(pointer_msg._sQ); }); @@ -1763,7 +1758,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should set the button mask so that future mouse movements use it', function () { client._mouse._onMouseButton(10, 12, 1, 0x010); client._mouse._onMouseMove(13, 9); - var pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0}; + var pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0, flush: function () {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); expect(client._sock).to.have.sent(pointer_msg._sQ); @@ -1829,7 +1824,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send a key message on a key press', function () { client._keyboard._onKeyPress(1234, 1); - var key_msg = {_sQ: new Uint8Array(8), _sQlen: 0}; + var key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: function () {}}; RFB.messages.keyEvent(key_msg, 1234, 1); expect(client._sock).to.have.sent(key_msg._sQ); }); From 37195e4b5e776d1ce19a65ba72009536de7dcd67 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 3 Jun 2016 15:22:19 +0200 Subject: [PATCH 155/527] Lower level check for framebuffer update requests Try to avoid using helper functions with complex logic when verifying results as those helper functions are also something we want to verify. Also add a test for a mix of clean and dirty areas specifically to make sure that helper function behaves properly. --- tests/test.rfb.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/tests/test.rfb.js b/tests/test.rfb.js index aed339c8..a0fdf8f2 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1068,9 +1068,7 @@ describe('Remote Frame Buffer Protocol Client', function() { flush: function () {}}; RFB.messages.pixelFormat(expected, 4, 3, true); RFB.messages.clientEncodings(expected, 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 } ] }; - RFB.messages.fbUpdateRequests(expected, expected_cdr, 27, 32); + RFB.messages.fbUpdateRequest(expected, false, 0, 0, 27, 32); send_server_init({ width: 27, height: 32 }, client); expect(client._sock).to.have.sent(expected._sQ); @@ -1157,9 +1155,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send an update request if there is sufficient data', function () { var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; - var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, - dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; - RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); + RFB.messages.fbUpdateRequest(expected_msg, false, 0, 0, 240, 20); client._framebufferUpdate = function () { return true; }; client._sock._websocket._receive_data(new Uint8Array([0])); @@ -1174,9 +1170,7 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should resume receiving an update if we previously did not have enough data', function () { var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; - var expected_cdr = { cleanBox: { x: 0, y: 0, w: 0, h: 0 }, - dirtyBoxes: [ { x: 0, y: 0, w: 240, h: 20 } ] }; - RFB.messages.fbUpdateRequests(expected_msg, expected_cdr, 240, 20); + RFB.messages.fbUpdateRequest(expected_msg, false, 0, 0, 240, 20); // just enough to set FBU.rects client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); @@ -1188,6 +1182,21 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._sock).to.have.sent(expected_msg._sQ); }); + it('should send a request for both clean and dirty areas', function () { + var expected_msg = {_sQ: new Uint8Array(20), _sQlen: 0, flush: function() {}}; + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 120, h: 20 }, + dirtyBoxes: [ { x: 120, y: 0, w: 120, h: 20 } ] }; + + RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 120, 20); + RFB.messages.fbUpdateRequest(expected_msg, false, 120, 0, 120, 20); + + client._framebufferUpdate = function () { return true; }; + client._display.getCleanDirtyReset = function () { return expected_cdr; }; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + it('should parse out information from a header before any actual data comes in', function () { client.set_onFBUReceive(sinon.spy()); var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' }; From 3df13262394515efe5877ce33fd0dbdf35e1b743 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 2 Jun 2016 16:00:33 +0200 Subject: [PATCH 156/527] Add support for fences We don't actually use these, but servers may require this for other features. --- include/rfb.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++- tests/test.rfb.js | 25 +++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index 14e0aa62..71672ffe 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -56,7 +56,8 @@ var RFB; ['last_rect', -224 ], ['Cursor', -239 ], ['ExtendedDesktopSize', -308 ], - ['xvp', -309 ] + ['xvp', -309 ], + ['Fence', -312 ] ]; this._encHandlers = {}; @@ -70,6 +71,8 @@ var RFB; this._disconnTimer = null; // disconnection timer this._msgTimer = null; // queued handle_msg timer + this._supportsFence = false; + // Frame buffer update state this._FBU = { rects: 0, @@ -1041,6 +1044,42 @@ var RFB; return true; }, + _handle_server_fence_msg: function() { + if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + var flags = this._sock.rQshift32(); + var length = this._sock.rQshift8(); + + if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } + var payload = this._sock.rQshiftStr(length); // FIXME: 64 bytes max + + this._supportsFence = true; + + /* + * Fence flags + * + * (1<<0) - BlockBefore + * (1<<1) - BlockAfter + * (1<<2) - SyncNext + * (1<<31) - Request + */ + + if (!(flags & (1<<31))) { + return this._fail("Unexpected fence response"); + } + + // Filter out unsupported flags + // FIXME: support syncNext + flags &= (1<<0) | (1<<1); + + // BlockBefore and BlockAfter are automatically handled by + // the fact that we process each incoming message + // synchronuosly. + RFB.messages.clientFence(this._sock, flags, payload); + + return true; + }, + _handle_xvp_msg: function () { if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } this._sock.rQskip8(); // Padding @@ -1092,6 +1131,9 @@ var RFB; case 3: // ServerCutText return this._handle_server_cut_text(); + case 248: // ServerFence + return this._handle_server_fence_msg(); + case 250: // XVP return this._handle_xvp_msg(); @@ -1350,6 +1392,33 @@ var RFB; sock.flush(); }, + clientFence: function (sock, flags, payload) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 248; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = flags >> 24; // flags + buff[offset + 5] = flags >> 16; + buff[offset + 6] = flags >> 8; + buff[offset + 7] = flags; + + var n = payload.length; + + buff[offset + 8] = n; // length + + for (var i = 0; i < n; i++) { + buff[offset + 9 + i] = payload.charCodeAt(i); + } + + sock._sQlen += 9 + n; + sock.flush(); + }, + pixelFormat: function (sock, bpp, depth, true_color) { var buff = sock._sQ; var offset = sock._sQlen; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index a0fdf8f2..be6aa1b2 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1705,6 +1705,31 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client.get_onBell()).to.have.been.calledOnce; }); + it('should respond correctly to ServerFence', function () { + var expected_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: function() {}}; + var incoming_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: function() {}}; + + var payload = "foo\x00ab9"; + + // ClientFence and ServerFence are identical in structure + RFB.messages.clientFence(expected_msg, (1<<0) | (1<<1), payload); + RFB.messages.clientFence(incoming_msg, 0xffffffff, payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + + expected_msg._sQlen = 0; + incoming_msg._sQlen = 0; + + RFB.messages.clientFence(expected_msg, (1<<0), payload); + RFB.messages.clientFence(incoming_msg, (1<<0) | (1<<31), payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + it('should fail on an unknown message type', function () { client._sock._websocket._receive_data(new Uint8Array([87])); expect(client._rfb_state).to.equal('failed'); From 3daa86cbae4fa3d6af18b32e300dd1bf66a005ea Mon Sep 17 00:00:00 2001 From: samhed Date: Wed, 8 Jun 2016 16:02:37 +0200 Subject: [PATCH 157/527] Update vnc_auto after the screen namechange Commit 553864e85813f886b9c60b4825c8d4b714cae695 changed the name of noVNC_screen to noVNC_container, vnc_auto.html was not updated accordingly. Fixes #621 --- vnc_auto.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnc_auto.html b/vnc_auto.html index bbf94d7b..2d81cca7 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -46,7 +46,7 @@ -
    +
    From b2cdd558590c942150838d99c7429ec9a43329ce Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 10 Jun 2016 17:15:42 +0200 Subject: [PATCH 158/527] Proper error handling for tight filters Don't throw an exception when we encounter an unsupported tight subencoding. --- include/rfb.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index c022969b..d0e6c8f7 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -2011,8 +2011,7 @@ var RFB; } else { // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter // Filter 2, Gradient is valid but not use if jpeg is enabled - // TODO(directxman12): why aren't we just calling '_fail' here - throw new Error("Unsupported tight subencoding received, filter: " + filterId); + this._fail("Unsupported tight subencoding received, filter: " + filterId); } break; case "copy": From 15e733f5330345b41e91927adaa2ce8f984f0593 Mon Sep 17 00:00:00 2001 From: Joel Martin Date: Mon, 13 Jun 2016 10:22:43 -0500 Subject: [PATCH 159/527] Clarify that utils/launch.sh is MPL-2.0 Also, note in the top-level license file that the default noVNC license for files that are not explicitly marked or mentioned in the LICENSE.txt file are by default MPL-2.0 licensed. --- LICENSE.txt | 4 ++++ utils/launch.sh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/LICENSE.txt b/LICENSE.txt index 924d2b0c..f217929f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -57,6 +57,10 @@ licenses (all MPL 2.0 compatible): utils/inflator.partial.js include/inflator.js : MIT (for pako) +Any other files not mentioned above are typically marked with +a copyright/license header at the top of the file. The default noVNC +license is MPL-2.0. + The following license texts are included: docs/LICENSE.MPL-2.0 diff --git a/utils/launch.sh b/utils/launch.sh index ecee06cd..af350b0a 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash +# Copyright 2016 Joel Martin +# Copyright 2016 Solly Ross +# Licensed under MPL 2.0 or any later version (see LICENSE.txt) + usage() { if [ "$*" ]; then echo "$*" From 67685d0700ad3f26e59fd3a5309dad2d2d993782 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 1 Jul 2016 14:50:30 -0400 Subject: [PATCH 160/527] Fix missing mistyped setTimeout handler in UI This commit fixes a mistyped setTimeout handler that would result in the desired function not getting called (due to a missing set of parentheses). It also removes some uncessary anonymous functions, and just passes the function objects directly to setTimeout. --- include/ui.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/ui.js b/include/ui.js index 4fc6f09d..be56d513 100644 --- a/include/ui.js +++ b/include/ui.js @@ -369,9 +369,7 @@ var UI; parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; // Show the popup for a maximum of 1.5 seconds - UI.popupStatusTimeout = setTimeout(function() { - UI.closePopup(); - }, 1500); + UI.popupStatusTimeout = setTimeout(UI.closePopup, 1500); }, closePopup: function() { @@ -728,7 +726,7 @@ var UI; UI.rfb.connect(host, port, password, path); //Close dialog. - setTimeout(function () { UI.setBarPosition; } ); + setTimeout(UI.setBarPosition, 100); $D('noVNC_logo').style.display = "none"; $D('noVNC_screen').style.display = "inline"; }, @@ -1155,7 +1153,7 @@ var UI; // text has been added to the field event.target.blur(); // This has to be ran outside of the input handler in order to work - setTimeout(function() { UI.keepKeyboard(); }, 0); + setTimeout(UI.keepKeyboard, 0); } else { UI.lastKeyboardinput = newValue; } From 5230ab676472cc48c06237ea861938c905782651 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Fri, 1 Jul 2016 14:16:09 -0400 Subject: [PATCH 161/527] Release 0.6.0 This bumps the release version to 0.6.0 --- docs/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/VERSION b/docs/VERSION index bd73f470..5a2a5806 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -0.4 +0.6 From da82b3426c27bf1a79f671c5825d68ab8c0c5d9f Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Mon, 4 Jul 2016 15:29:00 -0400 Subject: [PATCH 162/527] Release 0.6.1 Fixes version number mismatch (also adds more detailed instructions for releasing). Fixes #628 --- docs/VERSION | 2 +- docs/release.txt | 43 ++++++++++++++++++++++++++++++++++--------- package.json | 2 +- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docs/VERSION b/docs/VERSION index 5a2a5806..ee6cdce3 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -0.6 +0.6.1 diff --git a/docs/release.txt b/docs/release.txt index 1660b9b8..3e036354 100644 --- a/docs/release.txt +++ b/docs/release.txt @@ -1,9 +1,34 @@ -- Update and commit docs/VERSION -- Create version tag and tarball from tag - WVER=0.3 - git tag v${WVER} - git push origin master - git push origin v${WVER} - git archive --format=tar --prefix=novnc-${WVER}/ v${WVER} > novnc-${WVER}.tar - gzip novnc-${WVER}.tar -- Upload tarball to repo +- Decide a new version number X.Y.Z (follow SemVer) +- Update version in package.json +- Update version in docs/VERSION +- Commit the change with a commit like "Release X.Y.Z" +- Add a new release on GitHub called "vX.Y.Z", and populate it with + release notes of the following form (where A.B.C is the last release): + +Major Changes Since A.B.C +========================= + +*Insert warnings here about incompatibilities* + +*Thanks to all the contributors who filed bugs, added features, and fixed bugs +during this release :tada:* + +App-visible Changes +------------------- + +- *feature* a feature which improves the app usage (#PRNUM) +- *bugfix* a bug fix which fixes the app usage (#PRNUM) +- *refactor* a refactor which changes the app usage (#PRNUM) + +Library-visible Changes +----------------------- + +- *feature* a feature which improves the noVNC APIs (#PRNUM) +- *bugfix* a bug fix which fixes the noVNC APIs (#PRNUM) +- *refactor* a refactor which changes the noVNC APIs (#PRNUM) + +App-internals Changes +--------------------- + +- *bugfix* a bug fix with affects the internals of noVNC only (#PRNUM) +- *refactor* a refactor which affects the internals of noVNC only (#PRNUM) diff --git a/package.json b/package.json index 8f12f879..3db93eb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "noVNC", - "version": "0.5.1", + "version": "0.6.1", "description": "An HTML5 VNC client", "main": "karma.conf.js", "directories": { From 6024677ffaf9dcd3ada1d5771bd7c41ad3003df0 Mon Sep 17 00:00:00 2001 From: "Alexander E. Patrakov" Date: Sun, 31 Jul 2016 21:55:04 +0500 Subject: [PATCH 163/527] Fixed RGB data buffer size (#615) --- include/display.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/display.js b/include/display.js index a492817d..665fd3d2 100644 --- a/include/display.js +++ b/include/display.js @@ -496,7 +496,7 @@ var Display; // NB(directxman12): it's technically more performant here to use preallocated arrays, // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, // this probably isn't getting called *nearly* as much - var new_arr = new Uint8Array(width * height * 4); + var new_arr = new Uint8Array(width * height * 3); new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); this.renderQ_push({ 'type': 'blitRgb', From 83d3e02fd90fe29251083447f13ef92acfcfa1a1 Mon Sep 17 00:00:00 2001 From: nunojusto Date: Tue, 16 Aug 2016 22:10:14 +0100 Subject: [PATCH 164/527] Update launch.sh Just a correction of port in use test algoritm. This way we will not have problems when using port X and having some other service using zyX or any *X port --- utils/launch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/launch.sh b/utils/launch.sh index af350b0a..36119541 100755 --- a/utils/launch.sh +++ b/utils/launch.sh @@ -69,7 +69,7 @@ done which netstat >/dev/null 2>&1 \ || die "Must have netstat installed" -netstat -ltn | grep -qs "${PORT} .*LISTEN" \ +netstat -ltn | grep -qs ":${PORT} .*LISTEN" \ && die "Port ${PORT} in use. Try --listen PORT" trap "cleanup" TERM QUIT INT EXIT From 6521c6ac0f260fb4719a8805e3799da17d48f35e Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Tue, 22 Dec 2015 15:16:52 -0500 Subject: [PATCH 165/527] Fix ImageData Constructor Support Detection Our support detection for the `ImageData(data, width, height)` constructor would fail in certain browsers because the size of a 1x1 ImageData's Uint8ClampedArray is 4, not 1. --- include/display.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/display.js b/include/display.js index a492817d..a6a57227 100644 --- a/include/display.js +++ b/include/display.js @@ -17,7 +17,7 @@ var Display; var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; try { - new ImageData(new Uint8ClampedArray(1), 1, 1); + new ImageData(new Uint8ClampedArray(4), 1, 1); SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; } catch (ex) { // ignore failure From c7925074c43ee36d5ae078ead14ba249e7272457 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 24 Feb 2016 16:13:06 -0500 Subject: [PATCH 166/527] Revert poor workaround for brief Firefox bug This commit reverts a fix for a bug which briefly occured on certain nightlies of Firefox. It was never intended to persist in the code base, and is causing other actual errors to be swallowed. --- include/rfb.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index d0e6c8f7..694c14ba 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1165,14 +1165,7 @@ var RFB; this._timing.last_fbu = (new Date()).getTime(); - var handler = this._encHandlers[this._FBU.encoding]; - try { - //ret = this._encHandlers[this._FBU.encoding](); - ret = handler(); - } catch (ex) { - console.log("missed " + this._FBU.encoding + ": " + handler); - ret = this._encHandlers[this._FBU.encoding](); - } + ret = this._encHandlers[this._FBU.encoding](); now = (new Date()).getTime(); this._timing.cur_fbu += (now - this._timing.last_fbu); From e961641fa996a1dec440a0ca2550da6397adec93 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 25 Aug 2016 14:40:21 +0200 Subject: [PATCH 167/527] Fix the extra keys Commit 529c64e1036b622ff971b1a0cd13856837d3a9eb broke this. #shame# --- include/ui.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/ui.js b/include/ui.js index be56d513..2684f987 100644 --- a/include/ui.js +++ b/include/ui.js @@ -33,6 +33,7 @@ var UI; connSettingsOpen: false, clipboardOpen: false, keyboardVisible: false, + extraKeysVisible: false, isTouchDevice: false, isSafari: false, @@ -40,10 +41,8 @@ var UI; lastKeyboardinput: null, defaultKeyboardinputLen: 100, - shiftDown: false, - ctrlDown: false, - altDown: false, - altGrDown: false, + ctrlOn: false, + altOn: false, // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus From 9d16e512ba839108afd8a478efaf852c3ffd0404 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 25 Aug 2016 15:16:04 +0200 Subject: [PATCH 168/527] Proper spacing --- include/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/ui.js b/include/ui.js index 2684f987..ae2fdb67 100644 --- a/include/ui.js +++ b/include/ui.js @@ -33,7 +33,7 @@ var UI; connSettingsOpen: false, clipboardOpen: false, keyboardVisible: false, - extraKeysVisible: false, + extraKeysVisible: false, isTouchDevice: false, isSafari: false, From b9efece4a8b896a533cfd456098246011dc35f84 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 25 Aug 2016 15:32:15 +0200 Subject: [PATCH 169/527] Fix window close warning The code didn't follow current API for the beforeunload event. --- include/ui.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/include/ui.js b/include/ui.js index ae2fdb67..058ae5a4 100644 --- a/include/ui.js +++ b/include/ui.js @@ -156,11 +156,17 @@ var UI; Util.addEvent(window, 'load', UI.keyboardinputReset); + // While connected we want to display a confirmation dialogue + // if the user tries to leave the page Util.addEvent(window, 'beforeunload', function () { if (UI.rfb && UI.rfb_state === 'normal') { - return "You are currently connected."; + var msg = "You are currently connected."; + e.returnValue = msg; + return msg; + else { + return void 0; // To prevent the dialogue when disconnected } - } ); + }); // Show description by default when hosted at for kanaka.github.com if (location.host === "kanaka.github.io") { From da17d0369de93d9f879840978b6852295a456787 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 25 Aug 2016 16:49:02 +0200 Subject: [PATCH 170/527] Add missing event argument and curly bracket --- include/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/ui.js b/include/ui.js index 058ae5a4..03863634 100644 --- a/include/ui.js +++ b/include/ui.js @@ -158,12 +158,12 @@ var UI; // While connected we want to display a confirmation dialogue // if the user tries to leave the page - Util.addEvent(window, 'beforeunload', function () { + Util.addEvent(window, 'beforeunload', function (e) { if (UI.rfb && UI.rfb_state === 'normal') { var msg = "You are currently connected."; e.returnValue = msg; return msg; - else { + } else { return void 0; // To prevent the dialogue when disconnected } }); From 76a86ff51436ed3e4cf5125ca8d58240b52e9c37 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 2 Jun 2016 16:41:38 +0200 Subject: [PATCH 171/527] Add support for ContinuousUpdates Instead of requesting frame buffer updates we can, if the server supports it, continuously recieve frame buffer updates at a rate determined by the server. The server can use fencing messages and measure response times to determine how often it will continue to send updates. --- include/rfb.js | 67 +++++++++++++++++++++++++++++++++++++++++---- tests/test.rfb.js | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 71672ffe..e6597af2 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -57,7 +57,8 @@ var RFB; ['Cursor', -239 ], ['ExtendedDesktopSize', -308 ], ['xvp', -309 ], - ['Fence', -312 ] + ['Fence', -312 ], + ['ContinuousUpdates', -313 ] ]; this._encHandlers = {}; @@ -73,6 +74,9 @@ var RFB; this._supportsFence = false; + this._supportsContinuousUpdates = false; + this._enabledContinuousUpdates = false; + // Frame buffer update state this._FBU = { rects: 0, @@ -975,7 +979,7 @@ var RFB; RFB.messages.pixelFormat(this._sock, this._fb_Bpp, this._fb_depth, this._true_color); RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color); - RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + RFB.messages.fbUpdateRequests(this._sock, false, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); this._timing.pixels = 0; @@ -1051,7 +1055,13 @@ var RFB; var length = this._sock.rQshift8(); if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } - var payload = this._sock.rQshiftStr(length); // FIXME: 64 bytes max + + if (length > 64) { + Util.Warn("Bad payload length (" + length + ") in fence response"); + length = 64; + } + + var payload = this._sock.rQshiftStr(length); this._supportsFence = true; @@ -1116,7 +1126,10 @@ var RFB; case 0: // FramebufferUpdate var ret = this._framebufferUpdate(); if (ret) { - RFB.messages.fbUpdateRequests(this._sock, this._display.getCleanDirtyReset(), this._fb_width, this._fb_height); + RFB.messages.fbUpdateRequests(this._sock, + this._enabledContinuousUpdates, + this._display.getCleanDirtyReset(), + this._fb_width, this._fb_height); } return ret; @@ -1131,6 +1144,20 @@ var RFB; case 3: // ServerCutText return this._handle_server_cut_text(); + case 150: // EndOfContinuousUpdates + var first = !(this._supportsContinuousUpdates); + this._supportsContinuousUpdates = true; + this._enabledContinuousUpdates = false; + if (first) { + this._enabledContinuousUpdates = true; + this._updateContinuousUpdates(); + Util.Info("Enabling continuous updates."); + } else { + // FIXME: We need to send a framebufferupdaterequest here + // if we add support for turning off continuous updates + } + return true; + case 248: // ServerFence return this._handle_server_fence_msg(); @@ -1245,6 +1272,13 @@ var RFB; return true; // We finished this FBU }, + + _updateContinuousUpdates: function() { + if (!this._enabledContinuousUpdates) { return; } + + RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } }; Util.make_properties(RFB, [ @@ -1419,6 +1453,26 @@ var RFB; sock.flush(); }, + enableContinuousUpdates: function (sock, enable, x, y, width, height) { + var buff = sock._sQ; + var offset = sock._sQlen; + + buff[offset] = 150; // msg-type + buff[offset + 1] = enable; // enable-flag + + buff[offset + 2] = x >> 8; // x + buff[offset + 3] = x; + buff[offset + 4] = y >> 8; // y + buff[offset + 5] = y; + buff[offset + 6] = width >> 8; // width + buff[offset + 7] = width; + buff[offset + 8] = height >> 8; // height + buff[offset + 9] = height; + + sock._sQlen += 10; + sock.flush(); + }, + pixelFormat: function (sock, bpp, depth, true_color) { var buff = sock._sQ; var offset = sock._sQlen; @@ -1490,12 +1544,12 @@ var RFB; sock.flush(); }, - fbUpdateRequests: function (sock, cleanDirty, fb_width, fb_height) { + fbUpdateRequests: function (sock, onlyNonInc, cleanDirty, fb_width, fb_height) { var offsetIncrement = 0; var cb = cleanDirty.cleanBox; var w, h; - if (cb.w > 0 && cb.h > 0) { + if (!onlyNonInc && (cb.w > 0 && cb.h > 0)) { w = typeof cb.w === "undefined" ? fb_width : cb.w; h = typeof cb.h === "undefined" ? fb_height : cb.h; // Request incremental for clean box @@ -2102,6 +2156,7 @@ var RFB; this._display.resize(this._fb_width, this._fb_height); this._onFBResize(this, this._fb_width, this._fb_height); this._timing.fbu_rt_start = (new Date()).getTime(); + this._updateContinuousUpdates(); this._FBU.bytes = 0; this._FBU.rects -= 1; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index be6aa1b2..65ce5f88 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1197,6 +1197,33 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._sock).to.have.sent(expected_msg._sQ); }); + it('should only request non-incremental rects in continuous updates mode', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 120, h: 20 }, + dirtyBoxes: [ { x: 120, y: 0, w: 120, h: 20 } ] }; + + RFB.messages.fbUpdateRequest(expected_msg, false, 120, 0, 120, 20); + + client._enabledContinuousUpdates = true; + client._framebufferUpdate = function () { return true; }; + client._display.getCleanDirtyReset = function () { return expected_cdr; }; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send a request in continuous updates mode when clean', function () { + var expected_cdr = { cleanBox: { x: 0, y: 0, w: 240, h: 20 }, + dirtyBoxes: [] }; + + client._enabledContinuousUpdates = true; + client._framebufferUpdate = function () { return true; }; + client._display.getCleanDirtyReset = function () { return expected_cdr; }; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + it('should parse out information from a header before any actual data comes in', function () { client.set_onFBUReceive(sinon.spy()); var rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 0x02, encodingName: 'RRE' }; @@ -1730,6 +1757,49 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._sock).to.have.sent(expected_msg._sQ); }); + it('should enable continuous updates on first EndOfContinousUpdates', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; + + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 640, 20); + + expect(client._enabledContinuousUpdates).to.be.false; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.true; + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should disable continuous updates on subsequent EndOfContinousUpdates', function () { + client._enabledContinuousUpdates = true; + client._supportsContinuousUpdates = true; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.false; + }); + + it('should update continuous updates on resize', function () { + var expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: function() {}}; + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 90, 700); + + client._FBU.width = 450; + client._FBU.height = 160; + + client._encHandlers.handle_FB_resize(); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._enabledContinuousUpdates = true; + + client._FBU.width = 90; + client._FBU.height = 700; + + client._encHandlers.handle_FB_resize(); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + it('should fail on an unknown message type', function () { client._sock._websocket._receive_data(new Uint8Array([87])); expect(client._rfb_state).to.equal('failed'); From 12f4747ced89de7f37609d7740936f7a3a677d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Tu=E1=BA=A5n=20Anh?= Date: Fri, 26 Aug 2016 16:09:22 +0700 Subject: [PATCH 172/527] Removing unused import (#642) --- utils/json2graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/json2graph.py b/utils/json2graph.py index f9ae27d3..8992382f 100755 --- a/utils/json2graph.py +++ b/utils/json2graph.py @@ -7,7 +7,7 @@ Licensed under MPL-2.0 (see docs/LICENSE.MPL-2.0) ''' # a bar plot with errorbars -import sys, json, pprint +import sys, json import numpy as np import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties From c4df8ca92cd3bfcc669d03edca8cc050e0030801 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 26 Aug 2016 12:18:28 +0200 Subject: [PATCH 173/527] Remove bad trailing commas --- include/rfb.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/rfb.js b/include/rfb.js index 8d268b04..dd3ad8f2 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -153,7 +153,7 @@ var RFB; 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received - 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection + 'onXvpInit': function () { } // onXvpInit(version): XVP extensions active for this connection }); // main setup @@ -1297,7 +1297,7 @@ var RFB; ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received - ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection + ['onXvpInit', 'rw', 'func'] // onXvpInit(version): XVP extensions active for this connection ]); RFB.prototype.set_local_cursor = function (cursor) { From 9104d3b5672c1391fd40ff32e6bac7b5d3ed86a6 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 26 Aug 2016 12:19:30 +0200 Subject: [PATCH 174/527] Remove unnecessary debug logging --- include/rfb.js | 1 - 1 file changed, 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index dd3ad8f2..3e7ee7d2 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -1836,7 +1836,6 @@ var RFB; for (var i = 0; i < 4; i++) { if ((resetStreams >> i) & 1) { this._FBU.zlibs[i].reset(); - console.debug('RESET!'); Util.Info("Reset zlib stream " + i); } } From 4e0c36dda708628836dc6f5d68fc40d05c7716d9 Mon Sep 17 00:00:00 2001 From: samhed Date: Fri, 26 Aug 2016 12:20:51 +0200 Subject: [PATCH 175/527] Add missing return values --- include/rfb.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/rfb.js b/include/rfb.js index 3e7ee7d2..bc5555ab 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -249,6 +249,7 @@ var RFB; } this._updateState('connect'); + return true; }, disconnect: function () { @@ -274,6 +275,7 @@ var RFB; RFB.messages.keyEvent(this._sock, XK_Delete, 0); RFB.messages.keyEvent(this._sock, XK_Alt_L, 0); RFB.messages.keyEvent(this._sock, XK_Control_L, 0); + return true; }, xvpOp: function (ver, op) { @@ -307,6 +309,7 @@ var RFB; RFB.messages.keyEvent(this._sock, code, 1); RFB.messages.keyEvent(this._sock, code, 0); } + return true; }, clipboardPasteFrom: function (text) { @@ -822,7 +825,7 @@ var RFB; } } - this._fail("No supported sub-auth types!"); + return this._fail("No supported sub-auth types!"); }, _negotiate_authentication: function () { @@ -873,6 +876,8 @@ var RFB; return false; case 2: return this._fail("Too many auth attempts"); + default: + return this._fail("Unknown SecurityResult"); } }, @@ -989,6 +994,7 @@ var RFB; } else { this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name); } + return true; }, _init_msg: function () { @@ -1012,6 +1018,9 @@ var RFB; case 'ServerInitialisation': return this._negotiate_server_init(); + + default: + return this._fail("Unknown state: " + this._rfb_state); } }, From 99feba6ba8fee5b3a2b2dc99dc25e9179c560d31 Mon Sep 17 00:00:00 2001 From: Daniel Henrique Barboza Date: Fri, 26 Aug 2016 15:47:03 -0300 Subject: [PATCH 176/527] QEMU RFB extension - new file xtscancodes.js This new file contains the XT scancode mapping that the extension will use in rfb.js file. Signed-off-by: Daniel Henrique Barboza --- LICENSE.txt | 5 +- include/ui.js | 5 +- include/xtscancodes.js | 146 ++++++++++++++++++++++++++++++++++++++++ karma.conf.js | 1 + tests/input.html | 9 +-- tests/vnc_perf.html | 5 +- tests/vnc_playback.html | 5 +- vnc_auto.html | 5 +- 8 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 include/xtscancodes.js diff --git a/LICENSE.txt b/LICENSE.txt index f217929f..2ec4a642 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -17,6 +17,7 @@ is not limited to): include/util.js include/websock.js include/webutil.js + include/xtscancodes.js The HTML, CSS, font and images files that included with the noVNC source distibution (or repository) are not considered part of the @@ -45,7 +46,7 @@ the noVNC core library. Here is a list of those files and the original licenses (all MPL 2.0 compatible): include/base64.js : MPL 2.0 - + include/des.js : Various BSD style licenses include/chrome-app/tcp-stream.js @@ -53,7 +54,7 @@ licenses (all MPL 2.0 compatible): utils/websockify utils/websocket.py : LGPL 3 - + utils/inflator.partial.js include/inflator.js : MIT (for pako) diff --git a/include/ui.js b/include/ui.js index 03863634..d69a4f6d 100644 --- a/include/ui.js +++ b/include/ui.js @@ -18,8 +18,9 @@ var UI; // Load supporting scripts window.onscriptsload = function () { UI.load(); }; Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", - "keysymdef.js", "keyboard.js", "input.js", "display.js", - "rfb.js", "keysym.js", "inflator.js"]); + "keysymdef.js", "xtscancodes.js", "keyboard.js", + "input.js", "display.js", "rfb.js", "keysym.js", + "inflator.js"]); UI = { diff --git a/include/xtscancodes.js b/include/xtscancodes.js new file mode 100644 index 00000000..d19a0174 --- /dev/null +++ b/include/xtscancodes.js @@ -0,0 +1,146 @@ +var XtScancode = {}; +XtScancode["Escape"] = 0x0001; +XtScancode["Digit1"] = 0x0002; +XtScancode["Digit2"] = 0x0003; +XtScancode["Digit3"] = 0x0004; +XtScancode["Digit4"] = 0x0005; +XtScancode["Digit5"] = 0x0006; +XtScancode["Digit6"] = 0x0007; +XtScancode["Digit7"] = 0x0008; +XtScancode["Digit8"] = 0x0009; +XtScancode["Digit9"] = 0x000A; +XtScancode["Digit0"] = 0x000B; +XtScancode["Minus"] = 0x000C; +XtScancode["Equal"] = 0x000D; +XtScancode["Backspace"] = 0x000E; +XtScancode["Tab"] = 0x000F; +XtScancode["KeyQ"] = 0x0010; +XtScancode["KeyW"] = 0x0011; +XtScancode["KeyE"] = 0x0012; +XtScancode["KeyR"] = 0x0013; +XtScancode["KeyT"] = 0x0014; +XtScancode["KeyY"] = 0x0015; +XtScancode["KeyU"] = 0x0016; +XtScancode["KeyI"] = 0x0017; +XtScancode["KeyO"] = 0x0018; +XtScancode["KeyP"] = 0x0019; +XtScancode["BracketLeft"] = 0x001A; +XtScancode["BracketRight"] = 0x001B; +XtScancode["Enter"] = 0x001C; +XtScancode["ControlLeft"] = 0x001D; +XtScancode["KeyA"] = 0x001E; +XtScancode["KeyS"] = 0x001F; +XtScancode["KeyD"] = 0x0020; +XtScancode["KeyF"] = 0x0021; +XtScancode["KeyG"] = 0x0022; +XtScancode["KeyH"] = 0x0023; +XtScancode["KeyJ"] = 0x0024; +XtScancode["KeyK"] = 0x0025; +XtScancode["KeyL"] = 0x0026; +XtScancode["Semicolon"] = 0x0027; +XtScancode["Quote"] = 0x0028; +XtScancode["Backquote"] = 0x0029; +XtScancode["ShiftLeft"] = 0x002A; +XtScancode["Backslash"] = 0x002B; +XtScancode["KeyZ"] = 0x002C; +XtScancode["KeyX"] = 0x002D; +XtScancode["KeyC"] = 0x002E; +XtScancode["KeyV"] = 0x002F; +XtScancode["KeyB"] = 0x0030; +XtScancode["KeyN"] = 0x0031; +XtScancode["KeyM"] = 0x0032; +XtScancode["Comma"] = 0x0033; +XtScancode["Period"] = 0x0034; +XtScancode["Slash"] = 0x0035; +XtScancode["ShiftRight"] = 0x0036; +XtScancode["NumpadMultiply"] = 0x0037; +XtScancode["AltLeft"] = 0x0038; +XtScancode["Space"] = 0x0039; +XtScancode["CapsLock"] = 0x003A; +XtScancode["F1"] = 0x003B; +XtScancode["F2"] = 0x003C; +XtScancode["F3"] = 0x003D; +XtScancode["F4"] = 0x003E; +XtScancode["F5"] = 0x003F; +XtScancode["F6"] = 0x0040; +XtScancode["F7"] = 0x0041; +XtScancode["F8"] = 0x0042; +XtScancode["F9"] = 0x0043; +XtScancode["F10"] = 0x0044; +XtScancode["Pause"] = 0xE045; +XtScancode["ScrollLock"] = 0x0046; +XtScancode["Numpad7"] = 0x0047; +XtScancode["Numpad8"] = 0x0048; +XtScancode["Numpad9"] = 0x0049; +XtScancode["NumpadSubtract"] = 0x004A; +XtScancode["Numpad4"] = 0x004B; +XtScancode["Numpad5"] = 0x004C; +XtScancode["Numpad6"] = 0x004D; +XtScancode["NumpadAdd"] = 0x004E; +XtScancode["Numpad1"] = 0x004F; +XtScancode["Numpad2"] = 0x0050; +XtScancode["Numpad3"] = 0x0051; +XtScancode["Numpad0"] = 0x0052; +XtScancode["NumpadDecimal"] = 0x0053; +XtScancode["IntlBackslash"] = 0x0056; +XtScancode["F11"] = 0x0057; +XtScancode["F12"] = 0x0058; +XtScancode["IntlYen"] = 0x007D; +XtScancode["MediaTrackPrevious"] = 0xE010; +XtScancode["MediaTrackNext"] = 0xE019; +XtScancode["NumpadEnter"] = 0xE01C; +XtScancode["ControlRight"] = 0xE01D; +XtScancode["VolumeMute"] = 0xE020; +XtScancode["MediaPlayPause"] = 0xE022; +XtScancode["MediaStop"] = 0xE024; +XtScancode["VolumeDown"] = 0xE02E; +XtScancode["VolumeUp"] = 0xE030; +XtScancode["BrowserHome"] = 0xE032; +XtScancode["NumpadDivide"] = 0xE035; +XtScancode["PrintScreen"] = 0xE037; +XtScancode["AltRight"] = 0xE038; +XtScancode["NumLock"] = 0x0045; +XtScancode["Home"] = 0xE047; +XtScancode["ArrowUp"] = 0xE048; +XtScancode["PageUp"] = 0xE049; +XtScancode["ArrowLeft"] = 0xE04B; +XtScancode["ArrowRight"] = 0xE04D; +XtScancode["End"] = 0xE04F; +XtScancode["ArrowDown"] = 0xE050; +XtScancode["PageDown"] = 0xE051; +XtScancode["Insert"] = 0xE052; +XtScancode["Delete"] = 0xE053; +XtScancode["OSLeft"] = 0xE05B; +XtScancode["OSRight"] = 0xE05C; +XtScancode["ContextMenu"] = 0xE05D; +XtScancode["BrowserSearch"] = 0xE065; +XtScancode["BrowserFavorites"] = 0xE066; +XtScancode["BrowserRefresh"] = 0xE067; +XtScancode["BrowserStop"] = 0xE068; +XtScancode["BrowserForward"] = 0xE069; +XtScancode["BrowserBack"] = 0xE06A; +XtScancode["NumpadComma"] = 0x007E; +XtScancode["NumpadEqual"] = 0x0059; +XtScancode["F13"] = 0x0064; +XtScancode["F14"] = 0x0065; +XtScancode["F15"] = 0x0066; +XtScancode["F16"] = 0x0067; +XtScancode["F17"] = 0x0068; +XtScancode["F18"] = 0x0069; +XtScancode["F19"] = 0x006A; +XtScancode["F20"] = 0x006B; +XtScancode["F21"] = 0x006C; +XtScancode["F22"] = 0x006D; +XtScancode["F23"] = 0x006E; +XtScancode["F24"] = 0x0076; +XtScancode["KanaMode"] = 0x0070; +XtScancode["Lang2"] = 0x0071; +XtScancode["Lang1"] = 0x0072; +XtScancode["IntlRo"] = 0x0073; +XtScancode["Convert"] = 0x0079; +XtScancode["NonConvert"] = 0x007B; +XtScancode["LaunchApp2"] = 0xE021; +XtScancode["Power"] = 0xE05E; +XtScancode["LaunchApp1"] = 0xE06B; +XtScancode["LaunchMail"] = 0xE06C; +XtScancode["MediaSelect"] = 0xE06D; diff --git a/karma.conf.js b/karma.conf.js index 870b8551..2c49ffc7 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -115,6 +115,7 @@ module.exports = function(config) { 'include/base64.js', 'include/keysym.js', 'include/keysymdef.js', + 'include/xtscancodes.js', 'include/keyboard.js', 'include/input.js', 'include/websock.js', diff --git a/tests/input.html b/tests/input.html index 8416379b..301a7f8e 100644 --- a/tests/input.html +++ b/tests/input.html @@ -20,16 +20,17 @@ - + - - + + + --> - - - - - - - - - + + + + + + + + + - - + + diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index c4d2108e..168663db 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -33,24 +33,24 @@ - - + + diff --git a/utils/make-module-transform.js b/utils/make-module-transform.js new file mode 100644 index 00000000..bb48ae50 --- /dev/null +++ b/utils/make-module-transform.js @@ -0,0 +1,25 @@ +var through = require('through2'); + +var singleLineRe = /\/\* \[module\] ((.(?!\*\/))+) \*\//g; +var multiLineRe = /\/\* \[module\]\n(( * .+\n)+) \*\//g; + +var skipAsModule = /\/\* \[begin skip-as-module\] \*\/(.|\n)+\/\* \[end skip-as-module\] \*\//g; + +module.exports = function (file) { + var stream = through(function (buf, enc, next) { + var bufStr = buf.toString('utf8'); + bufStr = bufStr.replace(singleLineRe, "$1"); + bufStr = bufStr.replace(multiLineRe, function (match, mainLines) { + return mainLines.split(" * ").join(""); + }); + + bufStr = bufStr.replace(skipAsModule, ""); + + this.push(bufStr); + next(); + }); + + stream._is_make_module = true; + + return stream; +}; diff --git a/utils/use_require.js b/utils/use_require.js new file mode 100755 index 00000000..0c16db26 --- /dev/null +++ b/utils/use_require.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +var path = require('path'); +var program = require('commander'); +var fs = require('fs'); +var fse = require('fs-extra'); +var browserify = require('browserify'); + +var make_modules_transform = require('./make-module-transform'); +var babelify = require("babelify"); + + +program + .option('-b, --browserify', 'create a browserify bundled app') + .option('--as-require', 'output files using "require" instead of ES6 import and export') + .parse(process.argv); + +// the various important paths +var core_path = path.resolve(__dirname, '..', 'core'); +var app_path = path.resolve(__dirname, '..', 'app'); +var out_dir_base = path.resolve(__dirname, '..', 'build'); +var lib_dir_base = path.resolve(__dirname, '..', 'lib'); + +var make_browserify = function (src_files, opts) { + // change to the root noVNC directory + process.chdir(path.resolve(__dirname, '..')); + + var b = browserify(src_files, opts); + + // register the transforms + b.transform(make_modules_transform); + b.transform(babelify, + { plugins: ["add-module-exports", "transform-es2015-modules-commonjs"] }); + + return b; +}; + +var make_full_app = function () { + // make sure the output directory exists + fse.ensureDir(out_dir_base); + + // actually bundle the files into a browserified bundled + var ui_file = path.join(app_path, 'ui.js'); + var b = make_browserify(ui_file, {}); + var app_file = path.join(out_dir_base, 'app.js'); + b.bundle().pipe(fs.createWriteStream(app_file)); + + // copy over app-related resources (images, styles, etc) + var src_dir_app = path.join(__dirname, '..', 'app'); + fs.readdir(src_dir_app, function (err, files) { + if (err) { throw err; } + + files.forEach(function (src_file) { + var src_file_path = path.resolve(src_dir_app, src_file); + var out_file_path = path.resolve(out_dir_base, src_file); + var ext = path.extname(src_file); + if (ext === '.js' || ext === '.html') return; + fse.copy(src_file_path, out_file_path, function (err) { + if (err) { throw err; } + console.log("Copied file(s) from " + src_file_path + " to " + out_file_path); + }); + }); + }); + + // write out the modified vnc.html file that works with the bundle + var src_html_path = path.resolve(__dirname, '..', 'vnc.html'); + var out_html_path = path.resolve(out_dir_base, 'vnc.html'); + fs.readFile(src_html_path, function (err, contents_raw) { + if (err) { throw err; } + + var contents = contents_raw.toString(); + contents = contents.replace(/="app\//g, '="'); + + var start_marker = '\n'; + var end_marker = ''; + var start_ind = contents.indexOf(start_marker) + start_marker.length; + var end_ind = contents.indexOf(end_marker, start_ind); + + contents = contents.slice(0, start_ind) + '\n' + contents.slice(end_ind); + + fs.writeFile(out_html_path, contents, function (err) { + if (err) { throw err; } + console.log("Wrote " + out_html_path); + }); + }); +}; + +var make_lib_files = function (use_require) { + // make sure the output directory exists + fse.ensureDir(lib_dir_base); + + var through = require('through2'); + + var deps = {}; + var rfb_file = path.join(core_path, 'rfb.js'); + var b = make_browserify(rfb_file, {}); + b.on('transform', function (tr, file) { + if (tr._is_make_module) { + var new_path = path.join(lib_dir_base, path.basename(file)); + console.log("Writing " + new_path) + var fileStream = fs.createWriteStream(new_path); + + if (use_require) { + var babelificate = babelify(file, + { plugins: ["add-module-exports", "transform-es2015-modules-commonjs"] }); + tr.pipe(babelificate); + tr = babelificate; + } + tr.pipe(fileStream); + } + }); + + b.bundle(); +}; + +if (program.browserify) { + make_full_app(); +} else { + make_lib_files(program.asRequire); +} diff --git a/vnc.html b/vnc.html index 0b653065..bfc12017 100644 --- a/vnc.html +++ b/vnc.html @@ -27,18 +27,18 @@ - + - + - - - + + +
    -
    - - - - -
    - - - - - - -
    @@ -218,8 +218,10 @@
    - - + + + + diff --git a/vnc_auto.html b/vnc_auto.html index 597028e7..6fa3f253 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -27,22 +27,22 @@ - + - + - + - + @@ -77,10 +77,11 @@ "use strict"; // Load supporting scripts - Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", - "keysymdef.js", "xtscancodes.js", "keyboard.js", - "input.js", "display.js", "inflator.js", "rfb.js", - "keysym.js"]); + Util.load_scripts({ + 'core': ["base64.js", "websock.js", "des.js", "keysymdef.js", + "xtscancodes.js", "keyboard.js", "input.js", "display.js", + "inflator.js", "rfb.js", "keysym.js"], + 'app': ["webutil.js"]}); var rfb; var resizeTimeout; @@ -90,7 +91,7 @@ if (WebUtil.getConfigVar('resize', false)) { var innerW = window.innerWidth; var innerH = window.innerHeight; - var controlbarH = $D('noVNC_status_bar').offsetHeight; + var controlbarH = document.getElementById('noVNC_status_bar').offsetHeight; var padding = 5; if (innerW !== undefined && innerH !== undefined) rfb.requestDesktopSize(innerW, innerH - controlbarH - padding); @@ -107,11 +108,11 @@ msg += 'Password Required: '; msg += ''; msg += '<\/form>'; - $D('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); - $D('noVNC_status').innerHTML = msg; + document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); + document.getElementById('noVNC_status').innerHTML = msg; } function setPassword() { - rfb.sendPassword($D('password_input').value); + rfb.sendPassword(document.getElementById('password_input').value); return false; } function sendCtrlAltDel() { @@ -132,9 +133,9 @@ } function updateState(rfb, state, oldstate, msg) { var s, sb, cad, level; - s = $D('noVNC_status'); - sb = $D('noVNC_status_bar'); - cad = $D('sendCtrlAltDelButton'); + s = document.getElementById('noVNC_status'); + sb = document.getElementById('noVNC_status_bar'); + cad = document.getElementById('sendCtrlAltDelButton'); switch (state) { case 'failed': level = "error"; break; case 'fatal': level = "error"; break; @@ -169,7 +170,7 @@ function xvpInit(ver) { var xvpbuttons; - xvpbuttons = $D('noVNC_xvp_buttons'); + xvpbuttons = document.getElementById('noVNC_xvp_buttons'); if (ver >= 1) { xvpbuttons.style.display = 'inline'; } else { @@ -180,11 +181,11 @@ window.onscriptsload = function () { var host, port, password, path, token; - $D('sendCtrlAltDelButton').style.display = "inline"; - $D('sendCtrlAltDelButton').onclick = sendCtrlAltDel; - $D('xvpShutdownButton').onclick = xvpShutdown; - $D('xvpRebootButton').onclick = xvpReboot; - $D('xvpResetButton').onclick = xvpReset; + document.getElementById('sendCtrlAltDelButton').style.display = "inline"; + document.getElementById('sendCtrlAltDelButton').onclick = sendCtrlAltDel; + document.getElementById('xvpShutdownButton').onclick = xvpShutdown; + document.getElementById('xvpRebootButton').onclick = xvpReboot; + document.getElementById('xvpResetButton').onclick = xvpReset; WebUtil.init_logging(WebUtil.getConfigVar('logging', 'warn')); document.title = unescape(WebUtil.getConfigVar('title', 'noVNC')); @@ -223,7 +224,7 @@ } try { - rfb = new RFB({'target': $D('noVNC_canvas'), + rfb = new RFB({'target': document.getElementById('noVNC_canvas'), 'encrypt': WebUtil.getConfigVar('encrypt', (window.location.protocol === "https:")), 'repeaterID': WebUtil.getConfigVar('repeaterID', ''), From a62b1b352a976a57354761a229f061b8c462e51c Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 14 Sep 2016 13:53:13 -0400 Subject: [PATCH 184/527] Clean up unused files in tests There were quite a few old/irrelevant files in `tests/`. This commit removes them. --- tests/arrays.html | 39 ---- tests/arrays.js | 375 -------------------------------------- tests/base64.html | 91 --------- tests/base64.js | 12 -- tests/browser.js | 134 -------------- tests/canvas.html | 148 --------------- tests/cursor.html | 135 -------------- tests/face.png | Bin 2303 -> 0 bytes tests/face.png.js | 1 - tests/keyboard-tests.html | 29 --- tests/stats.js | 53 ------ tests/viewport.css | 43 ----- tests/viewport.html | 203 --------------------- 13 files changed, 1263 deletions(-) delete mode 100644 tests/arrays.html delete mode 100644 tests/arrays.js delete mode 100644 tests/base64.html delete mode 100644 tests/base64.js delete mode 100644 tests/browser.js delete mode 100644 tests/canvas.html delete mode 100644 tests/cursor.html delete mode 100644 tests/face.png delete mode 100644 tests/face.png.js delete mode 100644 tests/keyboard-tests.html delete mode 100644 tests/stats.js delete mode 100644 tests/viewport.css delete mode 100644 tests/viewport.html diff --git a/tests/arrays.html b/tests/arrays.html deleted file mode 100644 index 257df261..00000000 --- a/tests/arrays.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - Javascript Arrays Performance Test - - - - - - - - -

    Javascript Arrays Performance Test

    - Iterations:   - Array Size: *1024  - -   - -

    - Results:
    - -
    - - - - - - diff --git a/tests/arrays.js b/tests/arrays.js deleted file mode 100644 index 69da7fbb..00000000 --- a/tests/arrays.js +++ /dev/null @@ -1,375 +0,0 @@ -/* - * Javascript binary array performance tests - * Copyright (C) 2012 Joel Martin - * Licensed under MPL 2.0 (see LICENSE.txt) - */ - -var ctx, i, j, randlist, - new_normal, new_imageData, new_arrayBuffer, - browser = Browser.browser + " " + - Browser.version + " on " + - Browser.OS, - do_imageData = false, - do_arrayBuffer = false, - conf = { - 'create_cnt' : 2000, - 'read_cnt' : 5000000, - 'write_cnt' : 5000000, - 'iterations' : 0, - 'order_l1' : [browser], - 'order_l2' : ['normal', - 'imageData', - 'arrayBuffer'], - 'order_l3' : ['create', - 'sequentialRead', - 'randomRead', - 'sequentialWrite'] - }, - stats = {}, - testFunc = {}, - iteration, arraySize; - -var newline = "\n"; -if (Util.Engine.trident) { - var newline = "
    \n"; -} -function message(str) { - //console.log(str); - cell = $D('messages'); - cell.innerHTML += str + newline; - cell.scrollTop = cell.scrollHeight; -} - -function vmessage(str) { - if (verbose) { - message(str); - } else { - console.log(str); - } -} - -new_normal = function() { - var arr = [], i; - for (i = 0; i < arraySize; i++) { - arr[i] = 0; - } - return arr; -} - -/* Will be overridden with real function */ -new_imageData = function() { - throw("imageData not supported"); -}; - -new_imageData_createImageData = function() { - var imageData = ctx.createImageData(1024/4, arraySize / 1024); - return imageData.data; -}; - -new_imageData_getImageData = function() { - var imageData = ctx.getImageData(0, 0, 1024/4, arraySize / 1024), - arr = imageData.data; - for (i = 0; i < arraySize; i++) { - arr[i] = 0; - } - return arr; -}; - -new_arrayBuffer = function() { - var arr = new ArrayBuffer(arraySize); - return new Uint8Array(arr); -} - -function init_randlist() { - randlist = []; - for (var i=0; i < arraySize; i++) { - randlist[i] = parseInt(Math.random() * 256, 10); - } -} -function copy_randlist(arr) { - for (var i=0; i < arraySize; i++) { - arr[i] = randlist[i]; - } -} - -function begin() { - var i, j; - conf.iterations = parseInt($D('iterations').value, 10); - arraySize = parseInt($D('arraySize').value, 10) * 1024; - - init_randlist(); - - // TODO: randomize test_list - - stats = {}; - for (i = 0; i < conf.order_l2.length; i++) { - stats[conf.order_l2[i]] = {}; - for (j = 0; j < conf.order_l3.length; j++) { - stats[conf.order_l2[i]][conf.order_l3[j]] = []; - } - } - - $D('startButton').value = "Running"; - $D('startButton').disabled = true; - - message("running " + conf.iterations + " test iterations"); - iteration = 1; - setTimeout(run_next_iteration, 250); -} - -function finish() { - var totalTime, arrayType, testType, times; - message("tests finished"); - - for (j = 0; j < conf.order_l3.length; j++) { - testType = conf.order_l3[j]; - message("Test '" + testType + "'"); - for (i = 0; i < conf.order_l2.length; i++) { - arrayType = conf.order_l2[i]; - message(" Array Type '" + arrayType); - times = stats[arrayType][testType]; - message(" Average : " + times.mean() + "ms" + - " (Total: " + times.sum() + "ms)"); - message(" Min/Max : " + times.min() + "ms/" + - times.max() + "ms"); - message(" StdDev : " + times.stdDev() + "ms"); - } - } - - vmessage("array_chart.py JSON data:"); - chart_data = {'conf' : conf, 'stats' : { } }; - chart_data.stats[browser] = stats; - chart_data.stats['next_browser'] = {}; - vmessage(JSON.stringify(chart_data, null, 2)); - - $D('startButton').disabled = false; - $D('startButton').value = "Run Tests"; -} - -function run_next_iteration() { - var arrayType, testType, deltaTime; - - for (i = 0; i < conf.order_l2.length; i++) { - arrayType = conf.order_l2[i]; - if (arrayType === 'imageData' && (!do_imageData)) { - continue; - } - if (arrayType === 'arrayBuffer' && (!do_arrayBuffer)) { - continue; - } - for (j = 0; j < conf.order_l3.length; j++) { - testType = conf.order_l3[j]; - - deltaTime = testFunc[arrayType + "_" + testType](); - - stats[arrayType][testType].push(deltaTime); - vmessage("test " + (arrayType + "_" + testType) + - " time: " + (deltaTime) + "ms"); - } - } - - message("finished test iteration " + iteration); - if (iteration >= conf.iterations) { - setTimeout(finish, 1); - return; - } - iteration++; - setTimeout(run_next_iteration, 1); -} - -/* - * Test functions - */ - -testFunc["normal_create"] = function() { - var cnt, arrNormal, startTime, endTime; - vmessage("create normal array " + conf.create_cnt + "x, initialized to 0"); - - startTime = (new Date()).getTime(); - for (cnt = 0; cnt < conf.create_cnt; cnt++) { - arrNormal = new_normal(); - } - endTime = (new Date()).getTime(); - - return endTime - startTime; -}; - -testFunc["imageData_create"] = function() { - var cnt, arrImage, startTime, endTime; - vmessage("create imageData array " + conf.create_cnt + "x, initialized to 0"); - - startTime = (new Date()).getTime(); - for (cnt = 0; cnt < conf.create_cnt; cnt++) { - arrImage = new_imageData(); - } - endTime = (new Date()).getTime(); - - if (arrImage[103] !== 0) { - message("Initialization failed, arrImage[103] is: " + arrImage[103]); - throw("Initialization failed, arrImage[103] is: " + arrImage[103]); - } - return endTime - startTime; -}; - -testFunc["arrayBuffer_create"] = function() { - var cnt, arrBuffer, startTime, endTime; - vmessage("create arrayBuffer array " + conf.create_cnt + "x, initialized to 0"); - - startTime = (new Date()).getTime(); - for (cnt = 0; cnt < conf.create_cnt; cnt++) { - arrBuffer = new_arrayBuffer(); - } - endTime = (new Date()).getTime(); - - if (arrBuffer[103] !== 0) { - message("Initialization failed, arrBuffer[103] is: " + arrBuffer[103]); - throw("Initialization failed, arrBuffer[103] is: " + arrBuffer[103]); - } - return endTime - startTime; -}; - -function test_sequentialRead(arr) { - var i, j, cnt, startTime, endTime; - /* Initialize the array */ - copy_randlist(arr); - - startTime = (new Date()).getTime(); - i = 0; - j = 0; - for (cnt = 0; cnt < conf.read_cnt; cnt++) { - j = arr[i]; - i++; - if (i >= arraySize) { - i = 0; - } - } - endTime = (new Date()).getTime(); - - return endTime - startTime; -} - -function test_randomRead(arr) { - var i, cnt, startTime, endTime; - /* Initialize the array */ - copy_randlist(arr); // used as jumplist - - startTime = (new Date()).getTime(); - i = 0; - for (cnt = 0; cnt < conf.read_cnt; cnt++) { - i = (arr[i] + cnt) % arraySize; - } - endTime = (new Date()).getTime(); - - return endTime - startTime; -} - -function test_sequentialWrite(arr) { - var i, cnt, startTime, endTime; - /* Initialize the array */ - copy_randlist(arr); - - startTime = (new Date()).getTime(); - i = 0; - for (cnt = 0; cnt < conf.write_cnt; cnt++) { - arr[i] = (cnt % 256); - i++; - if (i >= arraySize) { - i = 0; - } - } - endTime = (new Date()).getTime(); - - return endTime - startTime; -} - -/* Sequential Read Tests */ -testFunc["normal_sequentialRead"] = function() { - vmessage("read normal array " + conf.read_cnt + "x"); - return test_sequentialRead(new_normal()); -}; - -testFunc["imageData_sequentialRead"] = function() { - vmessage("read imageData array " + conf.read_cnt + "x"); - return test_sequentialRead(new_imageData()); -}; - -testFunc["arrayBuffer_sequentialRead"] = function() { - vmessage("read arrayBuffer array " + conf.read_cnt + "x"); - return test_sequentialRead(new_arrayBuffer()); -}; - - -/* Random Read Tests */ -testFunc["normal_randomRead"] = function() { - vmessage("read normal array " + conf.read_cnt + "x"); - return test_randomRead(new_normal()); -}; - -testFunc["imageData_randomRead"] = function() { - vmessage("read imageData array " + conf.read_cnt + "x"); - return test_randomRead(new_imageData()); -}; - -testFunc["arrayBuffer_randomRead"] = function() { - vmessage("read arrayBuffer array " + conf.read_cnt + "x"); - return test_randomRead(new_arrayBuffer()); -}; - - -/* Sequential Write Tests */ -testFunc["normal_sequentialWrite"] = function() { - vmessage("write normal array " + conf.write_cnt + "x"); - return test_sequentialWrite(new_normal()); -}; - -testFunc["imageData_sequentialWrite"] = function() { - vmessage("write imageData array " + conf.write_cnt + "x"); - return test_sequentialWrite(new_imageData()); -}; - -testFunc["arrayBuffer_sequentialWrite"] = function() { - vmessage("write arrayBuffer array " + conf.write_cnt + "x"); - return test_sequentialWrite(new_arrayBuffer()); -}; - -init = function() { - vmessage(">> init"); - - $D('iterations').value = 10; - $D('arraySize').value = 10; - arraySize = parseInt($D('arraySize').value, 10) * 1024; - - message("Browser: " + browser); - - /* Determine browser binary array support */ - try { - ctx = $D('canvas').getContext('2d'); - new_imageData = new_imageData_createImageData; - new_imageData(); - do_imageData = true; - } catch (exc) { - vmessage("createImageData not supported: " + exc); - try { - ctx = $D('canvas').getContext('2d'); - new_imageData = new_imageData_getImageData; - blah = new_imageData(); - do_imageData = true; - } catch (exc) { - vmessage("getImageData not supported: " + exc); - } - } - if (! do_imageData) { - message("imageData arrays not supported"); - } - - try { - new_arrayBuffer(); - do_arrayBuffer = true; - } catch (exc) { - vmessage("Typed Arrays not supported: " + exc); - } - if (! do_arrayBuffer) { - message("Typed Arrays (ArrayBuffers) not suppoted"); - } - vmessage("<< init"); -} diff --git a/tests/base64.html b/tests/base64.html deleted file mode 100644 index dc45fdd5..00000000 --- a/tests/base64.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - Native Base64 Tests - - - - - -

    Native Base64 Tests

    - -
    - Messages:
    - - -
    - - - diff --git a/tests/base64.js b/tests/base64.js deleted file mode 100644 index 6ade00a3..00000000 --- a/tests/base64.js +++ /dev/null @@ -1,12 +0,0 @@ -// The following results in 'hello [MANGLED]' -// -// Filed as https://github.com/ry/node/issues/issue/402 - -var sys = require("sys"), - buf = new Buffer(1024), len, - str1 = "aGVsbG8g", // 'hello ' - str2 = "d29ybGQ=", // 'world' - -len = buf.write(str1, 0, 'base64'); -len += buf.write(str2, len, 'base64'); -sys.log("decoded result: " + buf.toString('binary', 0, len)); diff --git a/tests/browser.js b/tests/browser.js deleted file mode 100644 index 7cf8279c..00000000 --- a/tests/browser.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * From: - * http://www.quirksmode.org/js/detect.html - */ - -var Browser = { - init: function () { - this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; - this.version = this.searchVersion(navigator.userAgent) - || this.searchVersion(navigator.appVersion) - || "an unknown version"; - this.majorVersion = this.searchMajorVersion(navigator.userAgent) - || this.searchMajorVersion(navigator.appVersion) - || "an unknown version"; - this.fullVersion = this.searchFullVersion(navigator.userAgent) - || this.searchFullVersion(navigator.appVersion) - || "an unknown version"; - this.OS = this.searchString(this.dataOS) || "an unknown OS"; - }, - searchString: function (data) { - for (var i=0;i - - - Canvas Performance Test - - - - - - - - - Iterations:   - - Width:   - Height:   - -   - -

    - - Canvas (should see three squares and two happy faces):
    - - Canvas not supported. - - -
    - Results:
    - - - - - diff --git a/tests/cursor.html b/tests/cursor.html deleted file mode 100644 index 91e621b3..00000000 --- a/tests/cursor.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - Cursor Change test - - - - - - - - -

    Roll over the buttons to test cursors

    -
    - - - -
    -
    -
    - Debug:
    - -
    -
    - - Canvas not supported. - - - - - diff --git a/tests/face.png b/tests/face.png deleted file mode 100644 index 74c30d82f9736beffef5db259b3e40bb6de4ea2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2303 zcmVDcqz&LP?>7h(O|oIKntKx%ray+TL^a+~)T0!`f>n3cTgh|6ym( z?DyOM%r~>k`rm(L+4(DM%f`8sHkw5f14ij{imA#v*WJ|Q00hwIJr+{BoC65U{pI4W zU}}#{ib*uVz}!qTPqpNmuB>m}lqp;|fAU$yL}^e;Q60pI*t4&1c<9(`|Ju24|G~27 zSu((R_><5N$zql^I-kpZ zxo72fZ~prFbr*Ln>Byv0XOf=!`#-n6^y-54_LC<^95UH_mW+wwC=H*dHC zfI|R*;zVqF>9sA7|5}LTvB!Vsx+y6{6er9w%vO%8vxXRB+;yMd{=%EPb`#NtYp-%s z?wYlidY&JIfvN;VOc`SgrP8T$t8I>iF=`wHvRWC>A@;>qrt&O#}eAoZo%xO_yA}?zTJc5judC%A~baBA-Me zF2r#V1p#RlRD2k9LH8npY{z6l-EsE? zJ*)M-x6P7>0DNonSO2(bMgPIR6|25f_R6QmN2aGrNf?A-XpAvhDJ3hF%3q(|N=QJW z_u;#5z5Rj4=2kbACZn|079&FpaGIK$8|v#zr4qC3d++#8V|^Y(EGZ325m!4aPE3C9 zj=K(ha&*DM#TTvYUcF|0ds|DPPzQin$YwKNxpYlyYrSP@P;68%Q5xC1_e0;CG$w=z zeZMqO9No8n;=O(T)O5J8z46b#-}2SV*VfJxYUio7rST8Frsuldjw8V@1i%;r7+?Wx z_q%&;zU7`rANlE~4QoO2-SaJK*B)rQ8VEl)fRXpq|e(t8a}ZCjpv8Kwdg z0TN(f7QjfIOy?Fe%S4sInz=LoitxsFwRe9T`Xpe1B9KFjgE4@EIS2M1jKXkyYy>cX zIM6o`g~|BnF@Vi}>FNS71{nESOULr^*c<8Y^XGc1DVW+**Senc7*PZh*4hFTNROO6 zas2o(Arg?BHx#p;n?9JD$IWhb?Ac-PSa{b1Ojt@Km6MzDcs>TG+WCTbODIzo9*fB;+kyHn6x>3ss zn^~X)WX~qk097`dv7-bOgOX|yC4ijWPGv|KBLG*4Y3>$k=BM>!r&?7sYNu^b1;msH zoK!PNf$`aBnHf_`KoKY=2#^2?K!Bu@0VpC9gXL8+<)+)_?Gph+)yfqv^NM?6)f2@q zQQyv9Ehj;h+44dD>~aO5h$yP^s?BK?^u4pon9%5mIh}rKI9GpF&D@&*b6eN4Vf@HW zW;KE$qA-(f3MLQ3RA4+9f2PQXslWte0#F1~CB>k`pDLzvd81=vLW?h(939HHY&;|7 zGsQE@lnmp_0JG{rLC30%gZrNIihVP%8AgSuLKJ|i>7QxKE1eiRxPMW1kI@xopO;id zjIw!VNzI&vwW2S7wZO0V?3EN@$R^}`hJft4#4Cx#AecQbX33s>d}?WtU&a>2k+SxsGkmkfP8BAe9nAB#BI-(csv9J=Z>v&Nw6wgXI`s2IqWc zH-qJ2)iG8Fs{k07X);qJPpI(UGI&$P!&dq-sTx21;MM$jTT_{ZsT6lzmpcvs7LLu4 zq>zb_Qp(8xSe1UW>gxM5xh#nfK^m}O6`0)$mIcd!fs9Y8q>}+HhO{mlo!Hq;B5^+b zV29h?l8i7W zHYPGUG&*E_xt(4&J$Crmq5n87zepEWyDqO%T$fvxHA@QoY7mvyQb-}CluAe$hbMID zx9MD}YxPZ;x&=laC3Tc^*_dfE5^xA<<}7A>N$eMo_U}$Y!&`b&*+nkroI5V(+~w6x z0>FZ?{wVNjsjtGMQc^0VlvFAyzhnHrrBd0}&Q2hq_c5KJCYp`m#=aUXamD5ObJf(~_N^7l+Hku681c23KG260j+j4B%vMuhM$(Xxg z&;I|PG0r(STq0xWG!h5^!)YYTVl}{74V}+@QX_)d7PBquiza;mp++)VGluGdoI5eb Z{tpYglKBOBdFlWF002ovPDHLkV1m%%QeprA diff --git a/tests/face.png.js b/tests/face.png.js deleted file mode 100644 index e0b5d6ce..00000000 --- a/tests/face.png.js +++ /dev/null @@ -1 +0,0 @@ -var face64 = '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'; diff --git a/tests/keyboard-tests.html b/tests/keyboard-tests.html deleted file mode 100644 index a30aa6e0..00000000 --- a/tests/keyboard-tests.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - Mocha Tests - - - - -
    - - - - - - - - - - diff --git a/tests/stats.js b/tests/stats.js deleted file mode 100644 index cd3011cb..00000000 --- a/tests/stats.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Define some useful statistical functions on arrays of numbers - */ - -Array.prototype.sum = function() { - var i, sum = 0; - for (i = 0; i < this.length; i++) { - sum += this[i]; - } - return sum; -} - -Array.prototype.max = function() { - return Math.max.apply(null, this); -} - -Array.prototype.min = function() { - return Math.min.apply(null, this); -} - -Array.prototype.mean = function() { - return this.sum() / this.length; -} -Array.prototype.average = Array.prototype.mean; - -Array.prototype.median = function() { - var sorted = this.sort( function(a,b) { return a-b; }), - len = sorted.length; - if (len % 2) { - return sorted[Math.floor(len / 2)]; // Odd - } else { - return (sorted[len/2 - 1] + sorted[len/2]) / 2; // Even - } -} - -Array.prototype.stdDev = function(sample) { - var i, sumSqr = 0, mean = this.mean(), N; - - if (sample) { - // Population correction if this is a sample - N = this.length - 1; - } else { - // Standard deviation of just the array - N = this.length; - } - - for (i = 0; i < this.length; i++) { - sumSqr += Math.pow(this[i] - mean, 2); - } - - return Math.sqrt(sumSqr / N); -} - diff --git a/tests/viewport.css b/tests/viewport.css deleted file mode 100644 index 86f65ff0..00000000 --- a/tests/viewport.css +++ /dev/null @@ -1,43 +0,0 @@ -html,body { - margin: 0px; - padding: 0px; - width: 100%; - height: 100%; -} - -.flex-layout { - width: 100%; - height: 100%; - - display: box; - display: -webkit-box; - display: -moz-box; - display: -ms-box; - - box-orient: vertical; - -webkit-box-orient: vertical; - -moz-box-orient: vertical; - -ms-box-orient: vertical; - - box-align: stretch; - -webkit-box-align: stretch; - -moz-box-align: stretch; - -ms-box-align: stretch; -} -.flex-box { - box-flex: 1; - -webkit-box-flex: 1; - -moz-box-flex: 1; - -ms-box-flex: 1; -} - -.container { - margin: 0px; - padding: 0px; -} - -.canvas { - position: absolute; - border-style: dotted; - border-width: 1px; -} diff --git a/tests/viewport.html b/tests/viewport.html deleted file mode 100644 index 374d8b15..00000000 --- a/tests/viewport.html +++ /dev/null @@ -1,203 +0,0 @@ - - - Viewport Test - - - - - -
    -
    - Canvas: - -
    -
    -
    - - Canvas not supported. - -
    -
    -
    -
    - Results:
    - -
    -
    - - - - - - - - - - - - From 72bdd06ea2d3a2b7f102ac6e068a205433e52a38 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 3 Sep 2016 13:39:12 -0400 Subject: [PATCH 185/527] Clean up Util This commit removes unused code from Util, and moves the script-loading functionality to WebUtil. --- app/ui.js | 5 +- app/webutil.js | 69 ++++++++++++++++++ core/util.js | 158 ---------------------------------------- tests/input.html | 22 +++--- tests/vnc_perf.html | 2 +- tests/vnc_playback.html | 2 +- vnc.html | 1 + vnc_auto.html | 3 +- 8 files changed, 87 insertions(+), 175 deletions(-) diff --git a/app/ui.js b/app/ui.js index c8251fbe..45aa2ac3 100644 --- a/app/ui.js +++ b/app/ui.js @@ -25,11 +25,10 @@ var UI; /* [begin skip-as-module] */ // Load supporting scripts - Util.load_scripts( + WebUtil.load_scripts( {'core': ["base64.js", "websock.js", "des.js", "keysymdef.js", "xtscancodes.js", "keyboard.js", "input.js", "display.js", - "inflator.js", "rfb.js", "keysym.js"], - '.': ["webutil.js"]}); + "inflator.js", "rfb.js", "keysym.js"]}); window.onscriptsload = function () { UI.load(); }; /* [end skip-as-module] */ diff --git a/app/webutil.js b/app/webutil.js index f5f3077d..7f234dbf 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -278,4 +278,73 @@ WebUtil.injectParamIfMissing = function (path, param, value) { } }; +// Dynamically load scripts without using document.write() +// Reference: http://unixpapa.com/js/dyna.html +// +// Handles the case where load_scripts is invoked from a script that +// itself is loaded via load_scripts. Once all scripts are loaded the +// window.onscriptsloaded handler is called (if set). +WebUtil.get_include_uri = function (root_dir) { + return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI + root_dir + '/' : root_dir + '/'; +}; +WebUtil._loading_scripts = []; +WebUtil._pending_scripts = []; +WebUtil.load_scripts = function (files_by_dir) { + "use strict"; + var head = document.getElementsByTagName('head')[0], script, + ls = WebUtil._loading_scripts, ps = WebUtil._pending_scripts; + + var loadFunc = function (e) { + while (ls.length > 0 && (ls[0].readyState === 'loaded' || + ls[0].readyState === 'complete')) { + // For IE, append the script to trigger execution + var s = ls.shift(); + //console.log("loaded script: " + s.src); + head.appendChild(s); + } + if (!this.readyState || + (Util.Engine.presto && this.readyState === 'loaded') || + this.readyState === 'complete') { + if (ps.indexOf(this) >= 0) { + this.onload = this.onreadystatechange = null; + //console.log("completed script: " + this.src); + ps.splice(ps.indexOf(this), 1); + + // Call window.onscriptsload after last script loads + if (ps.length === 0 && window.onscriptsload) { + window.onscriptsload(); + } + } + } + }; + + var root_dirs = Object.keys(files_by_dir); + + for (var d = 0; d < root_dirs.length; d++) { + var root_dir = root_dirs[d]; + var files = files_by_dir[root_dir]; + + for (var f = 0; f < files.length; f++) { + script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = WebUtil.get_include_uri(root_dir) + files[f]; + //console.log("loading script: " + script.src); + script.onload = script.onreadystatechange = loadFunc; + // In-order script execution tricks + if (Util.Engine.trident) { + // For IE wait until readyState is 'loaded' before + // appending it which will trigger execution + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + ls.push(script); + } else { + // For webkit and firefox set async=false and append now + // https://developer.mozilla.org/en-US/docs/HTML/Element/script + script.async = false; + head.appendChild(script); + } + ps.push(script); + } + } +}; + /* [module] export default WebUtil; */ diff --git a/core/util.js b/core/util.js index 1b8744ba..3a2d241b 100644 --- a/core/util.js +++ b/core/util.js @@ -41,93 +41,6 @@ addFunc(Array, 'push32', function (num) { num & 0xFF); }); -// IE does not support map (even in IE9) -//This prototype is provided by the Mozilla foundation and -//is distributed under the MIT license. -//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -addFunc(Array, 'map', function (fun /*, thisp*/) { - "use strict"; - var len = this.length; - if (typeof fun != "function") { - throw new TypeError(); - } - - var res = new Array(len); - var thisp = arguments[1]; - for (var i = 0; i < len; i++) { - if (i in this) { - res[i] = fun.call(thisp, this[i], i, this); - } - } - - return res; -}); - -// IE <9 does not support indexOf -//This prototype is provided by the Mozilla foundation and -//is distributed under the MIT license. -//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -addFunc(Array, 'indexOf', function (elt /*, from*/) { - "use strict"; - var len = this.length >>> 0; - - var from = Number(arguments[1]) || 0; - from = (from < 0) ? Math.ceil(from) : Math.floor(from); - if (from < 0) { - from += len; - } - - for (; from < len; from++) { - if (from in this && - this[from] === elt) { - return from; - } - } - return -1; -}); - -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys -if (!Object.keys) { - Object.keys = (function () { - 'use strict'; - var hasOwnProperty = Object.prototype.hasOwnProperty, - hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), - dontEnums = [ - 'toString', - 'toLocaleString', - 'valueOf', - 'hasOwnProperty', - 'isPrototypeOf', - 'propertyIsEnumerable', - 'constructor' - ], - dontEnumsLength = dontEnums.length; - - return function (obj) { - if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { - throw new TypeError('Object.keys called on non-object'); - } - - var result = [], prop, i; - - for (prop in obj) { - if (hasOwnProperty.call(obj, prop)) { - result.push(prop); - } - } - - if (hasDontEnumBug) { - for (i = 0; i < dontEnumsLength; i++) { - if (hasOwnProperty.call(obj, dontEnums[i])) { - result.push(dontEnums[i]); - } - } - } - return result; - }; - })(); -} - // PhantomJS 1.x doesn't support bind, // so leave this in until PhantomJS 2.0 is released //This prototype is provided by the Mozilla foundation and @@ -368,77 +281,6 @@ Util.decodeUTF8 = function (utf8string) { * Cross-browser routines */ - -// Dynamically load scripts without using document.write() -// Reference: http://unixpapa.com/js/dyna.html -// -// Handles the case where load_scripts is invoked from a script that -// itself is loaded via load_scripts. Once all scripts are loaded the -// window.onscriptsloaded handler is called (if set). -Util.get_include_uri = function (root_dir) { - return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI + root_dir + '/' : root_dir + '/'; -}; -Util._loading_scripts = []; -Util._pending_scripts = []; -Util.load_scripts = function (files_by_dir) { - "use strict"; - var head = document.getElementsByTagName('head')[0], script, - ls = Util._loading_scripts, ps = Util._pending_scripts; - - var loadFunc = function (e) { - while (ls.length > 0 && (ls[0].readyState === 'loaded' || - ls[0].readyState === 'complete')) { - // For IE, append the script to trigger execution - var s = ls.shift(); - //console.log("loaded script: " + s.src); - head.appendChild(s); - } - if (!this.readyState || - (Util.Engine.presto && this.readyState === 'loaded') || - this.readyState === 'complete') { - if (ps.indexOf(this) >= 0) { - this.onload = this.onreadystatechange = null; - //console.log("completed script: " + this.src); - ps.splice(ps.indexOf(this), 1); - - // Call window.onscriptsload after last script loads - if (ps.length === 0 && window.onscriptsload) { - window.onscriptsload(); - } - } - } - }; - - var root_dirs = Object.Keys(files_by_dir); - - for (var d = 0; d < root_dirs.length; d++) { - var root_dir = root_dirs[d]; - var files = files_by_dir[root_dir]; - - for (var f = 0; f < files.length; f++) { - script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = Util.get_include_uri(root_dir) + files[f]; - //console.log("loading script: " + script.src); - script.onload = script.onreadystatechange = loadFunc; - // In-order script execution tricks - if (Util.Engine.trident) { - // For IE wait until readyState is 'loaded' before - // appending it which will trigger execution - // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order - ls.push(script); - } else { - // For webkit and firefox set async=false and append now - // https://developer.mozilla.org/en-US/docs/HTML/Element/script - script.async = false; - head.appendChild(script); - } - ps.push(script); - } - } -}; - - Util.getPosition = function(obj) { "use strict"; // NB(sross): the Mozilla developer reference seems to indicate that diff --git a/tests/input.html b/tests/input.html index ec3aefe6..824a9489 100644 --- a/tests/input.html +++ b/tests/input.html @@ -44,7 +44,7 @@ function message(str) { console.log(str); - cell = $D('messages'); + cell = document.getElementById('messages'); cell.innerHTML += msg_cnt + ": " + str + newline; cell.scrollTop = cell.scrollHeight; msg_cnt++; @@ -94,23 +94,23 @@ for (b = 0; b < blist.length; b++) { if (blist[b] === num) { - $D('button' + blist[b]).style.backgroundColor = "black"; - $D('button' + blist[b]).style.color = "lightgray"; + document.getElementById('button' + blist[b]).style.backgroundColor = "black"; + document.getElementById('button' + blist[b]).style.color = "lightgray"; } else { - $D('button' + blist[b]).style.backgroundColor = ""; - $D('button' + blist[b]).style.color = ""; + document.getElementById('button' + blist[b]).style.backgroundColor = ""; + document.getElementById('button' + blist[b]).style.color = ""; } } } window.onload = function() { - canvas = new Display({'target' : $D('canvas')}); + canvas = new Display({'target' : document.getElementById('canvas')}); keyboard = new Keyboard({'target': document, 'onKeyPress': rfbKeyPress}); Util.addEvent(document, 'keypress', rawKey); Util.addEvent(document, 'keydown', rawKey); Util.addEvent(document, 'keyup', rawKey); - mouse = new Mouse({'target': $D('canvas'), + mouse = new Mouse({'target': document.getElementById('canvas'), 'onMouseButton': mouseButton, 'onMouseMove': mouseMove}); @@ -121,10 +121,10 @@ if ('ontouchstart' in document.documentElement) { message("Touch device detected"); - $D('button-selection').style.display = "inline"; - $D('button1').onclick = function(){ selectButton(1) }; - $D('button2').onclick = function(){ selectButton(2) }; - $D('button4').onclick = function(){ selectButton(4) }; + document.getElementById('button-selection').style.display = "inline"; + document.getElementById('button1').onclick = function(){ selectButton(1) }; + document.getElementById('button2').onclick = function(){ selectButton(2) }; + document.getElementById('button4').onclick = function(){ selectButton(4) }; selectButton(); } diff --git a/tests/vnc_perf.html b/tests/vnc_perf.html index e6eee6eb..021bc4ee 100644 --- a/tests/vnc_perf.html +++ b/tests/vnc_perf.html @@ -49,7 +49,7 @@ msg("Loading " + fname); // Load supporting scripts - Util.load_scripts({ + WebUtil.load_scripts({ 'core': ["base64.js", "websock.js", "des.js", "keysym.js", "keysymdef.js", "xtscancodes.js", "keyboard.js", "input.js", "display.js", "rfb.js", "inflator.js"], diff --git a/tests/vnc_playback.html b/tests/vnc_playback.html index 168663db..611c371d 100644 --- a/tests/vnc_playback.html +++ b/tests/vnc_playback.html @@ -59,7 +59,7 @@ if (fname) { message("Loading " + fname); // Load supporting scripts - Util.load_scripts({ + WebUtil.load_scripts({ 'core': ["base64.js", "websock.js", "des.js", "keysym.js", "keysymdef.js", "xtscancodes.js", "keyboard.js", "input.js", "display.js", "rfb.js", "inflator.js"], diff --git a/vnc.html b/vnc.html index bfc12017..b33b704e 100644 --- a/vnc.html +++ b/vnc.html @@ -220,6 +220,7 @@
    + diff --git a/vnc_auto.html b/vnc_auto.html index 6fa3f253..2bb9013b 100644 --- a/vnc_auto.html +++ b/vnc_auto.html @@ -43,6 +43,7 @@ src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'> --> + @@ -77,7 +78,7 @@ "use strict"; // Load supporting scripts - Util.load_scripts({ + WebUtil.load_scripts({ 'core': ["base64.js", "websock.js", "des.js", "keysymdef.js", "xtscancodes.js", "keyboard.js", "input.js", "display.js", "inflator.js", "rfb.js", "keysym.js"], From 3949a095342153865682d716a7289c5de3e611c2 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 3 Sep 2016 13:49:55 -0400 Subject: [PATCH 186/527] Don't modify Array prototype This commit removes our modification of the Array prototype. It wasn't actually used much in the main code, anyway, and it's a bad practice to modify built-in prototypes. --- core/util.js | 23 ------- tests/test.rfb.js | 153 +++++++++++++++++++++++++-------------------- tests/test.util.js | 44 ------------- 3 files changed, 86 insertions(+), 134 deletions(-) diff --git a/core/util.js b/core/util.js index 3a2d241b..cbc74dc4 100644 --- a/core/util.js +++ b/core/util.js @@ -12,35 +12,12 @@ var Util = {}; -/* - * Make arrays quack - */ - var addFunc = function (cl, name, func) { if (!cl.prototype[name]) { Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); } }; -addFunc(Array, 'push8', function (num) { - "use strict"; - this.push(num & 0xFF); -}); - -addFunc(Array, 'push16', function (num) { - "use strict"; - this.push((num >> 8) & 0xFF, - num & 0xFF); -}); - -addFunc(Array, 'push32', function (num) { - "use strict"; - this.push((num >> 24) & 0xFF, - (num >> 16) & 0xFF, - (num >> 8) & 0xFF, - num & 0xFF); -}); - // PhantomJS 1.x doesn't support bind, // so leave this in until PhantomJS 2.0 is released //This prototype is provided by the Mozilla foundation and diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 06eeebe3..86d63787 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -13,6 +13,25 @@ function make_rfb (extra_opts) { return new RFB(extra_opts); } +var push8 = function (arr, num) { + "use strict"; + arr.push(num & 0xFF); +}; + +var push16 = function (arr, num) { + "use strict"; + arr.push((num >> 8) & 0xFF, + num & 0xFF); +}; + +var push32 = function (arr, num) { + "use strict"; + arr.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF); +}; + describe('Remote Frame Buffer Protocol Client', function() { "use strict"; before(FakeWebSocket.replace); @@ -232,17 +251,17 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should send the request with the given width and height', function () { var expected = [251]; - expected.push8(0); // padding - expected.push16(1); // width - expected.push16(2); // height - expected.push8(1); // number-of-screens - expected.push8(0); // padding before screen array - expected.push32(0); // id - expected.push16(0); // x-position - expected.push16(0); // y-position - expected.push16(1); // width - expected.push16(2); // height - expected.push32(0); // flags + push8(expected, 0); // padding + push16(expected, 1); // width + push16(expected, 2); // height + push8(expected, 1); // number-of-screens + push8(expected, 0); // padding before screen array + push32(expected, 0); // id + push16(expected, 0); // x-position + push16(expected, 0); // y-position + push16(expected, 1); // width + push16(expected, 2); // height + push32(expected, 0); // flags client.requestDesktopSize(1, 2); expect(client._sock).to.have.sent(new Uint8Array(expected)); @@ -662,7 +681,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var err_msg = "Whoopsies"; var data = [0, 0, 0, 0]; var err_len = err_msg.length; - data.push32(err_len); + push32(data, err_len); for (var i = 0; i < err_len; i++) { data.push(err_msg.charCodeAt(i)); } @@ -796,10 +815,10 @@ describe('Remote Frame Buffer Protocol Client', function() { function send_num_str_pairs(pairs, client) { var pairs_len = pairs.length; var data = []; - data.push32(pairs_len); + push32(data, pairs_len); for (var i = 0; i < pairs_len; i++) { - data.push32(pairs[i][0]); + push32(data, pairs[i][0]); var j; for (j = 0; j < 4; j++) { data.push(pairs[i][1].charCodeAt(j)); @@ -948,30 +967,30 @@ describe('Remote Frame Buffer Protocol Client', function() { } var data = []; - data.push16(full_opts.width); - data.push16(full_opts.height); + push16(data, full_opts.width); + push16(data, full_opts.height); data.push(full_opts.bpp); data.push(full_opts.depth); data.push(full_opts.big_endian); data.push(full_opts.true_color); - data.push16(full_opts.red_max); - data.push16(full_opts.green_max); - data.push16(full_opts.blue_max); - data.push8(full_opts.red_shift); - data.push8(full_opts.green_shift); - data.push8(full_opts.blue_shift); + push16(data, full_opts.red_max); + push16(data, full_opts.green_max); + push16(data, full_opts.blue_max); + push8(data, full_opts.red_shift); + push8(data, full_opts.green_shift); + push8(data, full_opts.blue_shift); // padding - data.push8(0); - data.push8(0); - data.push8(0); + push8(data, 0); + push8(data, 0); + push8(data, 0); client._sock._websocket._receive_data(new Uint8Array(data)); var name_data = []; - name_data.push32(full_opts.name.length); + push32(name_data, full_opts.name.length); for (var i = 0; i < full_opts.name.length; i++) { name_data.push(full_opts.name.charCodeAt(i)); } @@ -1003,10 +1022,10 @@ describe('Remote Frame Buffer Protocol Client', function() { send_server_init({}, client); var tight_data = []; - tight_data.push16(1); - tight_data.push16(2); - tight_data.push16(3); - tight_data.push16(0); + push16(tight_data, 1); + push16(tight_data, 2); + push16(tight_data, 3); + push16(tight_data, 0); for (var i = 0; i < 16 + 32 + 48; i++) { tight_data.push(i); } @@ -1136,16 +1155,16 @@ describe('Remote Frame Buffer Protocol Client', function() { // header data.push(0); // msg type data.push(0); // padding - data.push16(rect_cnt || rect_data.length); + push16(data, rect_cnt || rect_data.length); } for (var i = 0; i < rect_data.length; i++) { if (rect_info[i]) { - data.push16(rect_info[i].x); - data.push16(rect_info[i].y); - data.push16(rect_info[i].width); - data.push16(rect_info[i].height); - data.push32(rect_info[i].encoding); + push16(data, rect_info[i].x); + push16(data, rect_info[i].y); + push16(data, rect_info[i].width); + push16(data, rect_info[i].height); + push32(data, rect_info[i].encoding); } data = data.concat(rect_data[i]); } @@ -1339,24 +1358,24 @@ describe('Remote Frame Buffer Protocol Client', function() { it('should handle the RRE encoding', function () { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }]; var rect = []; - rect.push32(2); // 2 subrects - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 2); // 2 subrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color rect.push(0xff); // becomes ff0000ff --> #0000FF color rect.push(0x00); rect.push(0x00); rect.push(0xff); - rect.push16(0); // x: 0 - rect.push16(0); // y: 0 - rect.push16(2); // width: 2 - rect.push16(2); // height: 2 + push16(rect, 0); // x: 0 + push16(rect, 0); // y: 0 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 rect.push(0xff); // becomes ff0000ff --> #0000FF color rect.push(0x00); rect.push(0x00); rect.push(0xff); - rect.push16(2); // x: 2 - rect.push16(2); // y: 2 - rect.push16(2); // width: 2 - rect.push16(2); // height: 2 + push16(rect, 2); // x: 2 + push16(rect, 2); // y: 2 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 send_fbu_msg(info, [rect], client); expect(client._display).to.have.displayed(target_data_check); @@ -1384,7 +1403,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rect = []; rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color rect.push(0xff); // becomes ff0000ff --> #0000FF fg color rect.push(0x00); rect.push(0x00); @@ -1416,11 +1435,11 @@ describe('Remote Frame Buffer Protocol Client', function() { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rect = []; rect.push(0x02); - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color send_fbu_msg(info, [rect], client); var expected = []; - for (var i = 0; i < 16; i++) { expected.push32(0xff00ff); } + for (var i = 0; i < 16; i++) { push32(expected, 0xff00ff); } expect(client._display).to.have.displayed(new Uint8Array(expected)); }); @@ -1436,7 +1455,7 @@ describe('Remote Frame Buffer Protocol Client', function() { // send a bg frame rect.push(0x02); - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color // send an empty frame rect.push(0x00); @@ -1445,8 +1464,8 @@ describe('Remote Frame Buffer Protocol Client', function() { var expected = []; var i; - for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 1: solid - for (i = 0; i < 16; i++) { expected.push32(0xff00ff); } // rect 2: same bkground color + for (i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid + for (i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color expect(client._display).to.have.displayed(new Uint8Array(expected)); }); @@ -1454,7 +1473,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; var rect = []; rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color rect.push(2); // 2 subrects rect.push(0xff); // becomes ff0000ff --> #0000FF fg color rect.push(0x00); @@ -1480,7 +1499,7 @@ describe('Remote Frame Buffer Protocol Client', function() { var info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}]; var rect = []; rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects - rect.push32(0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color rect.push(0xff); // becomes ff0000ff --> #0000FF fg color rect.push(0x00); rect.push(0x00); @@ -1561,16 +1580,16 @@ describe('Remote Frame Buffer Protocol Client', function() { function make_screen_data (nr_of_screens) { var data = []; - data.push8(nr_of_screens); // number-of-screens - data.push8(0); // padding - data.push16(0); // padding + push8(data, nr_of_screens); // number-of-screens + push8(data, 0); // padding + push16(data, 0); // padding for (var i=0; i Date: Sat, 3 Sep 2016 13:53:47 -0400 Subject: [PATCH 187/527] Switch to PhantomJS 2.x for testing This commit switches over to use PhantomJS 2.x, bringing in a whole host of improvements (including `Function#bind`, so we can remove the `Function#bind` shim in core/util.js). --- core/util.js | 35 -------------------------------- package.json | 5 ++--- tests/run_from_console.casper.js | 2 +- tests/run_from_console.js | 2 +- 4 files changed, 4 insertions(+), 40 deletions(-) diff --git a/core/util.js b/core/util.js index cbc74dc4..f2e12ae1 100644 --- a/core/util.js +++ b/core/util.js @@ -11,41 +11,6 @@ var Util = {}; - -var addFunc = function (cl, name, func) { - if (!cl.prototype[name]) { - Object.defineProperty(cl.prototype, name, { enumerable: false, value: func }); - } -}; - -// PhantomJS 1.x doesn't support bind, -// so leave this in until PhantomJS 2.0 is released -//This prototype is provided by the Mozilla foundation and -//is distributed under the MIT license. -//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license -addFunc(Function, 'bind', function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 - // internal IsCallable function - throw new TypeError("Function.prototype.bind - " + - "what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; -}); - // // requestAnimationFrame shim with setTimeout fallback // diff --git a/package.json b/package.json index 26ee6ff4..c169e922 100644 --- a/package.json +++ b/package.json @@ -41,14 +41,13 @@ "karma-chai": "^0.1.0", "karma-mocha": "^0.1.10", "karma-mocha-reporter": "^1.0.0", - "karma-phantomjs-launcher": "^0.1.4", + "karma-phantomjs-launcher": "^1.0.0", "karma-sauce-launcher": "^0.2.10", "karma-sinon": "^1.0.4", "karma-sinon-chai-latest": "^0.1.0", "mocha": "^2.1.0", "open": "^0.0.5", - "phantom": "^0.7.2", - "phantomjs": "^1.9.15", + "phantomjs-prebuilt": "^2.1.4", "sinon": "^1.12.2", "sinon-chai": "^2.7.0", "spooky": "^0.2.5", diff --git a/tests/run_from_console.casper.js b/tests/run_from_console.casper.js index 6a738a3e..ba8546b9 100644 --- a/tests/run_from_console.casper.js +++ b/tests/run_from_console.casper.js @@ -1,7 +1,7 @@ var Spooky = require('spooky'); var path = require('path'); -var phantom_path = require('phantomjs').path; +var phantom_path = require('phantomjs-prebuilt').path; var casper_path = path.resolve(__dirname, '../node_modules/casperjs/bin/casperjs'); process.env.PHANTOMJS_EXECUTABLE = phantom_path; var casper_opts = { diff --git a/tests/run_from_console.js b/tests/run_from_console.js index d7d13da2..1a2b800a 100755 --- a/tests/run_from_console.js +++ b/tests/run_from_console.js @@ -92,7 +92,7 @@ if (program.autoInject) { var template = { header: "\n\n\n\n\n
    ", script_tag: function(p) { return ""; }, - footer: "\n\n" + footer: "\n\n" }; template.header += "\n" + template.script_tag(get_path('node_modules/chai/chai.js')); From e4fef7be2d21423bae5df5a8bd4ebe3b2088a9aa Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 3 Sep 2016 13:58:32 -0400 Subject: [PATCH 188/527] Util shouldn't modify window object This commits prevents Util from modifying the window object. - `window.requestAnimFrame` was removed (no polyfill is needed anymore) - the potential redefinition of `console.log` and friends was removed (all supported browsers have `console.xyz` defined anyway) --- core/display.js | 2 +- core/util.js | 63 ++++++++++++------------------------------- tests/test.display.js | 6 ++--- tests/test.util.js | 12 ++++++--- 4 files changed, 30 insertions(+), 53 deletions(-) diff --git a/core/display.js b/core/display.js index 79e1e4f6..e6239f48 100644 --- a/core/display.js +++ b/core/display.js @@ -778,7 +778,7 @@ } if (this._renderQ.length > 0) { - requestAnimFrame(this._scan_renderQ.bind(this)); + requestAnimationFrame(this._scan_renderQ.bind(this)); } }, }; diff --git a/core/util.js b/core/util.js index f2e12ae1..eda4616a 100644 --- a/core/util.js +++ b/core/util.js @@ -11,22 +11,6 @@ var Util = {}; -// -// requestAnimationFrame shim with setTimeout fallback -// - -window.requestAnimFrame = (function () { - "use strict"; - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function (callback) { - window.setTimeout(callback, 1000 / 60); - }; -})(); - /* * ------------------------------------------------------ * Namespaced in Util @@ -45,39 +29,26 @@ Util.init_logging = function (level) { } else { Util._log_level = level; } - if (typeof window.console === "undefined") { - if (typeof window.opera !== "undefined") { - window.console = { - 'log' : window.opera.postError, - 'warn' : window.opera.postError, - 'error': window.opera.postError - }; - } else { - window.console = { - 'log' : function (m) {}, - 'warn' : function (m) {}, - 'error': function (m) {} - }; - } - } Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; - /* jshint -W086 */ - switch (level) { - case 'debug': - Util.Debug = function (msg) { console.log(msg); }; - case 'info': - Util.Info = function (msg) { console.log(msg); }; - case 'warn': - Util.Warn = function (msg) { console.warn(msg); }; - case 'error': - Util.Error = function (msg) { console.error(msg); }; - case 'none': - break; - default: - throw new Error("invalid logging type '" + level + "'"); + if (typeof window.console !== "undefined") { + /* jshint -W086 */ + switch (level) { + case 'debug': + Util.Debug = function (msg) { console.log(msg); }; + case 'info': + Util.Info = function (msg) { console.info(msg); }; + case 'warn': + Util.Warn = function (msg) { console.warn(msg); }; + case 'error': + Util.Error = function (msg) { console.error(msg); }; + case 'none': + break; + default: + throw new Error("invalid logging type '" + level + "'"); + } + /* jshint +W086 */ } - /* jshint +W086 */ }; Util.get_logging = function () { return Util._log_level; diff --git a/tests/test.display.js b/tests/test.display.js index 32a92e22..7c7c693b 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -384,15 +384,15 @@ describe('Display/Canvas Helper', function () { display = new Display({ target: document.createElement('canvas'), prefer_js: false }); display.resize(4, 4); sinon.spy(display, '_scan_renderQ'); - this.old_requestAnimFrame = window.requestAnimFrame; - window.requestAnimFrame = function (cb) { + this.old_requestAnimationFrame = window.requestAnimationFrame; + window.requestAnimationFrame = function (cb) { this.next_frame_cb = cb; }.bind(this); this.next_frame = function () { this.next_frame_cb(); }; }); afterEach(function () { - window.requestAnimFrame = this.old_requestAnimFrame; + window.requestAnimationFrame = this.old_requestAnimationFrame; }); it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { diff --git a/tests/test.util.js b/tests/test.util.js index c4ac403b..c903c745 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -12,12 +12,14 @@ describe('Utils', function() { sinon.spy(console, 'log'); sinon.spy(console, 'warn'); sinon.spy(console, 'error'); + sinon.spy(console, 'info'); }); afterEach(function () { console.log.restore(); console.warn.restore(); console.error.restore(); + console.info.restore(); }); it('should use noop for levels lower than the min level', function () { @@ -27,12 +29,16 @@ describe('Utils', function() { expect(console.log).to.not.have.been.called; }); - it('should use console.log for Debug and Info', function () { + it('should use console.log for Debug', function () { Util.init_logging('debug'); Util.Debug('dbg'); - Util.Info('inf'); expect(console.log).to.have.been.calledWith('dbg'); - expect(console.log).to.have.been.calledWith('inf'); + }); + + it('should use console.info for Info', function () { + Util.init_logging('debug'); + Util.Info('inf'); + expect(console.info).to.have.been.calledWith('inf'); }); it('should use console.warn for Warn', function () { From b4ef49ea36883b0b90bb8c8d9ef97c836f731fd1 Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Sat, 3 Sep 2016 14:06:42 -0400 Subject: [PATCH 189/527] Remove unecessary event-related code from Util The event-related wrapper functions in Util existed mainly for backwards-compat. However, all currently supported browsers support the standard functions, so these wrappers are no longer needed. --- app/ui.js | 14 +++++------ core/input.js | 60 ++++++++++++++++++++++++------------------------ core/util.js | 36 ++--------------------------- tests/input.html | 6 ++--- 4 files changed, 42 insertions(+), 74 deletions(-) diff --git a/app/ui.js b/app/ui.js index 45aa2ac3..cb0cc9e9 100644 --- a/app/ui.js +++ b/app/ui.js @@ -142,7 +142,7 @@ var UI; UI.setViewClip(); UI.setBarPosition(); - Util.addEvent(window, 'resize', function () { + window.addEventListener('resize', function () { UI.applyResizeMode(); UI.setViewClip(); UI.updateViewDrag(); @@ -160,17 +160,17 @@ var UI; document.documentElement.webkitRequestFullscreen || document.body.msRequestFullscreen)) { document.getElementById('noVNC_fullscreen_button').style.display = "inline"; - Util.addEvent(window, 'fullscreenchange', UI.updateFullscreenButton); - Util.addEvent(window, 'mozfullscreenchange', UI.updateFullscreenButton); - Util.addEvent(window, 'webkitfullscreenchange', UI.updateFullscreenButton); - Util.addEvent(window, 'msfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('fullscreenchange', UI.updateFullscreenButton); + window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); } - Util.addEvent(window, 'load', UI.keyboardinputReset); + window.addEventListener('load', UI.keyboardinputReset); // While connected we want to display a confirmation dialogue // if the user tries to leave the page - Util.addEvent(window, 'beforeunload', function (e) { + window.addEventListener('beforeunload', function (e) { if (UI.rfb && UI.rfb_state === 'normal') { var msg = "You are currently connected."; e.returnValue = msg; diff --git a/core/input.js b/core/input.js index ec5382fd..e1fff1c0 100644 --- a/core/input.js +++ b/core/input.js @@ -122,12 +122,12 @@ //Util.Debug(">> Keyboard.grab"); var c = this._target; - Util.addEvent(c, 'keydown', this._eventHandlers.keydown); - Util.addEvent(c, 'keyup', this._eventHandlers.keyup); - Util.addEvent(c, 'keypress', this._eventHandlers.keypress); + c.addEventListener('keydown', this._eventHandlers.keydown); + c.addEventListener('keyup', this._eventHandlers.keyup); + c.addEventListener('keypress', this._eventHandlers.keypress); // Release (key up) if window loses focus - Util.addEvent(window, 'blur', this._eventHandlers.blur); + window.addEventListener('blur', this._eventHandlers.blur); //Util.Debug("<< Keyboard.grab"); }, @@ -136,10 +136,10 @@ //Util.Debug(">> Keyboard.ungrab"); var c = this._target; - Util.removeEvent(c, 'keydown', this._eventHandlers.keydown); - Util.removeEvent(c, 'keyup', this._eventHandlers.keyup); - Util.removeEvent(c, 'keypress', this._eventHandlers.keypress); - Util.removeEvent(window, 'blur', this._eventHandlers.blur); + c.removeEventListener('keydown', this._eventHandlers.keydown); + c.removeEventListener('keyup', this._eventHandlers.keyup); + c.removeEventListener('keypress', this._eventHandlers.keypress); + window.removeEventListener('blur', this._eventHandlers.blur); // Release (key up) all keys that are in a down state this._allKeysUp(); @@ -347,44 +347,44 @@ var c = this._target; if ('ontouchstart' in document.documentElement) { - Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown); - Util.addEvent(window, 'touchend', this._eventHandlers.mouseup); - Util.addEvent(c, 'touchend', this._eventHandlers.mouseup); - Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove); + c.addEventListener('touchstart', this._eventHandlers.mousedown); + window.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchmove', this._eventHandlers.mousemove); } else { - Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown); - Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup); - Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup); - Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove); - Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + c.addEventListener('mousedown', this._eventHandlers.mousedown); + window.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mousemove', this._eventHandlers.mousemove); + c.addEventListener((Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', this._eventHandlers.mousewheel); } /* Work around right and middle click browser behaviors */ - Util.addEvent(document, 'click', this._eventHandlers.mousedisable); - Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + document.addEventListener('click', this._eventHandlers.mousedisable); + document.body.addEventListener('contextmenu', this._eventHandlers.mousedisable); }, ungrab: function () { var c = this._target; if ('ontouchstart' in document.documentElement) { - Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown); - Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup); - Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup); - Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove); + c.removeEventListener('touchstart', this._eventHandlers.mousedown); + window.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchmove', this._eventHandlers.mousemove); } else { - Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown); - Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup); - Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup); - Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove); - Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', + c.removeEventListener('mousedown', this._eventHandlers.mousedown); + window.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mousemove', this._eventHandlers.mousemove); + c.removeEventListener((Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', this._eventHandlers.mousewheel); } /* Work around right and middle click browser behaviors */ - Util.removeEvent(document, 'click', this._eventHandlers.mousedisable); - Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable); + document.removeEventListener('click', this._eventHandlers.mousedisable); + document.body.removeEventListener('contextmenu', this._eventHandlers.mousedisable); } }; diff --git a/core/util.js b/core/util.js index eda4616a..d6a01932 100644 --- a/core/util.js +++ b/core/util.js @@ -232,41 +232,9 @@ Util.getEventPosition = function (e, obj, scale) { return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; }; - -// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events -Util.addEvent = function (obj, evType, fn) { - "use strict"; - if (obj.attachEvent) { - var r = obj.attachEvent("on" + evType, fn); - return r; - } else if (obj.addEventListener) { - obj.addEventListener(evType, fn, false); - return true; - } else { - throw new Error("Handler could not be attached"); - } -}; - -Util.removeEvent = function (obj, evType, fn) { - "use strict"; - if (obj.detachEvent) { - var r = obj.detachEvent("on" + evType, fn); - return r; - } else if (obj.removeEventListener) { - obj.removeEventListener(evType, fn, false); - return true; - } else { - throw new Error("Handler could not be removed"); - } -}; - Util.stopEvent = function (e) { - "use strict"; - if (e.stopPropagation) { e.stopPropagation(); } - else { e.cancelBubble = true; } - - if (e.preventDefault) { e.preventDefault(); } - else { e.returnValue = false; } + e.stopPropagation(); + e.preventDefault(); }; Util._cursor_uris_supported = null; diff --git a/tests/input.html b/tests/input.html index 824a9489..a513645c 100644 --- a/tests/input.html +++ b/tests/input.html @@ -107,9 +107,9 @@ canvas = new Display({'target' : document.getElementById('canvas')}); keyboard = new Keyboard({'target': document, 'onKeyPress': rfbKeyPress}); - Util.addEvent(document, 'keypress', rawKey); - Util.addEvent(document, 'keydown', rawKey); - Util.addEvent(document, 'keyup', rawKey); + document.addEventListener('keypress', rawKey); + document.addEventListener('keydown', rawKey); + document.addEventListener('keyup', rawKey); mouse = new Mouse({'target': document.getElementById('canvas'), 'onMouseButton': mouseButton, 'onMouseMove': mouseMove}); From bd5340c7ee92c28ef01519c1ef9eb456f93a892d Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 14 Sep 2016 13:45:08 -0400 Subject: [PATCH 190/527] Move input-related files into core/input This commit moves all the input-related files from `core/` to `core/input/`, and renames a couple as relevant (input.js --> input/devices.js, keyboard.js --> input/util.js). --- LICENSE.txt | 6 +++--- app/ui.js | 8 ++++---- core/{input.js => input/devices.js} | 4 ++-- core/{ => input}/keysym.js | 0 core/{ => input}/keysymdef.js | 0 core/{keyboard.js => input/util.js} | 0 core/{ => input}/xtscancodes.js | 0 core/rfb.js | 6 +++--- karma.conf.js | 10 +++++----- tests/input.html | 10 +++++----- tests/test.helper.js | 2 +- tests/test.keyboard.js | 2 +- tests/test.rfb.js | 2 +- tests/vnc_perf.html | 6 +++--- tests/vnc_playback.html | 6 +++--- utils/use_require.js | 3 ++- vnc_auto.html | 6 +++--- 17 files changed, 36 insertions(+), 35 deletions(-) rename core/{input.js => input/devices.js} (99%) rename core/{ => input}/keysym.js (100%) rename core/{ => input}/keysymdef.js (100%) rename core/{keyboard.js => input/util.js} (100%) rename core/{ => input}/xtscancodes.js (100%) diff --git a/LICENSE.txt b/LICENSE.txt index 41b45bb9..7b5de592 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -8,8 +8,8 @@ is not limited to): core/base64.js core/des.js core/display.js - core/input.js - core/keysym.js + core/input/devices.js + core/input/keysym.js core/logo.js core/playback.js core/rfb.js @@ -17,7 +17,7 @@ is not limited to): core/util.js core/websock.js app/webutil.js - core/xtscancodes.js + core/input/xtscancodes.js The HTML, CSS, font and images files that included with the noVNC source distibution (or repository) are not considered part of the diff --git a/app/ui.js b/app/ui.js index cb0cc9e9..78d397d7 100644 --- a/app/ui.js +++ b/app/ui.js @@ -12,7 +12,7 @@ /* [module] * import Util from "../core/util"; - * import KeyTable from "../core/keysym"; + * import KeyTable from "../core/input/keysym"; * import RFB from "../core/rfb"; * import Display from "../core/display"; * import WebUtil from "./webutil"; @@ -26,9 +26,9 @@ var UI; /* [begin skip-as-module] */ // Load supporting scripts WebUtil.load_scripts( - {'core': ["base64.js", "websock.js", "des.js", "keysymdef.js", - "xtscancodes.js", "keyboard.js", "input.js", "display.js", - "inflator.js", "rfb.js", "keysym.js"]}); + {'core': ["base64.js", "websock.js", "des.js", "input/keysymdef.js", + "input/xtscancodes.js", "input/util.js", "input/devices.js", + "display.js", "inflator.js", "rfb.js", "input/keysym.js"]}); window.onscriptsload = function () { UI.load(); }; /* [end skip-as-module] */ diff --git a/core/input.js b/core/input/devices.js similarity index 99% rename from core/input.js rename to core/input/devices.js index e1fff1c0..d283fc4e 100644 --- a/core/input.js +++ b/core/input/devices.js @@ -9,8 +9,8 @@ /*global window, Util */ /* [module] - * import Util from "./util"; - * import KeyboardUtil from "./keyboard"; + * import Util from "../util"; + * import KeyboardUtil from "./util"; */ /* [module] export */ var Keyboard; diff --git a/core/keysym.js b/core/input/keysym.js similarity index 100% rename from core/keysym.js rename to core/input/keysym.js diff --git a/core/keysymdef.js b/core/input/keysymdef.js similarity index 100% rename from core/keysymdef.js rename to core/input/keysymdef.js diff --git a/core/keyboard.js b/core/input/util.js similarity index 100% rename from core/keyboard.js rename to core/input/util.js diff --git a/core/xtscancodes.js b/core/input/xtscancodes.js similarity index 100% rename from core/xtscancodes.js rename to core/input/xtscancodes.js diff --git a/core/rfb.js b/core/rfb.js index 0ae96bd4..135d3a75 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -13,12 +13,12 @@ /* [module] * import Util from "./util"; * import Display from "./display"; - * import { Keyboard, Mouse } from "./input" + * import { Keyboard, Mouse } from "./input/devices" * import Websock from "./websock" * import Base64 from "./base64"; * import DES from "./des"; - * import KeyTable from "./keysym"; - * import XtScancode from "./xtscancodes"; + * import KeyTable from "./input/keysym"; + * import XtScancode from "./input/xtscancodes"; * import Inflator from "./inflator.mod"; */ /*jslint white: false, browser: true */ diff --git a/karma.conf.js b/karma.conf.js index f14dc429..5f3c20b9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -113,11 +113,11 @@ module.exports = function(config) { 'core/util.js', // load first to avoid issues, since methods are called immediately //'../core/*.js', 'core/base64.js', - 'core/keysym.js', - 'core/keysymdef.js', - 'core/xtscancodes.js', - 'core/keyboard.js', - 'core/input.js', + 'core/input/keysym.js', + 'core/input/keysymdef.js', + 'core/input/xtscancodes.js', + 'core/input/util.js', + 'core/input/devices.js', 'core/websock.js', 'core/rfb.js', 'core/des.js', diff --git a/tests/input.html b/tests/input.html index a513645c..a261924b 100644 --- a/tests/input.html +++ b/tests/input.html @@ -26,11 +26,11 @@ - - - - - + + + + + From ca5c74ad5ac2e6c294c1f170d01044edbb1d209a Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Aug 2016 16:08:29 +0200 Subject: [PATCH 214/527] Show all status messages in a popup top bar --- app/images/error.svg | 81 +++++++++++++++++++++++++++ app/images/info.svg | 87 +++++++++++++++++++++++++++++ app/images/warning.svg | 81 +++++++++++++++++++++++++++ app/styles/base.css | 122 +++++++++++++++++++---------------------- app/styles/black.css | 32 ----------- app/styles/blue.css | 51 ----------------- app/ui.js | 52 ++++++++++-------- vnc.html | 7 +-- 8 files changed, 335 insertions(+), 178 deletions(-) create mode 100644 app/images/error.svg create mode 100644 app/images/info.svg create mode 100644 app/images/warning.svg diff --git a/app/images/error.svg b/app/images/error.svg new file mode 100644 index 00000000..6c47e8f2 --- /dev/null +++ b/app/images/error.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/info.svg b/app/images/info.svg new file mode 100644 index 00000000..21d268c8 --- /dev/null +++ b/app/images/info.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/images/warning.svg b/app/images/warning.svg new file mode 100644 index 00000000..7114f9b1 --- /dev/null +++ b/app/images/warning.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/styles/base.css b/app/styles/base.css index c9974d69..e838b1de 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -39,6 +39,14 @@ html { top: 0; width: 100%; z-index: 200; + + background: #b2bdcd; /* Old browsers */ + background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ + background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ + background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ } /* General button style */ @@ -167,47 +175,6 @@ html { padding-left: 10px; } -/* Center status display */ -#noVNC_status { - font-size: 12px; - padding-top: 4px; - height:32px; - text-align: center; - font-weight: bold; - color: #fff; - z-index: 0; - position: absolute; - width: 100%; - margin-left: 0px; -} -.noVNC_status_normal { - background: #b2bdcd; /* Old browsers */ - background: -moz-linear-gradient(top, #b2bdcd 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#b2bdcd), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ - background: linear-gradient(top, #b2bdcd 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ -} -.noVNC_status_error { - background: #f04040; /* Old browsers */ - background: -moz-linear-gradient(top, #f04040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ - background: linear-gradient(top, #f04040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ -} -.noVNC_status_warn { - background: #f0f040; /* Old browsers */ - background: -moz-linear-gradient(top, #f0f040 0%, #899cb3 49%, #7e93af 51%, #6e84a3 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(49%,#899cb3), color-stop(51%,#7e93af), color-stop(100%,#6e84a3)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* IE10+ */ - background: linear-gradient(top, #f0f040 0%,#899cb3 49%,#7e93af 51%,#6e84a3 100%); /* W3C */ -} - /* Right side buttons */ .noVNC_buttons_right { float: right; @@ -295,28 +262,60 @@ html { float: right; } -/* Popup Status */ -#noVNC_popup_status { +/* ---------------------------------------- + * Status Dialog + * ---------------------------------------- + */ + +#noVNC_status { + display: none; position: fixed; - z-index: 1; + top: 0; + left: 0; + width: 100%; + z-index: 3; - margin: 15px; - top: 60px; - padding: 15px; - width: auto; + padding: 5px; - text-align: center; - font-weight: bold; + flex-direction: row; + justify-content: center; + align-content: center; + + line-height: 25px; word-wrap: break-word; color: #fff; - background: rgba(0,0,0,0.65); - -webkit-border-radius: 10px; - -moz-border-radius: 10px; - border-radius: 10px; + border-bottom: 1px solid rgba(0, 0, 0, 0.9); } -#noVNC_popup_status.noVNC_hidden { - display: none; +#noVNC_status.noVNC_open { + display: flex; +} + +#noVNC_status::before { + content: ""; + display: inline-block; + width: 25px; + height: 25px; + margin-right: 5px; +} + +#noVNC_status.noVNC_status_normal { + background: rgba(128,128,128,0.9); +} +#noVNC_status.noVNC_status_normal::before { + content: url("../images/info.svg") " "; +} +#noVNC_status.noVNC_status_error { + background: rgba(200,55,55,0.9); +} +#noVNC_status.noVNC_status_error::before { + content: url("../images/error.svg") " "; +} +#noVNC_status.noVNC_status_warn { + background: rgba(180,180,30,0.9); +} +#noVNC_status.noVNC_status_warn::before { + content: url("../images/warning.svg") " "; } /* ---------------------------------------- @@ -398,17 +397,6 @@ html { * ---------------------------------------- */ -/* left-align the status text on lower resolutions */ -@media screen and (max-width: 800px){ - #noVNC_status { - z-index: 1; - position: relative; - width: auto; - float: left; - margin-left: 4px; - } -} - @media screen and (max-width: 640px){ #noVNC_clipboard_text { width: 410px; diff --git a/app/styles/black.css b/app/styles/black.css index 9a3d5d2e..7c49ae06 100644 --- a/app/styles/black.css +++ b/app/styles/black.css @@ -6,38 +6,6 @@ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ -#noVNC_keyboardinput { - background-color:#000; -} - -.noVNC_status_normal { - background: #4c4c4c; /* Old browsers */ - background: -moz-linear-gradient(top, #4c4c4c 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#4c4c4c), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ - background: linear-gradient(top, #4c4c4c 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ -} -.noVNC_status_error { - background: #f04040; /* Old browsers */ - background: -moz-linear-gradient(top, #f04040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f04040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ - background: linear-gradient(top, #f04040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ -} -.noVNC_status_warn { - background: #f0f040; /* Old browsers */ - background: -moz-linear-gradient(top, #f0f040 0%, #2c2c2c 50%, #000000 51%, #131313 100%); /* FF3.6+ */ - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f0f040), color-stop(50%,#2c2c2c), color-stop(51%,#000000), color-stop(100%,#131313)); /* Chrome,Safari4+ */ - background: -webkit-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Chrome10+,Safari5.1+ */ - background: -o-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* Opera11.10+ */ - background: -ms-linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* IE10+ */ - background: linear-gradient(top, #f0f040 0%,#2c2c2c 50%,#000000 51%,#131313 100%); /* W3C */ -} - .noVNC_panel { border:2px solid #fff; background:#000; diff --git a/app/styles/blue.css b/app/styles/blue.css index b986d9b2..a59428ec 100644 --- a/app/styles/blue.css +++ b/app/styles/blue.css @@ -6,59 +6,8 @@ * This file is licensed under the 2-Clause BSD license (see LICENSE.txt). */ -.noVNC_status_normal { - background-color:#04073d; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.54, rgb(10,15,79)), - color-stop(0.5, rgb(4,7,61)) - ); - background-image: -moz-linear-gradient( - center bottom, - rgb(10,15,79) 54%, - rgb(4,7,61) 50% - ); -} -.noVNC_status_error { - background-color:#f04040; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.54, rgb(240,64,64)), - color-stop(0.5, rgb(4,7,61)) - ); - background-image: -moz-linear-gradient( - center bottom, - rgb(4,7,61) 54%, - rgb(249,64,64) 50% - ); -} -.noVNC_status_warn { - background-color:#f0f040; - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.54, rgb(240,240,64)), - color-stop(0.5, rgb(4,7,61)) - ); - background-image: -moz-linear-gradient( - center bottom, - rgb(4,7,61) 54%, - rgb(240,240,64) 50% - ); -} - .noVNC_panel { border:2px solid #fff; background:#04073d; color:#fff; } - -#noVNC_keyboardinput { - background-color:#04073d; -} - diff --git a/app/ui.js b/app/ui.js index b9e437e5..234af0aa 100644 --- a/app/ui.js +++ b/app/ui.js @@ -39,7 +39,7 @@ var UI; rfb_state: 'loaded', resizeTimeout: null, - popupStatusTimeout: null, + statusTimeout: null, hideKeyboardTimeout: null, keyboardVisible: false, @@ -175,6 +175,9 @@ var UI; UI.updateViewDrag(); UI.setBarPosition(); } ); + + document.getElementById("noVNC_status") + .addEventListener('click', UI.hideStatus); }, setupFullscreen: function() { @@ -196,10 +199,6 @@ var UI; .addEventListener('click', UI.toggleViewDrag); document.getElementById("noVNC_send_ctrl_alt_del_button") .addEventListener('click', UI.sendCtrlAltDel); - document.getElementById("noVNC_status") - .addEventListener('click', UI.popupStatus); - document.getElementById("noVNC_popup_status") - .addEventListener('click', UI.closePopup); }, addTouchSpecificHandlers: function() { @@ -313,10 +312,12 @@ var UI; updateState: function(rfb, state, oldstate, msg) { UI.rfb_state = state; var klass; + var timeout; switch (state) { case 'failed': case 'fatal': klass = "noVNC_status_error"; + timeout = 0; // zero means no timeout break; case 'normal': klass = "noVNC_status_normal"; @@ -341,8 +342,12 @@ var UI; } if (typeof(msg) !== 'undefined') { - document.getElementById('noVNC_control_bar').setAttribute("class", klass); - document.getElementById('noVNC_status').innerHTML = msg; + document.getElementById('noVNC_status') + .classList.remove("noVNC_status_normal", + "noVNC_status_warn", + "noVNC_status_error"); + document.getElementById('noVNC_status').classList.add(klass); + UI.showStatus(msg, timeout); } UI.updateVisualState(); @@ -432,27 +437,28 @@ var UI; //Util.Debug("<< updateVisualState"); }, - popupStatus: function(text) { - var psp = document.getElementById('noVNC_popup_status'); + showStatus: function(text, time) { + var statusElem = document.getElementById('noVNC_status'); - clearTimeout(UI.popupStatusTimeout); + clearTimeout(UI.statusTimeout); - if (typeof text === 'string') { - psp.innerHTML = text; - } else { - psp.innerHTML = document.getElementById('noVNC_status').innerHTML; + statusElem.innerHTML = text; + statusElem.classList.add("noVNC_open"); + + // If no time was specified, show the status for 1.5 seconds + if (typeof time === 'undefined') { + time = 1500; } - psp.classList.remove("noVNC_hidden"); - psp.style.left = window.innerWidth/2 - - parseInt(window.getComputedStyle(psp).width)/2 -30 + "px"; - // Show the popup for a maximum of 1.5 seconds - UI.popupStatusTimeout = setTimeout(UI.closePopup, 1500); + // A specified time of zero means no timeout + if (time != 0) { + UI.statusTimeout = window.setTimeout(UI.hideStatus, time); + } }, - closePopup: function() { - clearTimeout(UI.popupStatusTimeout); - document.getElementById('noVNC_popup_status').classList.add("noVNC_hidden"); + hideStatus: function() { + clearTimeout(UI.statusTimeout); + document.getElementById('noVNC_status').classList.remove("noVNC_open"); }, /* ------^------- @@ -1022,7 +1028,7 @@ var UI; // The browser is IE and we are in fullscreen mode. // - We need to force clipping while in fullscreen since // scrollbars doesn't work. - UI.popupStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); + UI.showStatus("Forcing clipping mode since scrollbars aren't supported by IE in fullscreen"); UI.rememberedClipSetting = UI.getSetting('clip'); UI.setViewClip(true); document.getElementById('noVNC_setting_clip').disabled = true; diff --git a/vnc.html b/vnc.html index 9d9fcaf3..1987bb4c 100644 --- a/vnc.html +++ b/vnc.html @@ -96,8 +96,6 @@ -
    -
    - -
    -
    + +
    From 8bf688265d67efe6a2904ca65fe5477d21f69ae3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 25 Aug 2016 16:31:38 +0200 Subject: [PATCH 215/527] Replace icons with SVG versions Resolution independent for high DPI devices, and easier to modify if we want to change something. --- app/images/alt.png | Bin 339 -> 0 bytes app/images/alt.svg | 92 ++++++++++++++++++++++++++++ app/images/clipboard.png | Bin 501 -> 0 bytes app/images/clipboard.svg | 106 +++++++++++++++++++++++++++++++++ app/images/connect.png | Bin 404 -> 0 bytes app/images/connect.svg | 96 +++++++++++++++++++++++++++++ app/images/ctrl.png | Bin 354 -> 0 bytes app/images/ctrl.svg | 96 +++++++++++++++++++++++++++++ app/images/ctrlaltdel.png | Bin 317 -> 0 bytes app/images/ctrlaltdel.svg | 100 +++++++++++++++++++++++++++++++ app/images/disconnect.png | Bin 1378 -> 0 bytes app/images/disconnect.svg | 94 +++++++++++++++++++++++++++++ app/images/drag.png | Bin 963 -> 0 bytes app/images/drag.svg | 76 +++++++++++++++++++++++ app/images/esc.png | Bin 385 -> 0 bytes app/images/esc.svg | 92 ++++++++++++++++++++++++++++ app/images/fullscreen.png | Bin 851 -> 0 bytes app/images/fullscreen.svg | 93 +++++++++++++++++++++++++++++ app/images/keyboard.png | Bin 1283 -> 0 bytes app/images/keyboard.svg | 88 +++++++++++++++++++++++++++ app/images/mouse_left.png | Bin 511 -> 0 bytes app/images/mouse_left.svg | 92 ++++++++++++++++++++++++++++ app/images/mouse_middle.png | Bin 517 -> 0 bytes app/images/mouse_middle.svg | 92 ++++++++++++++++++++++++++++ app/images/mouse_none.png | Bin 497 -> 0 bytes app/images/mouse_none.svg | 92 ++++++++++++++++++++++++++++ app/images/mouse_right.png | Bin 513 -> 0 bytes app/images/mouse_right.svg | 92 ++++++++++++++++++++++++++++ app/images/power.png | Bin 390 -> 0 bytes app/images/power.svg | 87 +++++++++++++++++++++++++++ app/images/settings.png | Bin 2495 -> 0 bytes app/images/settings.svg | 76 +++++++++++++++++++++++ app/images/tab.png | Bin 387 -> 0 bytes app/images/tab.svg | 86 ++++++++++++++++++++++++++ app/images/toggleextrakeys.png | Bin 735 -> 0 bytes app/images/toggleextrakeys.svg | 90 ++++++++++++++++++++++++++++ vnc.html | 36 +++++------ 37 files changed, 1658 insertions(+), 18 deletions(-) delete mode 100644 app/images/alt.png create mode 100644 app/images/alt.svg delete mode 100644 app/images/clipboard.png create mode 100644 app/images/clipboard.svg delete mode 100644 app/images/connect.png create mode 100644 app/images/connect.svg delete mode 100644 app/images/ctrl.png create mode 100644 app/images/ctrl.svg delete mode 100644 app/images/ctrlaltdel.png create mode 100644 app/images/ctrlaltdel.svg delete mode 100644 app/images/disconnect.png create mode 100644 app/images/disconnect.svg delete mode 100644 app/images/drag.png create mode 100644 app/images/drag.svg delete mode 100644 app/images/esc.png create mode 100644 app/images/esc.svg delete mode 100644 app/images/fullscreen.png create mode 100644 app/images/fullscreen.svg delete mode 100644 app/images/keyboard.png create mode 100644 app/images/keyboard.svg delete mode 100644 app/images/mouse_left.png create mode 100644 app/images/mouse_left.svg delete mode 100644 app/images/mouse_middle.png create mode 100644 app/images/mouse_middle.svg delete mode 100644 app/images/mouse_none.png create mode 100644 app/images/mouse_none.svg delete mode 100644 app/images/mouse_right.png create mode 100644 app/images/mouse_right.svg delete mode 100644 app/images/power.png create mode 100644 app/images/power.svg delete mode 100644 app/images/settings.png create mode 100644 app/images/settings.svg delete mode 100644 app/images/tab.png create mode 100644 app/images/tab.svg delete mode 100644 app/images/toggleextrakeys.png create mode 100644 app/images/toggleextrakeys.svg diff --git a/app/images/alt.png b/app/images/alt.png deleted file mode 100644 index d42af7b421b9f1193620cb4a7114f1df27886d1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AGfn3c|@SfJ1YPZ!4!kK=DA?d4@Mvg-UsUjQv!4@3E(23Sx$<4+2dh>^&G&f67BAwO*^{~A)gQHlw4ThU?9=P7U#iY$ zoU@gULH+&Szpr+?q{tjL>$J>$WYr(E@cJVsj=AZ~_m3?|T-1=LlQ4TyhX?0_FWA34~+4u + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/images/clipboard.png b/app/images/clipboard.png deleted file mode 100644 index 24df33c1c103755c23167c9a1d5eb51f9057060e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 501 zcmVP000>X1^@s6#OZ}&0005JNklLQsoytXHG%qsDu&24?kb6(gJMZNIUjQci;%GE_^nL#i=bVU0+3)v%yWQ@~-6FHu z?1ydJziiuX0zj=+gYWxKhGG0P3X&k2nr5MT4Y|wp8MrATn@2jALL#zx6ke_*NzM*Q(LB=qxlTR{ z-;+rcMIa&o#WNuyL{XG4;(6ZLfhv-T5CWd(<%>9ubKQ3ik|dGupG+on52n*8WzD<# rPah746vwf&3n2ssgTZ}w#D9YiP?b1W?a7*e00000NkvXXu0mjf;r`$= diff --git a/app/images/clipboard.svg b/app/images/clipboard.svg new file mode 100644 index 00000000..79af2752 --- /dev/null +++ b/app/images/clipboard.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/images/connect.png b/app/images/connect.png deleted file mode 100644 index 79e71adb85cbdd2da5b59d9e2c38609dd526d1be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 404 zcmV;F0c-w=P)P000>X1^@s6#OZ}&00047NklS9D+ncfe;N96$KSXNI`*e04{(81y?{Ei6R9Of(lMRK|`ICPyi+&eiUAZ zm9zdypa3aP8p(P)``(OKvkOZEAgLANt&n8Es+ql)h-}pOd;)L4*vyoZaUk(X(h2YZ zD6n1y173kMNkcPx$w|M`b>JGf1FjY{IRJKnLo*x0Y$q|00GDQVGp9jbXaej5Yrr!= z6v_iSix@a`oW`_)kjbxOY4Z0?^0`QVpqcM2o%j@J13gJ2vf(nCK`weuV_Im@L}d5R zDRAuZbw>Wp5CivAbYhOP!dl(+VukP6=Za^N0FPdB9#J3{kN0zN8cC>vtYm2u*aKRw zgA~{TE`S}r|8)dB0q4NbGfRO7GaHt4T@FcoWtg~-r2gDO6$V{N1I=O}sav-{^k{{! y4cr3b0^fFPm1r~rl2*vhD1?cbjhg8Ee|!N&_KPMesy;aY0000 + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/images/ctrl.png b/app/images/ctrl.png deleted file mode 100644 index a63b601f19d9e62a784b9ae3619792ee0e7e1a14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 354 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AH0j{KENDnOz4o-U3d9>?EKa`a+$6maEEblBXv zqublI^Z5ssChmi=1rK>`n#>Cpc5LscXyxJQ&70LIB0IC{X&RH3tZ?$3uhC)uAAQ%~ z=dm#0#6w#pC2J)m-^*`i-jt|dOLJh#dSJNyOLkI^a{|+uvu}bz`JFkM*S+>@X!rHq z_o7j{y!=7c)kEp~_RqX&X~247rqA8G!CuF+|LYjD=OyU#_5bp}sYKbLh*2~7a?){X-J diff --git a/app/images/ctrl.svg b/app/images/ctrl.svg new file mode 100644 index 00000000..856e9395 --- /dev/null +++ b/app/images/ctrl.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/app/images/ctrlaltdel.png b/app/images/ctrlaltdel.png deleted file mode 100644 index 31922e53242fe85fb1db641a15339e2c33670e5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 317 zcmV-D0mA-?P)P000>X1^@s6#OZ}&00035NklEIGHbhH+fhROmqGP}QpED4br)*+f` z_?l_{ec#LO+c)rEu-peF-2wwZk-8P|U}oc72a=w3m0px=;itmLe1 z#^)NUsv0@xQqw6Qf3i;eeYVpf={%ANX10{n1-g;U+RRoliQfnwftyI?tt`uHNl#|h z+ZT|ukaT5ci zMJI${Kqr++6=)XZJ#Ys&cHgZ7UcC3Sx~_*sQC#x%+c;nbytZig#9=-FEZMBb(?pXA P00000NkvXXu0mjfvjB`6 diff --git a/app/images/ctrlaltdel.svg b/app/images/ctrlaltdel.svg new file mode 100644 index 00000000..d7744ea3 --- /dev/null +++ b/app/images/ctrlaltdel.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/app/images/disconnect.png b/app/images/disconnect.png deleted file mode 100644 index 8832f5ea7e2bdde7016b9429917fd453e42aadb8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1378 zcmV-o1)chdP)P000>X1^@s6#OZ}&000FkNklcUSfZ5jnN)zSKqnqi|ej8W0VNMzMT<3}+XS8B?_ zl#0sGg$8V@f}@G4lQyjytg(rUPIWZVAYl?g@ne{Q_ih)yX9-2@rZ>5ZckVssfBxrx z?>ko*V+4aiZy*p@Dk7h2tv?cxVy$(Ni1cc$yG7)>*1A2NPG644`L~dG^wKf)ub-fW}^XAR-hK7dD1CxO(9*-y7*x1-T1_J?fI2@i~ z7{+%%CD3D<=Bi`Ij{W{GkVqslMJd(k0PH_`^5pK35w+IujkI-kcKXZ8$}#|3w{C67 z<#JU(k8Rs!E+c_g8qAn6mnt*w0@_!YP`8i-|CVi<;g^5ltU!h{Jq zfP)7Qu1_YDpNfdx)zy_hZQ8VF4#2DUnEgtAfFx*EvNjU%q@hFaVqtkqaUc zA306ayby^*el9F5JS!scjT<-4PbQP=0c_iT8VCeBcJAEymy^)0U@+)afk0p>ua0T! zx^?S%ob0`$R7U{n>gxP0EiEq`9rpnB&U!!4Twh;*tf;6c>-Boi15*Nlz)}UQbVO|! ztNy$?MdJ=1KAdp@lu~;g-zQ3`=H}++I)Gd*r>0Jw+UWSLR6w~y_10Jjp-`w`I0F~J zVQ}*J@#D1sj~_qwdORMxva<5FvtF)5q!_@tXrq>>rluypwYBv*Ky`KXZU?{udWSPu zy?XUK6DLm00+g1PW&uQ`SZS?`oPhRdB?^T?1&xi3FI@l;`Px}~VcT|#%b>2V&VTOQ zxo56pAP}&eQWhx@>2>VN-C$YPYmr?B(P(t917HJBZQDK(i^X=jrF6A&eS3O(l*6P~ zX|1~f!r`zV=Uy0wVT}|S*yCjOR4Em60fr~y`T``ANw2fNTZzbZ0HxII{{DUq z{EJvDwm@sWc>n(W&92Bwsh88IPyeZ@sp+iNdPzE+UUBHqp-;5dOSIODwbqruDj@I5 zl`9`QxYtarb-Rdc6OkR`$B*AOFfd>j6clU$Hg#qwC$KxEudmOa%jMPrUFmfCvWmy!S;H{)0o=H8;|TEQrAwDM zefqSB2n7WNR8&+D3WWq9o6VA!mxoht7et0l8X6jk9ewu28zA7T~MC6+jCr*3`kjZ2&`h31+{Bxfaa6v>?x-p{BXg%;1Bl^kfyAg}U zx=hnt1@wr>$0Bk&FE8&1KyPpF3ZKt6YglmCZ?4vQNli^naWope>;jmkxoUU|eru1{ zTDMhKSKlo!FE90YJOhSdXv?xBm&+M<@7@ixwY80Z@ZiCG_>Vo_9sm1lW%zL4UA$@s k + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/images/drag.png b/app/images/drag.png deleted file mode 100644 index 433f896d67d82cb3546cd7094bb0d2543cbf0971..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 963 zcmV;!13dhRP)P000>X1^@s6#OZ}&000AuNkl?%c$G}6qS_s+TZzH?`Q&kSb%;d5(z zInZ-K!!Q~mk;tojK96892wm3^jYe^DasmL*bsc`cA6r{nFiq2JYHIp>U|`^!h}3g@ zvNDiPr@xKGVt-ne6_iq9cXt=rY!+u{XUOOCkW%94=m@!74w*~_rfFh-fB&Doy}iEx z{00EbtPzp*bWdfKFtfJ2y!`s=>S{I|4y*e5dc2w;Wr>8~osi`UWd_DkB%&eN5n=v{%3Qf}hKmowr-5qpY$N2cTVrC?h$ydxQ z0a${F?n_06hlhV`ZEaQ2XcUi+k4n=t>hA6qPNx&h3}!}SW1|=x928PYDE02FeLf!= z8XDkqI)&Tq#_{p76MzQbv8-K1H%Lhv8yl2LrHGjUfS=k4A%va1vSUPqP$-1n-rlF9 zWn{Bi2Y><*nMIN9?QI7D%+Aj8#l;2n_4U#D`8hE&;XPI_F$^3I2Q*ECQi@H}gp~3d z+tNh~_w@8U&dtrS-|xrB$Os$`M={F}fr!A&P)b1vA@cdWy1cxEuIn%CHD&F}<#GrF z0;sF2gQjWFH0`4z_Bq+>GMNkv!@%U^EF06;pOE+eQ+yz*jZA|hgDvMh_^@i?`# zw7f0dR5tMo3k!5}bHlZ@wcOv|52`Mssw^Rd-6|X&9wHu(BM=DuQMyK}+>}TpW*;6N zelHXX+}YVlQdWI=%Ak}25us2hV0Cqs-EMbbdV2b&_f}$KWo1P$vtnk9jg3J{S=RT; z0*MHQVIYx6a3~ad&CFjB(ZdI`voSF-!O3KjZCIAYl~77CGjk*o;hCA4tiZ*>BDSJ;uxWB*0?d`4oT=h@+s7QNzdzrv< lb0dU6O-)Tz{D1s+@h<>w*z9LaLB0S0002ovPDHLkV1iTO#UTIy diff --git a/app/images/drag.svg b/app/images/drag.svg new file mode 100644 index 00000000..139caf94 --- /dev/null +++ b/app/images/drag.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/esc.png b/app/images/esc.png deleted file mode 100644 index ece5f7cbef684a41f4a7bb9d632e4cb2310d6b5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 385 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AG<$XUxK0|o|0K2I0N5Rc<;r)=~(;vmpwt~763 z(rf03YMC%Gv!>p;`x=s-3jg3Sv+J08Hk7^Wk+j5=)(kI8VZ)Z0e~+b2TwvK-mAbF2 z{@d)ElGk?5n>TO1$DHDGXL8aMJSYA45_?{oD9s()th9+WhB^2Fuf*zCdB(y{77Tew z&kS1%{uE|Re6Mm~@64I2-aW6}r=)cIK&W#;RvKH+>Qzztr;B)Z{7}kVtXRpGB`8_- zo?X|~X2#5`rF$<%rB3&A;!r=3y(X+NI`nR)$B{ESr#C&d_Xb*BZ7(n}k>jDxsd>+C zaTaJQEjIC;?vf&+TE00gNaUzi!N1irSG + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/app/images/fullscreen.png b/app/images/fullscreen.png deleted file mode 100644 index f4fa0ce8321de23929618a8d82ec0e42c094bafc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 851 zcmV-Z1FZasP)P000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-va^(3nu(M(YOEr0^mtR zK~zY`-Bv+s;y@IRx`*0@QVPXFFCKc8$+u~>Y$TrSyqG#dTNQ4bJ8V_B9{+$SK*ata|d z_Hu^3hQr~oH=E7W&xp)sGj%u|_Et8$s*dA0jYi{w=lQUY?Yo2L`EaArxNscD@p5vM zQfiu}NeChCjb^j?a~-L7yxPBk5JF7TG%2N&!x&>pk`fTY%yc?6m^Vy_=lRVKJlJFc z?A&xZH6Vl;Ns&A=C`x)X8a;YExk{O$k%5NkG@mu~yfX!NKkN}5v2 zI1g81E=f|tEo9ra4Iw=76M+z(*tTuE1tdvIFfV<>GF@`}7|ghHQ56<>}p_Yd&@ zhXp-|WrMKKuzF~*fjfK@hf8Rc)~>HVk7T%W}$3K$hi{VHgu;tEy^C5QJ^7NSNpOD4XPRxs7Z# zd!0-s(`=0p^1N6qUi?04u~@tiLY~vTG|`Fws00I=8X zcJJKPwBPUd%H?u$T`wt@%gKJf-*cbqcDr`~fW3S^zt!nkoc>D>vAZXilU$7{O2ZO;t_j0ZzBO&B@KA%6i*Mb(y`F#H5ehBaWNU(Scg0S-^SuF^{ dj`v;}eggqm-~P^IymkNp002ovPDHLkV1kszf;IpE diff --git a/app/images/fullscreen.svg b/app/images/fullscreen.svg new file mode 100644 index 00000000..29bd05da --- /dev/null +++ b/app/images/fullscreen.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/app/images/keyboard.png b/app/images/keyboard.png deleted file mode 100644 index f797952513b39cc75d0f0dd4d6716608e10217ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1283 zcmV+e1^oJnP)X1^@s6sPETi000EaNklRNjzKsl+3H2fX*Se?>)crik)8OqXL!g(Zk)QUl+u@eZ{N?8!v_!lURzreuTz#~)o$Ou{ncK!u58uobz4={SGnacq(;4w(VW_$&Tn0? zLI}}g46&zAPcHz$ULt<47-LVhIeLJBh>%XF$y!}QAP_(_7UAhsnk>u0Krn#eSd^ug z(`1!b(Ld0S;X^TADCEg3n(+F)h>wnk)A;$yRWL9yH#>)^$thg9d>IhH?CdP2Ca3Y! z+$BH=%+FuP)Z`TAlCx;HT3FcjvlqAg;`IegPfj5@GlR{|O)M-fVtR5KKVFzYty=4~ ztKG0fWOsT+R-mdX6o&#p2t`()soJ)OkYyQ)Cd1*7006QqL)A1WiV6n6AuCW-4T`LS zfkAdSplT`{vJ3_WiE(J^mfypX-^W&bH_NSCXW|FATe*W}`{>?%qSaUdQqGj>F}4;qINgs8wq? z^3GB8_xJJpckYp8Sr|P$hDapBQ%_PN7+mkKYV`y^?C#TK0kuNAd)jltXu2&tkJ-U z58kKbOcIrH1t&i_iO8AL;_9U4fg~KzHkK$PNeru;=_E_bY=I=X`e&0s?}Fa+y5r zwvA|P7!KKiTsDVhqk(8N3e~A{qhL_AR>R<62)ak-s}+kXoMV4j#+H5vY>}A`x{LY_0?>}|w)ThJ4!xj6} t2m;U`qQbdz=l(i-_H6Z4&i6*0`Va9a7RbS?<+lI;002ovPDHLkV1f&1X|4bO diff --git a/app/images/keyboard.svg b/app/images/keyboard.svg new file mode 100644 index 00000000..137b350a --- /dev/null +++ b/app/images/keyboard.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/images/mouse_left.png b/app/images/mouse_left.png deleted file mode 100644 index 1de7a486c76ff1b95efd7e5ee36dd6943cc6dd3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 511 zcmVX1^@s6HR9gx0005TNkly zKhD}f5XIlxu*TB5qC!GGMv9cV1WC&YKqwXxk_(^!S^NKxY&N7`ufy{^BuRp5wTe=yggB0|-EL8<)l{q1 zx;aUA_riL;R!Ne;vMfj`A*BRk49#Zq*`4x_>nA1i%`P>6%qjo?002ovPDHLkV1jL+ B<81%{ diff --git a/app/images/mouse_left.svg b/app/images/mouse_left.svg new file mode 100644 index 00000000..ce4cca41 --- /dev/null +++ b/app/images/mouse_left.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/images/mouse_middle.png b/app/images/mouse_middle.png deleted file mode 100644 index 81fbd9bd375b2daa88b6bac6a7972c58b708a67b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 517 zcmV+g0{Z=lP)X1^@s6HR9gx0005ZNkly zzltJ35XQfr(H;kS;J`?bd5jA#-1w4>%zXkyP6VL?HBbltU?dpHCifEe1osR^UUJ;X z1cRfFO!r0W*)ulkvb)@VSrz^Ls=KHz;G>D86P$Co+wG9g=OLv;9LE3v%d!9faU4Sk z0ZJ)M)5PI$kc1H8c^=V%XS3OlAP62f=cn9oIK=l~k4o{cpPta^bWZVPGJ%v5jYi|d zGtLd0&F0HF6aW}wnP(OX1tqT6>xiOAoaYoUrBrnQux&d7zLc{pOJOO25CUo4noE*I zi4|}h#~H_VyUjqND9Ysa`@OyurfFsjK@i*orj#nNG8Nr!Hwl7(FvbWWnMwR-A_P)O z06>x?5`bikq19@gZk9VY9FIqgMk75@QwIR)^?LeNJm2CTJ%6CI*SK`}9;JKxYG9q- z4g6|_(xBgO0(KndX)qYP2xQx~epfV`&2PT%-wkcI+i+d?rxpwVuIv8sJnv@cVzB_F zbX%!ZK3_dN9*>iLzb~(8I-N?+d2)%N{l4@0T&z|rQZAR_I1a)vM6p;zE|-Jv`&h5n zD3waGR;yi{q^o;jxm?OH3}F}sgb)xyfH8(@wR-PP`Oozit@F&YG*W3V00000NkvXX Hu0mjf=bh+1 diff --git a/app/images/mouse_middle.svg b/app/images/mouse_middle.svg new file mode 100644 index 00000000..6603425c --- /dev/null +++ b/app/images/mouse_middle.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/images/mouse_none.png b/app/images/mouse_none.png deleted file mode 100644 index 93dbf5780777973578edaf7ae35f65ed01e5718e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 497 zcmVX1^@s6HR9gx0005FNkly z!HVKQ5QhIwv}2%i@+3$eV-E8o-hBc+tO#O*=)n*Y#FOAj_Br+m_8B~RS~ikHFlwZ` z_b|+0CpM$Y>@fS^{-W!vqNpz5e-kMOIOpnkJfdE&Ln(!#C;$MC;{X7PqJWeVlv3EX zjq~}e2q7d4!wTz7%jNRNWHM3L3W7kDH6D*OAIGtZqDWn{tZTF1@1Hs6S~130rGZAH zq4|Elk37$%5lku7GXS`*TLIsSIgX>TG$W-%S@*_-5SrJ(MNw2T4u?YplIM9Pb~>HR zqp)qel8~n9U0_P7=Cu`>PNyPGQ^FV{gqXL75CWxMmOu!h04T;7hQr}oXSM5u%jJS7 zip)eq9ROrDo0&)P+QlP!{Xpeh>(=8(RG#gJflc}}@P`#DgMR%Ju;+Q-gCO`Tkn6hU zTQL|6z9mWWFmyB;!T0^2Mlb;QzW+N6!@HsD^%|7YL#NYudiQZ0$6`L8R}Oqxi^W25 z&c!W;@%ye;E4kfnNxR*K=XuDo49#W}wOS2Hl3=&nq19@sUaxm^lis}xo6SaLSq96p nAf<$q5{xl)yWK}`$`{uk{?qW@S6u}n00000NkvXXu0mjfItA$@ diff --git a/app/images/mouse_none.svg b/app/images/mouse_none.svg new file mode 100644 index 00000000..3e0f838a --- /dev/null +++ b/app/images/mouse_none.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/images/mouse_right.png b/app/images/mouse_right.png deleted file mode 100644 index 355b25dc9a03e6d8dc4b956dc51185b5ff8f63e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 513 zcmV+c0{;DpP)X1^@s6HR9gx0005VNkly zv8ozD6o&uV)may2D=R@3@)EbWW$N@+xA6%C$s!2bXrV5OSP52gU*WcSf;@wyunJk3 zz~ZVSJ9DdRoLvVs+=S%s&SC!V%pA@f;C~azC&DmPyWI}uav4e~2q6Ffj^h9TgbH|GwsANd6d{B}QACX3*=+WObN(?5!&7cF8X*XRd~v+|d^*KXA6}r8Lc85Q@vL*h zX0v%bhXMd&tnkcArJ}{p-##HtQ+b}#z?4$m0l;9)~m zJX}3InM|_5V4$vPI-RO846{oN + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/app/images/power.png b/app/images/power.png deleted file mode 100644 index f68fd0813c02a625f3fb5097353b7825f287939f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)r;MP5p&nmUBLf4Yu&0Y-h{y4#VS!wS40wF6uK&wz zeJ1b0dp`q-xCQJ1W&L*#{jK6yZ}@>J>(2+?q>Rf`4Fzff;%=K%nq(i6F}{53M#?;U zLBZzq!>RXNprLZ*hUsB!Gewso}{}^ zyS5zqsqt+q$3eS=bG3syL}aJxswlO({ZMz^<8@I+aHiGz+=bUm8NM^#j@;<7{?FO# zrb}1(?6I-+nz%_ndc~vpE9T2SE8eKOh+S~6fqC~wzI*9n-{OAGRx&S`su`qNAN+(V z&&XBkX?|VBYQH4q-F|DjwzF)~vb(lB%jUA~j<{R9onkH8Tw^2Zl+T|DXQ^pge?vdW h@Q(gJk3Tz4NXMlvaGP>8%>o!=44$rjF6*2UngBV_o1g#y diff --git a/app/images/power.svg b/app/images/power.svg new file mode 100644 index 00000000..4925d3e8 --- /dev/null +++ b/app/images/power.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/images/settings.png b/app/images/settings.png deleted file mode 100644 index a43f5e100b3dc9b5aa30812521b036e085b44a39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2495 zcmZWq2{ap67mm@f1(k}e3~4G-RY|Rth{RgU#IA}GOAR0Knplb(*-?ZwKD}lgz1@N<{mC zgt7iq{!H2pO9THaZ)1wiuP9-HDzwN87W5^b67wj}f|6FgVcZV{$DmL``DtHF2@-Q8d zh$%I5K+clPszf)Il|p}A@vJD$n9C{WNhDL*ayESt3FAtrNO}?%Fwlnclx;Su=g?hr zKNRRh8TTkZ0;x3fJ~1eF7Rl@^SJfjh1?8tcPLH$#DO@0bJe%9UxMr7LaZkHdTg!Fz z!=hd0p?QtY^^Kp;t~EBlA4$6<_(2C-q&j%TaJPALJtSc^JbiioiDL4=+J$tq^T^0d z&x7O7JDB!3ehcH66a~_W$uwh8Z#J=`!hU^z>uFf@9NwVdygn1Vh1Wlc3n9Y%=mI8(BcN1nS zEePZnwlVha!*sbf9kX`AfW4X-pJ2ZnaS?4STPA!y87N{KE7P9U`tFa{{2(ZQ#% zrMZqhT???c__y<<`qqSX<1X2)n;Ir=oEBy4y9ub3Kka~U0cOlRciJ8KUNE=mmB-X} zgHSml%67OZC-EzY}Wq@|xUU!dVO-_)Phm|uA*BXtD6VUSWHDOXk5 zQW@ner1m7A7+_V+um$kLs5V)vKi*mbT?0nMJyMTPE%iBn(^gG{mMu!i619A6F^s6t zPhI4%bsY5IQanRZt0(Dm@?W3NBhH)?Ajm!I3q{D}X@$)QS40p_%}b3aVRGxD)AUD( zVd1yP(_7%tX$G?7L{c#~*8WCV*!-$w%g7#4cprIH!m5EeBsC=|^yfPO=u z_x*z~5(@bSVO%tTx>(ymOlTowh&DnSaTnlwsRxA~|A*v%Jf`GOVhDxKpwNOK2fW@s zv@nJN6naqT`}#*GiTJ$^JuD>fTNM%!Ne(1a$w7=zqy|C*`JH?}7egYVL&JOm$VA3B z-QVfHHDK)Rhx}I?9Ukd+e|66pwf2;@w`F)mHky)IW5dd(g$I{fu$wG|d ze9__p6WkgfAMeb09B}J?dhyshtD+}vXUM0e9zf;!?ae{B&)zxhEoBoisbXBO1aw(x zl*GMvTX_S=d^jd{A8$~k#v&fHc)T}DPR%wg=#JtcHbZEq@Y^?@@9p%W_V&W>U55SA z#A#Rvpi(2J=jXq??(U9%IX+GZHZp4a`f@ara5j$gw!dF0pk})#DJiKmI5@b4Kp+Tb zHLG3D^a=}Ghle!m6jxNx<31D>7Mg8;{_I{@os%Q~;p4}WS=N*H0|OrXi@AW<*QQRYsojc=jg3?QgV&x94`)Xbi3K4M5j#eB{HqJOxw)QjW#!*4*0??F^CwA5 zON(A=-u=D2yxa|k!%Oz#1|QV&Q{*^0IE>%4P(BuDGkIG$vn{WW0DGDD z<(w>>;B_?i2aDQK-)hxZ?e@O&qA!N7L-o4)I!+on&f4Jd#cw+6GOfspzWpOk>Z9$l z4Vjsl>&IS4eC5r~dUSVpZym9x&(8QO$;oXAq8jR2TlYi_m@x`CnPjPYm4_uIRyNkw zkqS~$VX;m4%$ml=#_HPI4M6oA2(#IBx3Q?t-+^u>4Fa{rYkGKiRNon7Zf|enF@31) ziXp0(*W|-cO^Q22aAR(c0_=*Dvf`KxX@N7Qg9lj>L+#T%psklIsw{7K)eV@tgTFql zxu=^-@3?*YwymG9Z_?w!wqW7H^7(}Y#?&}CSqeWTk!e+!n3A%={jh}-I`?*e+7rea z_@vQ>1{5wu<)|8u$8*jYr+HOnFMRqG6{ivzb7&%ZW%Si6yuIYrJ;9^(;gOM%A;G~K z>YAEW5=`n}D@&(yMPV@5-rSV?^e+)&ChVLX{WlR40s;bOO|1CqGcz(6bp-{)=OEB! zTiae>{#7Qwgq7X?J9Kh%T$Q7vqHSAFz{OQ`JcOcKRDYZCFw}&Ux3om%I>yw`#|z|n zUlKHYQJnLb55uSVnr?6NtOE5cX-q_q{|&zGO$v7#sNWPVA1%QlSze#%Y5)KL diff --git a/app/images/settings.svg b/app/images/settings.svg new file mode 100644 index 00000000..dbb2e80a --- /dev/null +++ b/app/images/settings.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/tab.png b/app/images/tab.png deleted file mode 100644 index 84134872a881a627f745a432a3c951254cda26cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^k|4~%1|*NXY)uAIY)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP)y9AGr(VqkNfCexMc)B=-cpQH_#nA7tgMe!?hZ5_{ zMsAl!Od$nK=NL4coJ!_2I4fwan^WPez236Q zxV~)Pmbreb!~RE$eG$IAmU)pz*R|Ls(Q=zi{&XKq&Aq}nY2Dc~42<~)YG*C`o4+Gi zfzhrp-BY~xz4ptrjS(^y&#NZ29sIEF7IV+@&p+#?rS3j(MVx!X=$#3b5uMxna)02 zw(86+ea1O6Bj-JL&z$wG@ph=1;^e&(PHLnW`jei;9RARTq fhprAg`;}G9rs2;o<>%Ld!NuU|>gTe~DWM4f3=5y^ diff --git a/app/images/tab.svg b/app/images/tab.svg new file mode 100644 index 00000000..1ccb3229 --- /dev/null +++ b/app/images/tab.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/app/images/toggleextrakeys.png b/app/images/toggleextrakeys.png deleted file mode 100644 index ad8e0a70d3eb7b5f4462f808ab987053fa4b0e56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 735 zcmV<50wDc~P)P000>X1^@s6#OZ}&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-3J*792=g7&T0Sv0&Gb{ zK~zY`y;U)56hRdJ-ppi!CtQjM#~p&_LBP(=X7C5tSO^h>^eIGa36M2V8-Xh|B|c z0YCsW^8Y^p1OTrA>=4nxq$KMA7Do8L0eDPA`$bOYTou6Q5WwB?-B01PwB;o+fp z@6A9JPGh|HW^;3MYIb&(lu~2>JOprWOfs+X(zO7*G{&IWY)S}WI)qTwS_1&I)`+60 zs=k~97!Nf5{J2{SKSYZsk! zq|T{@Q;fA%^7^l4S+>${x24zX0RVcv9@_1;WLdVNwO;Mx$$S4qYyEJ{)3QZGpGKVQ zol@%Q+}s@P?d=IOf3Vg*D|!~HBfiQIZhFw~y}v0UHRs&Y_V#w_oLlObbksbl7bJR= zW!a-Biry2^oe}TWL6&6?c6WEL1K79Lh9b~<$*uY=B349xlweX3xmDIsHAW{M0OxBN zM#hm`x?LGbnNFs>_XJ?74D&DZq%~Aq4WMI;iJAF}QtEy|Z#01TB4*CS_6MPNtqWrX RMSuVR002ovPDHLkV1j-yMWX-! diff --git a/app/images/toggleextrakeys.svg b/app/images/toggleextrakeys.svg new file mode 100644 index 00000000..b578c0d4 --- /dev/null +++ b/app/images/toggleextrakeys.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/vnc.html b/vnc.html index 1987bb4c..b5bd2822 100644 --- a/vnc.html +++ b/vnc.html @@ -55,21 +55,21 @@
    -
    - - - - - - -
    @@ -113,7 +113,7 @@
    -
    @@ -125,12 +125,12 @@
    - -
    @@ -170,10 +170,10 @@
    - -
    From b0c6d3c6aa15e90642048c2f789c5e19bfbb7a63 Mon Sep 17 00:00:00 2001 From: samhed Date: Thu, 25 Aug 2016 16:55:55 +0200 Subject: [PATCH 216/527] Remove redundant modifier state variables --- app/ui.js | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/app/ui.js b/app/ui.js index 234af0aa..21f28cbb 100644 --- a/app/ui.js +++ b/app/ui.js @@ -50,9 +50,6 @@ var UI; lastKeyboardinput: null, defaultKeyboardinputLen: 100, - ctrlOn: false, - altOn: false, - // Setup rfb object, load settings from browser storage, then call // UI.init to setup the UI/menus load: function(callback) { @@ -1280,31 +1277,25 @@ var UI; toggleCtrl: function() { UI.keepKeyboard(); - if(UI.ctrlOn === false) { - UI.rfb.sendKey(KeyTable.XK_Control_L, true); - document.getElementById('noVNC_toggle_ctrl_button') - .classList.add("noVNC_selected"); - UI.ctrlOn = true; - } else if(UI.ctrlOn === true) { + var btn = document.getElementById('noVNC_toggle_ctrl_button'); + if (btn.classList.contains("noVNC_selected")) { UI.rfb.sendKey(KeyTable.XK_Control_L, false); - document.getElementById('noVNC_toggle_ctrl_button') - .classList.remove("noVNC_selected"); - UI.ctrlOn = false; + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Control_L, true); + btn.classList.add("noVNC_selected"); } }, toggleAlt: function() { UI.keepKeyboard(); - if(UI.altOn === false) { - UI.rfb.sendKey(KeyTable.XK_Alt_L, true); - document.getElementById('noVNC_toggle_alt_button') - .classList.add("noVNC_selected"); - UI.altOn = true; - } else if(UI.altOn === true) { + var btn = document.getElementById('noVNC_toggle_alt_button'); + if (btn.classList.contains("noVNC_selected")) { UI.rfb.sendKey(KeyTable.XK_Alt_L, false); - document.getElementById('noVNC_toggle_alt_button') - .classList.remove("noVNC_selected"); - UI.altOn = false; + btn.classList.remove("noVNC_selected"); + } else { + UI.rfb.sendKey(KeyTable.XK_Alt_L, true); + btn.classList.add("noVNC_selected"); } }, From 24d8b311ca3442b26dd0f0e0fce8dd0ba4763e81 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 25 Aug 2016 20:11:11 +0200 Subject: [PATCH 217/527] Bling up popups Make the popups a bit more prominent by adding shadows and animations. --- app/styles/base.css | 68 +++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index e838b1de..c0d7fceb 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -88,33 +88,30 @@ html { /* Panels */ .noVNC_panel { - display: none; - position: relative; + position: fixed; + top: 46px; + right: 15px; + transform: translate(0, -50px); + + transition: 0.5s ease-in-out; + + visibility: hidden; + opacity: 0; + padding: 15px; - color: #fff; - background: #fff; /* default background for browsers without gradient support */ - /* css3 */ - /*background: -webkit-gradient(linear, 0 0, 0 100%, from(#2e88c4), to(#075698)); - background: -moz-linear-gradient(#2e88c4, #075698); - background: -o-linear-gradient(#2e88c4, #075698); - background: linear-gradient(#2e88c4, #075698);*/ + + background: #fff; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; color: #000; border: 2px solid #E0E0E0; -} -.noVNC_panel:after { - content: ""; - position: absolute; - top: -40px; /* value = - border-top-width - border-bottom-width */ - right: 50px; /* controls horizontal position */ - border-style: solid; - border-width: 40px 40px 0 0; /* vary these values to change the angle of the vertex */ - border-color: transparent #E0E0E0; + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); } .noVNC_panel.noVNC_open { - display: block; + visibility: visible; + opacity: 1; + transform: translate(0, 0); } /* noVNC Touch Device only buttons */ @@ -186,12 +183,6 @@ html { /* XVP Shutdown/Reboot */ #noVNC_xvp { - top: 73px; - right: 30px; - position: fixed; -} -#noVNC_xvp:after { - right: 125px; } #noVNC_xvp_buttons { display: none; @@ -199,12 +190,6 @@ html { /* Clipboard */ #noVNC_clipboard { - top: 73px; - right: 30px; - position: fixed; -} -#noVNC_clipboard:after { - right: 85px; } #noVNC_clipboard_text { width: 500px; @@ -215,9 +200,6 @@ html { /* Settings */ #noVNC_settings { - top: 73px; - right: 20px; - position: fixed; } #noVNC_settings ul { list-style: none; @@ -233,12 +215,6 @@ html { /* Connection Controls */ #noVNC_connect_controls { - top: 73px; - right: 12px; - position: fixed; -} -#noVNC_connect_controls:after { - right:15px; } #noVNC_connect_controls ul { list-style: none; @@ -268,15 +244,21 @@ html { */ #noVNC_status { - display: none; position: fixed; top: 0; left: 0; width: 100%; z-index: 3; + transform: translateY(-100%); + + transition: 0.5s ease-in-out; + + visibility: hidden; + opacity: 0; padding: 5px; + display: flex; flex-direction: row; justify-content: center; align-content: center; @@ -288,7 +270,9 @@ html { border-bottom: 1px solid rgba(0, 0, 0, 0.9); } #noVNC_status.noVNC_open { - display: flex; + transform: translateY(0); + visibility: visible; + opacity: 1; } #noVNC_status::before { From 79fd3b1fcff7204b836928e64a9f634fe5e179ec Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 30 Aug 2016 15:23:20 +0200 Subject: [PATCH 218/527] Apply CSS style to input elements Different browsers have wildly different defaults for input elements. Try to get a consistent interface by applying our own style. --- app/styles/base.css | 85 +++++++++++++++++++++++++++++++++++++++------ vnc.html | 6 ++-- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index c0d7fceb..81c45169 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -26,6 +26,77 @@ html { white-space: nowrap; } +/* ---------------------------------------- + * Input Elements + * ---------------------------------------- + */ + +input[type=input], input[type=password], input:not([type]), textarea { + /* Disable default rendering */ + -webkit-appearance: none; + -moz-appearance: none; + background: none; + + margin: 2px; + padding: 2px; + border: 1px solid rgb(192, 192, 192); + border-radius: 5px; + color: black; + background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); +} + +input[type=button], select { + /* Disable default rendering */ + -webkit-appearance: none; + -moz-appearance: none; + background: none; + + margin: 2px; + padding: 2px; + border: 1px solid rgb(192, 192, 192); + border-bottom-width: 2px; + border-radius: 5px; + color: black; + background: linear-gradient(to top, rgb(255, 255, 255), rgb(240, 240, 240)); + + /* This avoids it jumping around when :active */ + vertical-align: middle; +} + +input[type=button] { + padding-left: 20px; + padding-right: 20px; +} + +option { + color: black; + background: white; +} + +input[type=input]:focus, input[type=password]:focus, +input:not([type]):focus, input[type=button]:focus, +textarea:focus, select:focus { + box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5); + border-color: rgb(74, 144, 217); + outline: none; +} + +input[type=button]::-moz-focus-inner { + border: none; +} + +input[type=input]:disabled, input[type=password]:disabled, +input:not([type]):disabled, input[type=button]:disabled, +textarea:disabled, select:disabled { + color: rgb(128, 128, 128); + background: rgb(240, 240, 240); +} + +input[type=button]:active, select:active { + border-bottom-width: 1px; + margin-top: 3px; +} + /* ---------------------------------------- * Control Bar * ---------------------------------------- @@ -114,6 +185,10 @@ html { transform: translate(0, 0); } +.noVNC_submit { + float: right; +} + /* noVNC Touch Device only buttons */ #noVNC_mobile_buttons { display: inline; @@ -194,9 +269,6 @@ html { #noVNC_clipboard_text { width: 500px; } -#noVNC_clipboard_clear_button { - float: right; -} /* Settings */ #noVNC_settings { @@ -209,9 +281,6 @@ html { #noVNC_setting_path { width: 100px; } -#noVNC_settings_apply { - float: right; -} /* Connection Controls */ #noVNC_connect_controls { @@ -233,10 +302,6 @@ html { #noVNC_setting_password { width: 150px; } -#noVNC_connect_button { - width: 110px; - float: right; -} /* ---------------------------------------- * Status Dialog diff --git a/vnc.html b/vnc.html index b5bd2822..6c2b9d65 100644 --- a/vnc.html +++ b/vnc.html @@ -121,7 +121,7 @@
    + value="Clear" class="noVNC_submit" />
    @@ -165,7 +165,7 @@
    -
  • +
  • @@ -182,7 +182,7 @@
  • -
  • +
  • From c8d4402f582e4d4fb62ca75aae3241f9e0917190 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 25 Aug 2016 20:45:52 +0200 Subject: [PATCH 219/527] Move touch text input out of control bar It has nothing to do with the control bar, so hide it somewhere more sensible. --- vnc.html | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vnc.html b/vnc.html index 6c2b9d65..a660988b 100644 --- a/vnc.html +++ b/vnc.html @@ -72,13 +72,6 @@ - -
    @@ -197,6 +190,14 @@
    + + + Canvas not supported. From 8d7708c82edc9c0d3dc22a28ded5a563ce786bb2 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 14:42:03 +0200 Subject: [PATCH 220/527] Abstract status dialog CSS class handling --- app/ui.js | 85 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/app/ui.js b/app/ui.js index 21f28cbb..9689852a 100644 --- a/app/ui.js +++ b/app/ui.js @@ -308,43 +308,33 @@ var UI; updateState: function(rfb, state, oldstate, msg) { UI.rfb_state = state; - var klass; - var timeout; - switch (state) { - case 'failed': - case 'fatal': - klass = "noVNC_status_error"; - timeout = 0; // zero means no timeout - break; - case 'normal': - klass = "noVNC_status_normal"; - break; - case 'disconnected': - /* falls through */ - case 'loaded': - klass = "noVNC_status_normal"; - break; - case 'password': - UI.toggleConnectPanel(); - - document.getElementById('noVNC_connect_button').value = "Send Password"; - document.getElementById('noVNC_connect_button').onclick = UI.setPassword; - document.getElementById('noVNC_setting_password').focus(); - - klass = "noVNC_status_warn"; - break; - default: - klass = "noVNC_status_warn"; - break; - } if (typeof(msg) !== 'undefined') { - document.getElementById('noVNC_status') - .classList.remove("noVNC_status_normal", - "noVNC_status_warn", - "noVNC_status_error"); - document.getElementById('noVNC_status').classList.add(klass); - UI.showStatus(msg, timeout); + switch (state) { + case 'failed': + case 'fatal': + // zero means no timeout + UI.showStatus(msg, 'error', 0); + break; + case 'normal': + /* falls through */ + case 'disconnected': + case 'loaded': + UI.showStatus(msg, 'normal'); + break; + case 'password': + UI.toggleConnectPanel(); + + document.getElementById('noVNC_connect_button').value = "Send Password"; + document.getElementById('noVNC_connect_button').onclick = UI.setPassword; + document.getElementById('noVNC_setting_password').focus(); + + UI.showStatus(msg, 'warn'); + break; + default: + UI.showStatus(msg, 'warn'); + break; + } } UI.updateVisualState(); @@ -434,11 +424,34 @@ var UI; //Util.Debug("<< updateVisualState"); }, - showStatus: function(text, time) { + showStatus: function(text, status_type, time) { var statusElem = document.getElementById('noVNC_status'); clearTimeout(UI.statusTimeout); + if (typeof status_type === 'undefined') { + status_type = 'normal'; + } + + statusElem.classList.remove("noVNC_status_normal", + "noVNC_status_warn", + "noVNC_status_error"); + + switch (status_type) { + case 'warning': + case 'warn': + statusElem.classList.add("noVNC_status_warn"); + break; + case 'error': + statusElem.classList.add("noVNC_status_error"); + break; + case 'normal': + case 'info': + default: + statusElem.classList.add("noVNC_status_normal"); + break; + } + statusElem.innerHTML = text; statusElem.classList.add("noVNC_open"); From 8a7ec6ea1919741035e35d111a5680eb396a0bdc Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 29 Aug 2016 14:46:58 +0200 Subject: [PATCH 221/527] Add a separate dialog for the password prompt The user might be queried for the password during the connect stage if no password was previously provided. Add a separate dialog for this rather than abusing the connect dialog. --- app/styles/base.css | 49 +++++++++++++++++++++++++++++++++++++++++++++ app/ui.js | 26 ++++++++++++++---------- vnc.html | 10 +++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 81c45169..96f830bb 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -97,6 +97,32 @@ input[type=button]:active, select:active { margin-top: 3px; } +/* ---------------------------------------- + * WebKit centering hacks + * ---------------------------------------- + */ + +.noVNC_center { + /* + * This is a workaround because webkit misrenders transforms and + * uses non-integer coordinates, resulting in blurry content. + * Ideally we'd use "top: 50%; transform: translateY(-50%);" on + * the objects instead. + */ + display: flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; +} +.noVNC_center > * { + pointer-events: auto; +} + /* ---------------------------------------- * Control Bar * ---------------------------------------- @@ -367,6 +393,29 @@ input[type=button]:active, select:active { content: url("../images/warning.svg") " "; } +/* ---------------------------------------- + * Password Dialog + * ---------------------------------------- + */ + +#noVNC_password_dlg { + transform: translateY(-50px); +} +#noVNC_password_dlg.noVNC_open { + transform: translateY(0); +} +#noVNC_password_dlg ul { + list-style: none; + margin: 0px; + padding: 0px; +} +#noVNC_password_dlg li { + padding-bottom:8px; +} +#noVNC_password_input { + width: 150px; +} + /* ---------------------------------------- * Main Area * ---------------------------------------- diff --git a/app/ui.js b/app/ui.js index 9689852a..33e0e997 100644 --- a/app/ui.js +++ b/app/ui.js @@ -249,6 +249,9 @@ var UI; .addEventListener('click', UI.disconnect); document.getElementById("noVNC_connect_button") .addEventListener('click', UI.connect); + + document.getElementById("noVNC_password_button") + .addEventListener('click', UI.setPassword); }, addClipboardHandlers: function() { @@ -323,11 +326,11 @@ var UI; UI.showStatus(msg, 'normal'); break; case 'password': - UI.toggleConnectPanel(); - - document.getElementById('noVNC_connect_button').value = "Send Password"; - document.getElementById('noVNC_connect_button').onclick = UI.setPassword; - document.getElementById('noVNC_setting_password').focus(); + document.getElementById('noVNC_password_dlg') + .classList.add('noVNC_open'); + setTimeout(function () { + document.getElementById(('noVNC_password_input').focus()); + }, 100); UI.showStatus(msg, 'warn'); break; @@ -397,6 +400,10 @@ var UI; // It is enabled (toggled) by direct click on the button UI.setViewDrag(false); + // State change also closes the password dialog + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + switch (UI.rfb_state) { case 'fatal': case 'failed': @@ -827,12 +834,9 @@ var UI; }, setPassword: function() { - UI.rfb.sendPassword(document.getElementById('noVNC_setting_password').value); - //Reset connect button. - document.getElementById('noVNC_connect_button').value = "Connect"; - document.getElementById('noVNC_connect_button').onclick = UI.connect; - //Hide connection panel. - UI.toggleConnectPanel(); + UI.rfb.sendPassword(document.getElementById('noVNC_password_input').value); + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); return false; }, diff --git a/vnc.html b/vnc.html index a660988b..9c677eec 100644 --- a/vnc.html +++ b/vnc.html @@ -185,6 +185,16 @@
    + +
    +
    +
      +
    • +
    • +
    +
    +
    +

    no
    VNC

    From d616c9226cb06b82e68d1d8cbedcb7ea41b0fe2c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 25 Aug 2016 20:49:52 +0200 Subject: [PATCH 222/527] Merge control bar sections There is no status information in the centre anymore, so merge the left and right sections of the control bar. --- app/styles/base.css | 15 +++------------ vnc.html | 4 +--- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 96f830bb..3edd2adc 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -265,21 +265,12 @@ input[type=button]:active, select:active { display: inline; } -/* Left side buttons */ -.noVNC_buttons_left { - float: left; +.noVNC_buttons { + float: right; z-index: 1; position: relative; - padding-left: 10px; -} - -/* Right side buttons */ -.noVNC_buttons_right { - float: right; - right: 0px; - z-index: 2; - position: absolute; padding-right: 10px; + padding-left: 10px; } /* XVP Shutdown/Reboot */ diff --git a/vnc.html b/vnc.html index 9c677eec..f20b00c3 100644 --- a/vnc.html +++ b/vnc.html @@ -52,7 +52,7 @@
    -
    +
    -
    -
    Date: Thu, 25 Aug 2016 20:52:46 +0200 Subject: [PATCH 223/527] Enable extra keys for all devices Manual special keys can be useful on all devices, so stop restricting it to just touch devices. --- app/ui.js | 3 +++ vnc.html | 28 +++++++++++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/ui.js b/app/ui.js index 33e0e997..4b34e243 100644 --- a/app/ui.js +++ b/app/ui.js @@ -84,6 +84,7 @@ var UI; UI.setupFullscreen(); UI.addControlbarHandlers(); UI.addTouchSpecificHandlers(); + UI.addExtraKeysHandlers(); UI.addXvpHandlers(); UI.addConnectionControlHandlers(); UI.addClipboardHandlers(); @@ -218,7 +219,9 @@ var UI; .addEventListener('submit', function () { return false; }); window.addEventListener('load', UI.keyboardinputReset); + }, + addExtraKeysHandlers: function() { document.getElementById("noVNC_toggle_extra_keys_button") .addEventListener('click', UI.toggleExtraKeys); document.getElementById("noVNC_toggle_ctrl_button") diff --git a/vnc.html b/vnc.html index f20b00c3..8676e83d 100644 --- a/vnc.html +++ b/vnc.html @@ -72,19 +72,21 @@ -
    - -
    - - - - -
    +
    + + +
    + +
    + + + +
    From f7c725cecfd591b7f221863da74a9965fa297504 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 25 Aug 2016 21:02:49 +0200 Subject: [PATCH 224/527] Improve active button visual response --- app/styles/base.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/styles/base.css b/app/styles/base.css index 3edd2adc..a462eae6 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -179,6 +179,13 @@ input[type=button]:active, select:active { .noVNC_button:disabled { opacity: 0.4; } +.noVNC_button:focus { + outline: none; +} +.noVNC_button:active { + padding-top: 5px; + padding-bottom: 3px; +} .noVNC_button.noVNC_hidden { display: none; } From 8434cc81b39fb6a64854d8844798e6307995e735 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 10:46:20 +0200 Subject: [PATCH 225/527] Make status dialog independent of control bar --- vnc.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vnc.html b/vnc.html index 8676e83d..9a6aa29b 100644 --- a/vnc.html +++ b/vnc.html @@ -180,11 +180,11 @@
    - -
    -
    + +
    +
    From 6244e383e49855772713df97e0bad1560b0424f7 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Wed, 14 Sep 2016 16:10:24 +0200 Subject: [PATCH 226/527] Let CSS update UI for touch and connected state Avoid a lot of JavaScript code that can easily be handed using style sheets instead, specifically items that should only be shown when on a touch device, or items that depend on the connected state. --- app/styles/base.css | 32 +++++++++++-- app/ui.js | 112 +++++++++++++------------------------------- vnc.html | 4 +- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index a462eae6..ed77c6b9 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -222,11 +222,18 @@ input[type=button]:active, select:active { float: right; } +:root:not(.noVNC_connected) #noVNC_view_drag_button { + display: none; +} + /* noVNC Touch Device only buttons */ #noVNC_mobile_buttons { display: inline; } -#noVNC_mobile_buttons.noVNC_hidden { +:root:not(.noVNC_connected) #noVNC_mobile_buttons { + display: none; +} +:root:not(.noVNC_touch) #noVNC_mobile_buttons { display: none; } @@ -242,7 +249,7 @@ input[type=button]:active, select:active { ime-mode: disabled; } -#noVNC_toggle_extra_keys_button { +:root:not(.noVNC_connected) #noVNC_extra_keys { display: none; } @@ -280,7 +287,15 @@ input[type=button]:active, select:active { padding-left: 10px; } +/* Send Ctrl+Alt+Delete */ +:root:not(.noVNC_connected) #noVNC_send_ctrl_alt_del_button { + display: none; +} + /* XVP Shutdown/Reboot */ +:root:not(.noVNC_connected) #noVNC_xvp_button { + display: none; +} #noVNC_xvp { } #noVNC_xvp_buttons { @@ -288,6 +303,9 @@ input[type=button]:active, select:active { } /* Clipboard */ +:root:not(.noVNC_connected) #noVNC_clipboard_button { + display: none; +} #noVNC_clipboard { } #noVNC_clipboard_text { @@ -307,7 +325,11 @@ input[type=button]:active, select:active { } /* Connection Controls */ -#noVNC_connect_controls { +:root.noVNC_connected #noVNC_connect_controls_button { + display: none; +} +:root:not(.noVNC_connected) #noVNC_disconnect_button { + display: none; } #noVNC_connect_controls ul { list-style: none; @@ -440,7 +462,7 @@ input[type=button]:active, select:active { width: auto; height: auto; } -#noVNC_screen.noVNC_hidden { +:root:not(.noVNC_connected) #noVNC_screen { display: none; } @@ -480,7 +502,7 @@ input[type=button]:active, select:active { -1px 1px 0 #000, 1px 1px 0 #000; } -#noVNC_logo.noVNC_hidden { +:root.noVNC_connected #noVNC_logo { display: none; } diff --git a/app/ui.js b/app/ui.js index 4b34e243..6c09e150 100644 --- a/app/ui.js +++ b/app/ui.js @@ -66,12 +66,9 @@ var UI; UI.initSettings(); - // Show mouse selector buttons on touch screen devices + // Adapt the interface for touch screen devices if (UI.isTouchDevice) { - // Show mobile buttons - document.getElementById('noVNC_mobile_buttons') - .classList.remove("noVNC_hidden"); - UI.hideMouseButton(); + document.documentElement.classList.add("noVNC_touch"); // Remove the address bar setTimeout(function() { window.scrollTo(0, 1); }, 100); UI.forceSetting('clip', true); @@ -368,34 +365,11 @@ var UI; document.getElementById('noVNC_setting_repeaterID').disabled = connected; if (connected) { - document.getElementById('noVNC_logo') - .classList.add("noVNC_hidden"); - document.getElementById('noVNC_screen') - .classList.remove("noVNC_hidden"); + document.documentElement.classList.add("noVNC_connected"); UI.updateViewClip(); UI.setMouseButton(1); - document.getElementById('noVNC_clipboard_button') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_keyboard_button') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_extra_keys') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_send_ctrl_alt_del_button') - .classList.remove("noVNC_hidden"); } else { - document.getElementById('noVNC_logo') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_screen') - .classList.add("noVNC_hidden"); - UI.hideMouseButton(); - document.getElementById('noVNC_clipboard_button') - .classList.add("noVNC_hidden"); - document.getElementById('noVNC_keyboard_button') - .classList.add("noVNC_hidden"); - document.getElementById('noVNC_extra_keys') - .classList.add("noVNC_hidden"); - document.getElementById('noVNC_send_ctrl_alt_del_button') - .classList.add("noVNC_hidden"); + document.documentElement.classList.remove("noVNC_connected"); UI.updateXvpButton(0); } @@ -411,23 +385,11 @@ var UI; case 'fatal': case 'failed': case 'disconnected': - document.getElementById('noVNC_connect_controls_button') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_disconnect_button') - .classList.add("noVNC_hidden"); UI.openConnectPanel(); break; case 'loaded': - document.getElementById('noVNC_connect_controls_button') - .classList.remove("noVNC_hidden"); - document.getElementById('noVNC_disconnect_button') - .classList.add("noVNC_hidden"); break; default: - document.getElementById('noVNC_connect_controls_button') - .classList.add("noVNC_hidden"); - document.getElementById('noVNC_disconnect_button') - .classList.remove("noVNC_hidden"); break; } @@ -1086,52 +1048,48 @@ var UI; updateViewDrag: function() { var clipping = false; + if (UI.rfb_state !== 'normal') return; + // Check if viewport drag is possible. It is only possible // if the remote display is clipping the client display. - if (UI.rfb_state === 'normal' && - UI.rfb.get_display().get_viewport() && + if (UI.rfb.get_display().get_viewport() && UI.rfb.get_display().clippingDisplay()) { clipping = true; } var viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (UI.rfb_state !== 'normal') { - // Always hide when not connected - viewDragButton.classList.add("noVNC_hidden"); + if (!clipping && + UI.rfb.get_viewportDrag()) { + // The size of the remote display is the same or smaller + // than the client display. Make sure viewport drag isn't + // active when it can't be used. + UI.rfb.set_viewportDrag(false); + } + + if (UI.rfb.get_viewportDrag()) { + viewDragButton.classList.add("noVNC_selected"); } else { - if (!clipping && - UI.rfb.get_viewportDrag()) { - // The size of the remote display is the same or smaller - // than the client display. Make sure viewport drag isn't - // active when it can't be used. - UI.rfb.set_viewportDrag(false); - } + viewDragButton.classList.remove("noVNC_selected"); + } - if (UI.rfb.get_viewportDrag()) { - viewDragButton.classList.add("noVNC_selected"); - } else { - viewDragButton.classList.remove("noVNC_selected"); - } + // Different behaviour for touch vs non-touch + // The button is disabled instead of hidden on touch devices + if (UI.isTouchDevice) { + viewDragButton.classList.remove("noVNC_hidden"); - // Different behaviour for touch vs non-touch - // The button is disabled instead of hidden on touch devices - if (UI.isTouchDevice) { - viewDragButton.classList.remove("noVNC_hidden"); - - if (clipping) { - viewDragButton.disabled = false; - } else { - viewDragButton.disabled = true; - } - } else { + if (clipping) { viewDragButton.disabled = false; + } else { + viewDragButton.disabled = true; + } + } else { + viewDragButton.disabled = false; - if (clipping) { - viewDragButton.classList.remove("noVNC_hidden"); - } else { - viewDragButton.classList.add("noVNC_hidden"); - } + if (clipping) { + viewDragButton.classList.remove("noVNC_hidden"); + } else { + viewDragButton.classList.add("noVNC_hidden"); } } }, @@ -1329,10 +1287,6 @@ var UI; * MISC * ------v------*/ - hideMouseButton: function() { - UI.setMouseButton(-1); - }, - setMouseButton: function(num) { if (UI.rfb) { UI.rfb.get_mouse().set_touchButton(num); diff --git a/vnc.html b/vnc.html index 9a6aa29b..8b68ff50 100644 --- a/vnc.html +++ b/vnc.html @@ -60,7 +60,7 @@ title="Move/Drag Viewport" /> -
    +
    no
    VNC -
    +
    -
    - -
    +
    +
    -
    +
    +
    +
    @@ -99,16 +100,19 @@ +
    +
    +
    @@ -116,6 +120,7 @@
    +
    +
    • Encrypt
    • @@ -161,6 +167,7 @@
    +
    +
    • @@ -178,7 +186,8 @@
    -
    +
    +
    From 4d3aa0ef9d3cc5baab835ad2b545cf80b77c7ae7 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 12:24:57 +0200 Subject: [PATCH 228/527] Add logo to control bar --- app/styles/base.css | 20 ++++++++++++-------- vnc.html | 4 +++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 3f6ccdf8..df10c7df 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -468,14 +468,22 @@ input[type=button]:active, select:active { url('Orbitron700.ttf') format('truetype'); } -#noVNC_logo { - margin-top: 60px; - margin-left: 60px; +.noVNC_logo { color:yellow; text-align:left; font-family: 'Orbitron', 'OrbitronTTF', sans-serif; - font-size: 180px; + font-size: 13px; line-height:90%; + text-shadow: 1px 1px 0 #000; +} +.noVNC_logo span{ + color:green; +} + +#noVNC_logo { + margin-top: 60px; + margin-left: 60px; + font-size: 180px; text-shadow: 5px 5px 0 #000, -1px -1px 0 #000, @@ -487,10 +495,6 @@ input[type=button]:active, select:active { display: none; } -#noVNC_logo span{ - color:green; -} - /* ---------------------------------------- * Media sizing * ---------------------------------------- diff --git a/vnc.html b/vnc.html index 4f6aa876..aeb63f8d 100644 --- a/vnc.html +++ b/vnc.html @@ -53,6 +53,8 @@
    +

    no
    VNC

    +
    -

    no
    VNC

    +

    no
    VNC

    From 728b5d9e1a32c16adef006fc25235bbb27d36172 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 12:37:09 +0200 Subject: [PATCH 229/527] Make control bar transparent when idle --- app/styles/base.css | 5 +++++ app/ui.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/app/styles/base.css b/app/styles/base.css index df10c7df..179a20b7 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -142,9 +142,14 @@ input[type=button]:active, select:active { position: fixed; z-index: 2; + transition: 0.5s ease-in-out; + /* Edge misrenders animations wihthout this */ transform: translateX(0); } +:root.noVNC_connected #noVNC_control_bar_anchor.noVNC_idle { + opacity: 0.8; +} #noVNC_control_bar { position: relative; diff --git a/app/ui.js b/app/ui.js index 27187fb1..6e2ff03f 100644 --- a/app/ui.js +++ b/app/ui.js @@ -41,6 +41,7 @@ var UI; resizeTimeout: null, statusTimeout: null, hideKeyboardTimeout: null, + controlbarTimeout: null, keyboardVisible: false, @@ -188,6 +189,15 @@ var UI; }, addControlbarHandlers: function() { + document.getElementById("noVNC_control_bar") + .addEventListener('mousemove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mouseup', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keypress', UI.activateControlbar); + document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); document.getElementById("noVNC_send_ctrl_alt_del_button") @@ -213,6 +223,15 @@ var UI; document.getElementById("noVNC_keyboardinput") .addEventListener('submit', function () { return false; }); + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchmove', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchend', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.activateControlbar); + window.addEventListener('load', UI.keyboardinputReset); }, @@ -441,6 +460,20 @@ var UI; document.getElementById('noVNC_status').classList.remove("noVNC_open"); }, + activateControlbar: function() { + clearTimeout(UI.controlbarTimeout); + // We manipulate the anchor instead of the actual control + // bar in order to avoid creating new a stacking group + document.getElementById('noVNC_control_bar_anchor') + .classList.remove("noVNC_idle"); + UI.controlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + }, + + idleControlbar: function() { + document.getElementById('noVNC_control_bar_anchor') + .classList.add("noVNC_idle"); + }, + /* ------^------- * /VISUAL * ============== From 38323d4d92df7fd5f6e95a2adc2d7d91a32c9039 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 13:24:25 +0200 Subject: [PATCH 230/527] Add handle to open and close the control bar --- app/images/handle.svg | 82 +++++++++++++++++++ app/images/handle_bg.svg | 172 +++++++++++++++++++++++++++++++++++++++ app/styles/base.css | 43 +++++++++- app/styles/black.css | 2 +- app/styles/blue.css | 2 +- app/ui.js | 29 +++++++ vnc.html | 10 ++- 7 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 app/images/handle.svg create mode 100644 app/images/handle_bg.svg diff --git a/app/images/handle.svg b/app/images/handle.svg new file mode 100644 index 00000000..4a7a126f --- /dev/null +++ b/app/images/handle.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/app/images/handle_bg.svg b/app/images/handle_bg.svg new file mode 100644 index 00000000..b4c4d131 --- /dev/null +++ b/app/images/handle_bg.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/app/styles/base.css b/app/styles/base.css index 179a20b7..ec682c46 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -153,18 +153,55 @@ input[type=button]:active, select:active { #noVNC_control_bar { position: relative; - left: -30px; + /* left: calc(-35px - 10px - 5px - 30px), but IE doesn't animate calc */ + left: -80px; + + transition: 0.5s ease-in-out; background-color: rgb(110, 132, 163); border-radius: 0 10px 10px 0; - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); - /* The extra border is to get a proper shadow */ border-color: rgb(110, 132, 163); border-style: solid; border-width: 0 0 0 30px; } +#noVNC_control_bar.noVNC_open { + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5); + left: -30px; +} + +#noVNC_control_bar_handle { + position: absolute; + right: -15px; + top: 10%; + width: 50px; + height: 50px; + z-index: -2; + cursor: pointer; + border-radius: 0 5px 5px 0; + background-color: rgb(83, 99, 122); + background-image: url("../images/handle_bg.svg"); + background-repeat: no-repeat; + background-position: right; + box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.5); +} +#noVNC_control_bar_handle:after { + content: ""; + transition: 0.5s ease-in-out; + background: url("../images/handle.svg"); + position: absolute; + top: 22px; /* (50px-6px)/2 */ + right: 5px; + width: 5px; + height: 6px; +} +#noVNC_control_bar.noVNC_open #noVNC_control_bar_handle:after { + transform: translateX(1px) rotate(180deg); +} +:root:not(.noVNC_connected) #noVNC_control_bar_handle { + display: none; +} #noVNC_control_bar .noVNC_scroll { max-height: 100vh; /* Chrome is buggy with 100% */ diff --git a/app/styles/black.css b/app/styles/black.css index e3ad2493..d982a813 100644 --- a/app/styles/black.css +++ b/app/styles/black.css @@ -12,7 +12,7 @@ color:#fff; } -#noVNC_control_bar { +#noVNC_control_bar, #noVNC_control_bar_handle { background: #4c4c4c; } diff --git a/app/styles/blue.css b/app/styles/blue.css index 3db77270..e36e6d1d 100644 --- a/app/styles/blue.css +++ b/app/styles/blue.css @@ -12,6 +12,6 @@ color:#fff; } -#noVNC_control_bar { +#noVNC_control_bar, #noVNC_control_bar_handle { background: #080F80; } diff --git a/app/ui.js b/app/ui.js index 6e2ff03f..0c7cf1ec 100644 --- a/app/ui.js +++ b/app/ui.js @@ -198,6 +198,9 @@ var UI; document.getElementById("noVNC_control_bar") .addEventListener('keypress', UI.activateControlbar); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('click', UI.toggleControlbar); + document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); document.getElementById("noVNC_send_ctrl_alt_del_button") @@ -474,6 +477,26 @@ var UI; .classList.add("noVNC_idle"); }, + openControlbar: function() { + document.getElementById('noVNC_control_bar') + .classList.add("noVNC_open"); + }, + + closeControlbar: function() { + UI.closeAllPanels(); + document.getElementById('noVNC_control_bar') + .classList.remove("noVNC_open"); + }, + + toggleControlbar: function() { + if (document.getElementById('noVNC_control_bar') + .classList.contains("noVNC_open")) { + UI.closeControlbar(); + } else { + UI.openControlbar(); + } + }, + /* ------^------- * /VISUAL * ============== @@ -612,6 +635,7 @@ var UI; openSettingsPanel: function() { UI.closeAllPanels(); + UI.openControlbar(); UI.updateSetting('encrypt'); UI.updateSetting('true_color'); @@ -664,6 +688,7 @@ var UI; openXvpPanel: function() { UI.closeAllPanels(); + UI.openControlbar(); document.getElementById('noVNC_xvp') .classList.add("noVNC_open"); @@ -708,6 +733,7 @@ var UI; openClipboardPanel: function() { UI.closeAllPanels(); + UI.openControlbar(); document.getElementById('noVNC_clipboard') .classList.add("noVNC_open"); @@ -757,6 +783,7 @@ var UI; openConnectPanel: function() { UI.closeAllPanels(); + UI.openControlbar(); document.getElementById('noVNC_connect_controls') .classList.add("noVNC_open"); @@ -789,6 +816,7 @@ var UI; connect: function() { UI.closeAllPanels(); + UI.closeControlbar(); var host = document.getElementById('noVNC_setting_host').value; var port = document.getElementById('noVNC_setting_port').value; @@ -1250,6 +1278,7 @@ var UI; openExtraKeys: function() { UI.closeAllPanels(); + UI.openControlbar(); document.getElementById('noVNC_modifiers') .classList.add("noVNC_open"); diff --git a/vnc.html b/vnc.html index aeb63f8d..29d82521 100644 --- a/vnc.html +++ b/vnc.html @@ -52,7 +52,11 @@
    -
    +
    +
    + +
    +

    no
    VNC

    @@ -189,7 +193,9 @@
    -
    + +
    +
    From 65e3d7d6a66176c0edb0d298b905f25cf48cd1c2 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 16:12:51 +0200 Subject: [PATCH 231/527] Make control bar handle easier to hit on mobile --- app/styles/base.css | 14 ++++++++++++++ vnc.html | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/styles/base.css b/app/styles/base.css index ec682c46..78f97dff 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -22,6 +22,10 @@ html { height:100%; } +.noVNC_only_touch.noVNC_hidden { + display: none; +} + /* ---------------------------------------- * Input Elements * ---------------------------------------- @@ -202,6 +206,16 @@ input[type=button]:active, select:active { :root:not(.noVNC_connected) #noVNC_control_bar_handle { display: none; } +#noVNC_control_bar_handle div { + position: absolute; + right: -35px; + top: 0; + width: 50px; + height: 50px; +} +:root:not(.noVNC_touch) #noVNC_control_bar_handle div { + display: none; +} #noVNC_control_bar .noVNC_scroll { max-height: 100vh; /* Chrome is buggy with 100% */ diff --git a/vnc.html b/vnc.html index 29d82521..5019bfd9 100644 --- a/vnc.html +++ b/vnc.html @@ -53,7 +53,7 @@
    -
    +
    From 04b399e27d3eaa9ba55d46c05c1d3760dc22d1e6 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Wed, 14 Sep 2016 13:09:12 +0200 Subject: [PATCH 232/527] Allow moving the controlbar handle This also adds emulation of Element.setCapture() as only Firefox and Internet Explorer/Edge currently supports it. --- app/styles/base.css | 3 +- app/ui.js | 113 ++++++++++++++++++++++++++++++++++++++++++-- app/webutil.js | 93 ++++++++++++++++++++++++++++++++++++ core/util.js | 10 ++-- 4 files changed, 212 insertions(+), 7 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 78f97dff..8fbcb282 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -178,7 +178,8 @@ input[type=button]:active, select:active { #noVNC_control_bar_handle { position: absolute; right: -15px; - top: 10%; + top: 0; + transform: translateY(35px); width: 50px; height: 50px; z-index: -2; diff --git a/app/ui.js b/app/ui.js index 0c7cf1ec..cdf20a96 100644 --- a/app/ui.js +++ b/app/ui.js @@ -43,6 +43,10 @@ var UI; hideKeyboardTimeout: null, controlbarTimeout: null, + controlbarGrabbed: false, + controlbarDrag: false, + controlbarMouseDownClientY: 0, + controlbarMouseDownOffsetY: 0, keyboardVisible: false, isTouchDevice: false, @@ -198,13 +202,19 @@ var UI; document.getElementById("noVNC_control_bar") .addEventListener('keypress', UI.activateControlbar); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('click', UI.toggleControlbar); - document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); document.getElementById("noVNC_send_ctrl_alt_del_button") .addEventListener('click', UI.sendCtrlAltDel); + + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousedown', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mouseup', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('mousemove', UI.dragControlbarHandle); + // resize events aren't available for elements + window.addEventListener('resize', UI.updateControlbarHandle); }, addTouchSpecificHandlers: function() { @@ -235,6 +245,13 @@ var UI; document.getElementById("noVNC_control_bar") .addEventListener('input', UI.activateControlbar); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchstart', UI.controlbarHandleMouseDown); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchend', UI.controlbarHandleMouseUp); + document.getElementById("noVNC_control_bar_handle") + .addEventListener('touchmove', UI.dragControlbarHandle); + window.addEventListener('load', UI.keyboardinputReset); }, @@ -497,6 +514,96 @@ var UI; } }, + dragControlbarHandle: function (e) { + if (!UI.controlbarGrabbed) return; + + var ptr = Util.getPointerEvent(e); + + if (!UI.controlbarDrag) { + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + var dragThreshold = 10 * (window.devicePixelRatio || 1); + var dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + + if (dragDistance < dragThreshold) return; + + UI.controlbarDrag = true; + } + + var eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; + + UI.moveControlbarHandle(eventY); + + e.preventDefault(); + e.stopPropagation(); + }, + + // Move the handle but don't allow any position outside the bounds + moveControlbarHandle: function (posY) { + var handle = document.getElementById("noVNC_control_bar_handle"); + var handleHeight = Util.getPosition(handle).height; + var controlbar = document.getElementById("noVNC_control_bar"); + var controlbarBounds = Util.getPosition(controlbar); + var controlbarTop = controlbarBounds.y; + var controlbarBottom = controlbarBounds.y + controlbarBounds.height; + var margin = 10; + + var viewportY = posY; + + // Refuse coordinates outside the control bar + if (viewportY < controlbarTop + margin) { + viewportY = controlbarTop + margin; + } else if (viewportY > controlbarBottom - handleHeight - margin) { + viewportY = controlbarBottom - handleHeight - margin; + } + + // Corner case: control bar too small for stable position + if (controlbarBounds.height < (handleHeight + margin * 2)) { + viewportY = controlbarTop + (controlbarBounds.height - handleHeight) / 2; + } + + var relativeY = viewportY - controlbarTop; + handle.style.transform = "translateY(" + relativeY + "px)"; + }, + + updateControlbarHandle: function () { + var handle = document.getElementById("noVNC_control_bar_handle"); + var pos = Util.getPosition(handle); + UI.moveControlbarHandle(pos.y); + }, + + controlbarHandleMouseUp: function(e) { + if ((e.type == "mouseup") && (e.button != 0)) + return; + + // mouseup and mousedown on the same place toggles the controlbar + if (UI.controlbarGrabbed && !UI.controlbarDrag) { + UI.toggleControlbar(); + e.preventDefault(); + e.stopPropagation(); + } + UI.controlbarGrabbed = false; + }, + + controlbarHandleMouseDown: function(e) { + if ((e.type == "mousedown") && (e.button != 0)) + return; + + var ptr = Util.getPointerEvent(e); + + var handle = document.getElementById("noVNC_control_bar_handle"); + var bounds = handle.getBoundingClientRect(); + + WebUtil.setCapture(handle); + UI.controlbarGrabbed = true; + UI.controlbarDrag = false; + + UI.controlbarMouseDownClientY = ptr.clientY; + UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; + e.preventDefault(); + e.stopPropagation(); + }, + /* ------^------- * /VISUAL * ============== diff --git a/app/webutil.js b/app/webutil.js index 7f234dbf..38bb3967 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -278,6 +278,99 @@ WebUtil.injectParamIfMissing = function (path, param, value) { } }; +// Emulate Element.setCapture() when not supported + +WebUtil._captureRecursion = false; +WebUtil._captureProxy = function (e) { + // Recursion protection as we'll see our own event + if (WebUtil._captureRecursion) return; + + // Clone the event as we cannot dispatch an already dispatched event + var newEv = new e.constructor(e.type, e); + + WebUtil._captureRecursion = true; + WebUtil._captureElem.dispatchEvent(newEv); + WebUtil._captureRecursion = false; + + // Implicitly release the capture on button release + if ((e.type === "mouseup") || (e.type === "touchend")) { + WebUtil.releaseCapture(); + } +}; + +WebUtil.setCapture = function (elem) { + if (elem.setCapture) { + + elem.setCapture(); + + // IE releases capture on 'click' events which might not trigger + elem.addEventListener('mouseup', WebUtil.releaseCapture); + elem.addEventListener('touchend', WebUtil.releaseCapture); + + } else { + // Safari on iOS 9 has a broken constructor for TouchEvent. + // We are fine in this case however, since Safari seems to + // have some sort of implicit setCapture magic anyway. + if (window.TouchEvent !== undefined) { + try { + new TouchEvent("touchstart"); + } catch (TypeError) { + return; + } + } + + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + + if (captureElem === null) { + captureElem = document.createElement("div"); + captureElem.id = "noVNC_mouse_capture_elem"; + captureElem.style.position = "fixed"; + captureElem.style.top = "0px"; + captureElem.style.left = "0px"; + captureElem.style.width = "100%"; + captureElem.style.height = "100%"; + captureElem.style.zIndex = 10000; + captureElem.style.display = "none"; + document.body.appendChild(captureElem); + + captureElem.addEventListener('mousemove', WebUtil._captureProxy); + captureElem.addEventListener('mouseup', WebUtil._captureProxy); + + captureElem.addEventListener('touchmove', WebUtil._captureProxy); + captureElem.addEventListener('touchend', WebUtil._captureProxy); + } + + WebUtil._captureElem = elem; + captureElem.style.display = null; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', WebUtil._captureProxy); + window.addEventListener('mouseup', WebUtil._captureProxy); + + window.addEventListener('touchmove', WebUtil._captureProxy); + window.addEventListener('touchend', WebUtil._captureProxy); + } +}; + +WebUtil.releaseCapture = function () { + if (document.releaseCapture) { + + document.releaseCapture(); + + } else { + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + WebUtil._captureElem = null; + captureElem.style.display = "none"; + + window.removeEventListener('mousemove', WebUtil._captureProxy); + window.removeEventListener('mouseup', WebUtil._captureProxy); + + window.removeEventListener('touchmove', WebUtil._captureProxy); + window.removeEventListener('touchend', WebUtil._captureProxy); + } +}; + // Dynamically load scripts without using document.write() // Reference: http://unixpapa.com/js/dyna.html // diff --git a/core/util.js b/core/util.js index d6a01932..3cc1e876 100644 --- a/core/util.js +++ b/core/util.js @@ -204,14 +204,18 @@ Util.getPosition = function(obj) { 'width': objPosition.width, 'height': objPosition.height}; }; +Util.getPointerEvent = function (e) { + var evt; + evt = (e ? e : window.event); + evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); + return evt; +}; // Get mouse event position in DOM element Util.getEventPosition = function (e, obj, scale) { "use strict"; var evt, docX, docY, pos; - //if (!e) evt = window.event; - evt = (e ? e : window.event); - evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); + evt = Util.getPointerEvent(e); if (evt.pageX || evt.pageY) { docX = evt.pageX; docY = evt.pageY; From 3f93a385a273299ec4662b89c8f99752d5e392f1 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Wed, 31 Aug 2016 11:57:38 +0200 Subject: [PATCH 233/527] Auto-close the toolbar on connect after a delay --- app/ui.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/ui.js b/app/ui.js index cdf20a96..0ccffadc 100644 --- a/app/ui.js +++ b/app/ui.js @@ -41,7 +41,8 @@ var UI; resizeTimeout: null, statusTimeout: null, hideKeyboardTimeout: null, - controlbarTimeout: null, + idleControlbarTimeout: null, + closeControlbarTimeout: null, controlbarGrabbed: false, controlbarDrag: false, @@ -202,6 +203,11 @@ var UI; document.getElementById("noVNC_control_bar") .addEventListener('keypress', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('mousedown', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('keypress', UI.keepControlbar); + document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); document.getElementById("noVNC_send_ctrl_alt_del_button") @@ -245,6 +251,11 @@ var UI; document.getElementById("noVNC_control_bar") .addEventListener('input', UI.activateControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('touchstart', UI.keepControlbar); + document.getElementById("noVNC_control_bar") + .addEventListener('input', UI.keepControlbar); + document.getElementById("noVNC_control_bar_handle") .addEventListener('touchstart', UI.controlbarHandleMouseDown); document.getElementById("noVNC_control_bar_handle") @@ -405,6 +416,9 @@ var UI; document.documentElement.classList.add("noVNC_connected"); UI.updateViewClip(); UI.setMouseButton(1); + + // Hide the controlbar after 2 seconds + UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); } else { document.documentElement.classList.remove("noVNC_connected"); UI.updateXvpButton(0); @@ -480,13 +494,13 @@ var UI; document.getElementById('noVNC_status').classList.remove("noVNC_open"); }, - activateControlbar: function() { - clearTimeout(UI.controlbarTimeout); + activateControlbar: function(event) { + clearTimeout(UI.idleControlbarTimeout); // We manipulate the anchor instead of the actual control // bar in order to avoid creating new a stacking group document.getElementById('noVNC_control_bar_anchor') .classList.remove("noVNC_idle"); - UI.controlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); }, idleControlbar: function() { @@ -494,6 +508,10 @@ var UI; .classList.add("noVNC_idle"); }, + keepControlbar: function() { + clearTimeout(UI.closeControlbarTimeout); + }, + openControlbar: function() { document.getElementById('noVNC_control_bar') .classList.add("noVNC_open"); @@ -923,7 +941,6 @@ var UI; connect: function() { UI.closeAllPanels(); - UI.closeControlbar(); var host = document.getElementById('noVNC_setting_host').value; var port = document.getElementById('noVNC_setting_port').value; From cd5a035d901e536d726400708c7929e5df0fa7b2 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 13:30:28 +0200 Subject: [PATCH 234/527] Make sure all buttons have a tooltip --- vnc.html | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/vnc.html b/vnc.html index 5019bfd9..b7074237 100644 --- a/vnc.html +++ b/vnc.html @@ -67,13 +67,17 @@
    + id="noVNC_mouse_button0" class="noVNC_button" + title="Active Mouse Button"/> + id="noVNC_mouse_button1" class="noVNC_button" + title="Active Mouse Button"/> + id="noVNC_mouse_button2" class="noVNC_button" + title="Active Mouse Button"/> + id="noVNC_mouse_button4" class="noVNC_button" + title="Active Mouse Button"/> @@ -82,17 +86,22 @@
    + id="noVNC_toggle_extra_keys_button" class="noVNC_button" + title="Show Extra Keys"/>
    + id="noVNC_toggle_ctrl_button" class="noVNC_button" + title="Toggle Ctrl"/> + id="noVNC_toggle_alt_button" class="noVNC_button" + title="Toggle Alt"/> + id="noVNC_send_tab_button" class="noVNC_button" + title="Send Tab"/> + id="noVNC_send_esc_button" class="noVNC_button" + title="Send Escape"/>
    From ca25d2ae26ffb22a2b7f9a4c56393165ca70052b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 26 Aug 2016 13:34:01 +0200 Subject: [PATCH 235/527] Move Ctrl+Alt+Del to extra keys menu The extra keys menu is now present for all devices, so avoid clutter by moving the Ctrl+Alt+Del button there as well. --- app/styles/base.css | 5 ----- app/ui.js | 5 +++-- vnc.html | 8 +++----- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 8fbcb282..b87c52e8 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -316,11 +316,6 @@ input[type=button]:active, select:active { display: none; } -/* Send Ctrl+Alt+Delete */ -:root:not(.noVNC_connected) #noVNC_send_ctrl_alt_del_button { - display: none; -} - #noVNC_modifiers { background-color: rgb(92, 92, 92); border: none; diff --git a/app/ui.js b/app/ui.js index 0ccffadc..f78b74c5 100644 --- a/app/ui.js +++ b/app/ui.js @@ -210,8 +210,6 @@ var UI; document.getElementById("noVNC_view_drag_button") .addEventListener('click', UI.toggleViewDrag); - document.getElementById("noVNC_send_ctrl_alt_del_button") - .addEventListener('click', UI.sendCtrlAltDel); document.getElementById("noVNC_control_bar_handle") .addEventListener('mousedown', UI.controlbarHandleMouseDown); @@ -277,6 +275,8 @@ var UI; .addEventListener('click', UI.sendTab); document.getElementById("noVNC_send_esc_button") .addEventListener('click', UI.sendEsc); + document.getElementById("noVNC_send_ctrl_alt_del_button") + .addEventListener('click', UI.sendCtrlAltDel); }, addXvpHandlers: function() { @@ -1462,6 +1462,7 @@ var UI; }, sendCtrlAltDel: function() { + UI.keepKeyboard(); UI.rfb.sendCtrlAltDel(); }, diff --git a/vnc.html b/vnc.html index b7074237..8a68ba6f 100644 --- a/vnc.html +++ b/vnc.html @@ -102,15 +102,13 @@ +
    - - - Date: Tue, 30 Aug 2016 16:38:09 +0200 Subject: [PATCH 236/527] Clean up panel layouts Make sure everything follows a consistent style and is properly labeled. --- app/styles/base.css | 30 ++++++------- vnc.html | 105 +++++++++++++++++++++++++++++++------------- 2 files changed, 89 insertions(+), 46 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index b87c52e8..bc5fc280 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -283,6 +283,15 @@ input[type=button]:active, select:active { transform: translateX(75px); } +.noVNC_panel hr { + border: none; + border-top: 1px solid rgb(192, 192, 192); +} + +.noVNC_panel label { + display: block; +} + .noVNC_submit { float: right; } @@ -332,6 +341,10 @@ input[type=button]:active, select:active { display: none; } +#noVNC_xvp input[type=button] { + width: 100%; +} + /* Clipboard */ :root:not(.noVNC_connected) #noVNC_clipboard_button { display: none; @@ -350,7 +363,7 @@ input[type=button]:active, select:active { } #noVNC_settings ul { list-style: none; - margin: 3px; + margin: 0px; padding: 0px; } #noVNC_setting_path { @@ -369,18 +382,9 @@ input[type=button]:active, select:active { margin: 0px; padding: 0px; } -#noVNC_connect_controls li { - padding-bottom:8px; -} -#noVNC_setting_host { - width:150px; -} #noVNC_setting_port { width: 80px; } -#noVNC_setting_password { - width: 150px; -} /* ---------------------------------------- * Status Dialog @@ -462,12 +466,6 @@ input[type=button]:active, select:active { margin: 0px; padding: 0px; } -#noVNC_password_dlg li { - padding-bottom:8px; -} -#noVNC_password_input { - width: 150px; -} /* ---------------------------------------- * Main Area diff --git a/vnc.html b/vnc.html index 8a68ba6f..0d97734b 100644 --- a/vnc.html +++ b/vnc.html @@ -147,37 +147,62 @@
      -
    • Encrypt
    • -
    • True Color
    • -
    • Local Cursor
    • -
    • Clip to Window
    • -
    • Shared Mode
    • -
    • View Only
    • -
      -
    • Path
    • -
    • +
    • +
    • -
    • Repeater ID
    • -
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +

    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +

    • -
    • + + +
    • -
    • + + + +
    • +

    • +
    • +
    • -
      -
    @@ -192,11 +217,26 @@
      -
    • -
    • -
    • -
    • -
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +

    • +
    • + +
    @@ -213,8 +253,13 @@
      -
    • -
    • +
    • + + +
    • +
    • + +
    From 5454c3451124741a0244eed66c5fc8afb6306a53 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 30 Aug 2016 16:38:45 +0200 Subject: [PATCH 237/527] Add headings to panels --- app/styles/base.css | 15 +++++++++++++++ app/styles/black.css | 2 +- app/styles/blue.css | 2 +- vnc.html | 12 ++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index bc5fc280..89b8eeaf 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -292,6 +292,21 @@ input[type=button]:active, select:active { display: block; } +.noVNC_panel .noVNC_heading { + background-color: rgb(110, 132, 163); + border-radius: 5px; + padding: 5px; + /* Compensate for padding in image */ + padding-right: 8px; + color: white; + font-size: 20px; + margin-bottom: 10px; + white-space: nowrap; +} +.noVNC_panel .noVNC_heading img { + vertical-align: bottom; +} + .noVNC_submit { float: right; } diff --git a/app/styles/black.css b/app/styles/black.css index d982a813..178fa100 100644 --- a/app/styles/black.css +++ b/app/styles/black.css @@ -12,7 +12,7 @@ color:#fff; } -#noVNC_control_bar, #noVNC_control_bar_handle { +#noVNC_control_bar, #noVNC_control_bar_handle, .noVNC_panel .noVNC_heading { background: #4c4c4c; } diff --git a/app/styles/blue.css b/app/styles/blue.css index e36e6d1d..a38ede15 100644 --- a/app/styles/blue.css +++ b/app/styles/blue.css @@ -12,6 +12,6 @@ color:#fff; } -#noVNC_control_bar, #noVNC_control_bar_handle { +#noVNC_control_bar, #noVNC_control_bar_handle, .noVNC_panel .noVNC_heading { background: #080F80; } diff --git a/vnc.html b/vnc.html index 0d97734b..50cd7795 100644 --- a/vnc.html +++ b/vnc.html @@ -115,6 +115,9 @@ title="Shutdown/Reboot..." />
    +
    + Power +
    @@ -127,6 +130,9 @@ title="Clipboard" />
    +
    + Clipboard +

    @@ -147,6 +153,9 @@
    -

  • -
  • - -
  • From b3d91b78e132494fcfa2fdafa1c85caff8788bd0 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 23 Jan 2017 13:25:55 +0100 Subject: [PATCH 378/527] Rename and move init function for fullscreen --- app/ui.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/app/ui.js b/app/ui.js index 5fda992b..8c208eb8 100644 --- a/app/ui.js +++ b/app/ui.js @@ -143,9 +143,10 @@ var UI; UI.toggleControlbarSide(); } - // Setup and initialize event handlers + UI.initFullscreen(); + + // Setup event handlers UI.setupWindowEvents(); - UI.setupFullscreen(); UI.addControlbarHandlers(); UI.addTouchSpecificHandlers(); UI.addExtraKeysHandlers(); @@ -180,6 +181,20 @@ var UI; } }, + initFullscreen: function() { + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!UI.isSafari && + (document.documentElement.requestFullscreen || + document.documentElement.mozRequestFullScreen || + document.documentElement.webkitRequestFullscreen || + document.body.msRequestFullscreen)) { + document.getElementById('noVNC_fullscreen_button') + .classList.remove("noVNC_hidden"); + UI.addFullscreenHandlers(); + } + }, + initSettings: function() { var i; @@ -229,20 +244,6 @@ var UI; .addEventListener('click', UI.hideStatus); }, - setupFullscreen: function() { - // Only show the button if fullscreen is properly supported - // * Safari doesn't support alphanumerical input while in fullscreen - if (!UI.isSafari && - (document.documentElement.requestFullscreen || - document.documentElement.mozRequestFullScreen || - document.documentElement.webkitRequestFullscreen || - document.body.msRequestFullscreen)) { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_hidden"); - UI.addFullscreenHandlers(); - } - }, - addControlbarHandlers: function() { document.getElementById("noVNC_control_bar") .addEventListener('mousemove', UI.activateControlbar); From 3fdc69ceda028d68ce6b828b71f512a8c2d83928 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 23 Jan 2017 14:53:12 +0100 Subject: [PATCH 379/527] Rename function for adding resize handlers Moves the one handler for the status out of the function as well. It didn't fit in with the function's old name either. --- app/ui.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/ui.js b/app/ui.js index 8c208eb8..4b9b27d4 100644 --- a/app/ui.js +++ b/app/ui.js @@ -146,7 +146,7 @@ var UI; UI.initFullscreen(); // Setup event handlers - UI.setupWindowEvents(); + UI.addResizeHandlers(); UI.addControlbarHandlers(); UI.addTouchSpecificHandlers(); UI.addExtraKeysHandlers(); @@ -154,6 +154,8 @@ var UI; UI.addConnectionControlHandlers(); UI.addClipboardHandlers(); UI.addSettingsHandlers(); + document.getElementById("noVNC_status") + .addEventListener('click', UI.hideStatus); UI.openControlbar(); @@ -235,13 +237,10 @@ var UI; UI.initSetting('reconnect_delay', 5000); }, - setupWindowEvents: function() { + addResizeHandlers: function() { window.addEventListener('resize', UI.applyResizeMode); window.addEventListener('resize', UI.updateViewClip); window.addEventListener('resize', UI.updateViewDrag); - - document.getElementById("noVNC_status") - .addEventListener('click', UI.hideStatus); }, addControlbarHandlers: function() { From 59387b34ea68d529f661a490b1d7f42af9772f70 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 23 Jan 2017 14:54:31 +0100 Subject: [PATCH 380/527] Separate init functions from event handling setup Adds a new section for all functions that add event handlers. --- app/ui.js | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/app/ui.js b/app/ui.js index 4b9b27d4..9f0cf169 100644 --- a/app/ui.js +++ b/app/ui.js @@ -237,6 +237,34 @@ var UI; UI.initSetting('reconnect_delay', 5000); }, + initRFB: function() { + try { + UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), + 'onNotification': UI.notification, + 'onUpdateState': UI.updateState, + 'onDisconnected': UI.disconnectFinished, + 'onPasswordRequired': UI.passwordRequired, + 'onXvpInit': UI.updateXvpButton, + 'onClipboard': UI.clipboardReceive, + 'onBell': UI.bell, + 'onFBUComplete': UI.initialResize, + 'onFBResize': UI.updateSessionSize, + 'onDesktopName': UI.updateDesktopName}); + return true; + } catch (exc) { + var msg = "Unable to create RFB client -- " + exc; + Util.Error(msg); + UI.showStatus(msg, 'error'); + return false; + } + }, + +/* ------^------- + * /INIT + * ============== + * EVENT HANDLERS + * ------v------*/ + addResizeHandlers: function() { window.addEventListener('resize', UI.applyResizeMode); window.addEventListener('resize', UI.updateViewClip); @@ -416,30 +444,8 @@ var UI; window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); }, - initRFB: function() { - try { - UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), - 'onNotification': UI.notification, - 'onUpdateState': UI.updateState, - 'onDisconnected': UI.disconnectFinished, - 'onPasswordRequired': UI.passwordRequired, - 'onXvpInit': UI.updateXvpButton, - 'onClipboard': UI.clipboardReceive, - 'onBell': UI.bell, - 'onFBUComplete': UI.initialResize, - 'onFBResize': UI.updateSessionSize, - 'onDesktopName': UI.updateDesktopName}); - return true; - } catch (exc) { - var msg = "Unable to create RFB client -- " + exc; - Util.Error(msg); - UI.showStatus(msg, 'error'); - return false; - } - }, - /* ------^------- - * /INIT + * /EVENT HANDLERS * ============== * VISUAL * ------v------*/ From 623b1b7d96f20e5021e433f635a7cfa4c2c56f9b Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Mon, 23 Jan 2017 15:37:04 +0100 Subject: [PATCH 381/527] Allow resize setting to be changed while connected There is no reason to why this shouldn't be possible. --- app/ui.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index 9f0cf169..d51d9195 100644 --- a/app/ui.js +++ b/app/ui.js @@ -421,6 +421,8 @@ var UI; UI.addSettingChangeHandler('true_color'); UI.addSettingChangeHandler('cursor'); UI.addSettingChangeHandler('resize'); + UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); + UI.addSettingChangeHandler('resize', UI.applyResizeMode); UI.addSettingChangeHandler('clip'); UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('view_only'); @@ -505,7 +507,6 @@ var UI; } UI.enableDisableViewClip(); - document.getElementById('noVNC_setting_resize').disabled = UI.connected; document.getElementById('noVNC_setting_shared').disabled = UI.connected; document.getElementById('noVNC_setting_view_only').disabled = UI.connected; document.getElementById('noVNC_setting_host').disabled = UI.connected; @@ -1224,6 +1225,8 @@ var UI; var display = UI.rfb.get_display(); var resizeMode = UI.getSetting('resize'); + display.set_scale(1); + UI.rfb.get_mouse().set_scale(1); if (resizeMode === 'remote') { From 9865432a027e0ee5461ef1c5d93dd11a40a804e0 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 11:43:17 +0100 Subject: [PATCH 382/527] Center canvas (again) The previous attempt could leave parts of the canvas outside the document, making it impossible to reach. Use a safer method as recommended by Mozilla. --- app/styles/base.css | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index b8b6be06..88472d97 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -526,7 +526,7 @@ input[type=button]:active, select:active { background-color: #fff; color: #fff; border: 0; - position: relative; + position: absolute; left: -40px; z-index: -1; ime-mode: disabled; @@ -786,29 +786,19 @@ input[type=button]:active, select:active { /* Main container */ #noVNC_container { - display: table; width: 100%; height: 100%; background-color: #313131; border-bottom-right-radius: 800px 600px; /*border-top-left-radius: 800px 600px;*/ } -:root.noVNC_connected #noVNC_container { - background-color: rgb(40, 40, 40); - border-radius: 0; -} /* HTML5 Canvas */ #noVNC_screen { - position: absolute; - margin: 0px; - padding: 0px; - bottom: 0px; - top: 0px; - left: 0px; - right: 0px; - width: auto; - height: auto; + display: flex; + width: 100%; + height: 100%; + background-color: rgb(40, 40, 40); } :root:not(.noVNC_connected) #noVNC_screen { display: none; @@ -818,11 +808,6 @@ input[type=button]:active, select:active { * scaling will occur. Canvas size depends on remote VNC * settings and noVNC settings. */ #noVNC_canvas { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; margin: auto; } From f6299e0aed5d46aa7de9e99e1ab102270f5d32d3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 11:44:24 +0100 Subject: [PATCH 383/527] Move keyboard style in CSS Put it so it matches where it is in the HTML. --- app/styles/base.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 88472d97..323b6d35 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -520,18 +520,6 @@ input[type=button]:active, select:active { display: none; } -#noVNC_keyboardinput { - width: 1px; - height: 1px; - background-color: #fff; - color: #fff; - border: 0; - position: absolute; - left: -40px; - z-index: -1; - ime-mode: disabled; -} - /* Extra manual keys */ :root:not(.noVNC_connected) #noVNC_extra_keys { display: none; @@ -793,6 +781,18 @@ input[type=button]:active, select:active { /*border-top-left-radius: 800px 600px;*/ } +#noVNC_keyboardinput { + width: 1px; + height: 1px; + background-color: #fff; + color: #fff; + border: 0; + position: absolute; + left: -40px; + z-index: -1; + ime-mode: disabled; +} + /* HTML5 Canvas */ #noVNC_screen { display: flex; From a6e52f9a73a6c4f7e46e1ba463b532f4b459fad9 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 11:56:02 +0100 Subject: [PATCH 384/527] Merge display scale changes --- core/display.js | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/core/display.js b/core/display.js index ac2e1e5f..5eab7969 100644 --- a/core/display.js +++ b/core/display.js @@ -573,37 +573,22 @@ }, autoscale: function (containerWidth, containerHeight, downscaleOnly) { + var vp = this._viewportLoc; var targetAspectRatio = containerWidth / containerHeight; - var fbAspectRatio = this._fb_width / this._fb_height; + var fbAspectRatio = vp.w / vp.h; var scaleRatio; if (fbAspectRatio >= targetAspectRatio) { - scaleRatio = containerWidth / this._fb_width; + scaleRatio = containerWidth / vp.w; } else { - scaleRatio = containerHeight / this._fb_height; + scaleRatio = containerHeight / vp.h; } - var targetW, targetH; if (scaleRatio > 1.0 && downscaleOnly) { - targetW = this._fb_width; - targetH = this._fb_height; scaleRatio = 1.0; - } else if (fbAspectRatio >= targetAspectRatio) { - targetW = containerWidth; - targetH = Math.round(containerWidth / fbAspectRatio); - } else { - targetW = Math.round(containerHeight * fbAspectRatio); - targetH = containerHeight; } - // NB(directxman12): If you set the width directly, or set the - // style width to a number, the canvas is cleared. - // However, if you set the style width to a string - // ('NNNpx'), the canvas is scaled without clearing. - this._target.style.width = targetW + 'px'; - this._target.style.height = targetH + 'px'; - - this._scale = scaleRatio; + this._rescale(scaleRatio); return scaleRatio; // so that the mouse, etc scale can be set }, @@ -612,6 +597,11 @@ _rescale: function (factor) { this._scale = factor; var vp = this._viewportLoc; + + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. this._target.style.width = Math.round(factor * vp.w) + 'px'; this._target.style.height = Math.round(factor * vp.h) + 'px'; }, From 2e6a58fb1571dce23063a01f18da1aa3682a7e1f Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 12:00:13 +0100 Subject: [PATCH 385/527] Avoid changing scale unless necessary It causes the browser to redo the layout needlessly otherwise, having annoying effects like moving the scroll position. --- core/display.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/display.js b/core/display.js index 5eab7969..8da80317 100644 --- a/core/display.js +++ b/core/display.js @@ -602,8 +602,14 @@ // style width to a number, the canvas is cleared. // However, if you set the style width to a string // ('NNNpx'), the canvas is scaled without clearing. - this._target.style.width = Math.round(factor * vp.w) + 'px'; - this._target.style.height = Math.round(factor * vp.h) + 'px'; + var width = Math.round(factor * vp.w) + 'px'; + var height = Math.round(factor * vp.h) + 'px'; + + if ((this._target.style.width !== width) || + (this._target.style.height !== height)) { + this._target.style.width = width; + this._target.style.height = height; + } }, _setFillColor: function (color) { From 648c83984c31c5e479efbddbc38f4b1d384bbf1d Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 13:17:43 +0100 Subject: [PATCH 386/527] Scroll correct element The element we want scrolling around is noVNC_screen, not the entire window. This also allows us to compute the screen size without fiddling the scrollbars on and off. --- app/styles/base.css | 1 + app/ui.js | 16 +--------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 323b6d35..0518acc4 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -798,6 +798,7 @@ input[type=button]:active, select:active { display: flex; width: 100%; height: 100%; + overflow: auto; background-color: rgb(40, 40, 40); } :root:not(.noVNC_connected) #noVNC_screen { diff --git a/app/ui.js b/app/ui.js index d51d9195..47263555 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1259,21 +1259,7 @@ var UI; // Gets the the size of the available viewport in the browser window screenSize: function() { var screen = document.getElementById('noVNC_screen'); - - // Hide the scrollbars until the size is calculated - screen.style.overflow = "hidden"; - - var pos = Util.getPosition(screen); - var w = pos.width; - var h = pos.height; - - screen.style.overflow = "visible"; - - if (isNaN(w) || isNaN(h)) { - return false; - } else { - return {w: w, h: h}; - } + return {w: screen.offsetWidth, h: screen.offsetHeight}; }, // Normally we only apply the current resize mode after a window resize From 84b05d24b0b7cfeb9c19684909b25dbd35c41a90 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 13:19:15 +0100 Subject: [PATCH 387/527] Allow submitting the password from the input field --- app/styles/base.css | 16 +++++++++++----- app/ui.js | 5 +++-- vnc.html | 6 +++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 0518acc4..c8b63b3b 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -91,7 +91,7 @@ input[type=input], input[type=password], input:not([type]), textarea { background: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240)); } -input[type=button], select { +input[type=button], input[type=submit], select { /* Disable default rendering */ -webkit-appearance: none; -moz-appearance: none; @@ -109,7 +109,7 @@ input[type=button], select { vertical-align: middle; } -input[type=button] { +input[type=button], input[type=submit] { padding-left: 20px; padding-right: 20px; } @@ -121,29 +121,35 @@ option { input[type=input]:focus, input[type=password]:focus, input:not([type]):focus, input[type=button]:focus, +input[type=submit]:focus, textarea:focus, select:focus { box-shadow: 0px 0px 3px rgba(74, 144, 217, 0.5); border-color: rgb(74, 144, 217); outline: none; } -input[type=button]::-moz-focus-inner { +input[type=button]::-moz-focus-inner, +input[type=submit]::-moz-focus-inner { border: none; } input[type=input]:disabled, input[type=password]:disabled, input:not([type]):disabled, input[type=button]:disabled, +input[type=submit]:disabled, textarea:disabled, select:disabled { color: rgb(128, 128, 128); background: rgb(240, 240, 240); } -input[type=button]:active, select:active { +input[type=button]:active, input[type=submit]:active, +select:active { border-bottom-width: 1px; margin-top: 3px; } -:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled), :root:not(.noVNC_touch) select:hover:not(:disabled) { +:root:not(.noVNC_touch) input[type=button]:hover:not(:disabled), +:root:not(.noVNC_touch) input[type=submit]:hover:not(:disabled), +:root:not(.noVNC_touch) select:hover:not(:disabled) { background: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250)); } diff --git a/app/ui.js b/app/ui.js index 47263555..83233a7c 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1152,13 +1152,14 @@ var UI; UI.showStatus(msg, "warning"); }, - setPassword: function() { + setPassword: function(e) { var password = document.getElementById('noVNC_password_input').value; UI.rfb.sendPassword(password); UI.reconnect_password = password; document.getElementById('noVNC_password_dlg') .classList.remove('noVNC_open'); - return false; + // Prevent actually submitting the form + e.preventDefault(); }, /* ------^------- diff --git a/vnc.html b/vnc.html index 0048046d..c6f0d25b 100644 --- a/vnc.html +++ b/vnc.html @@ -277,17 +277,17 @@
    -
    +
    • - +
    -
    +
    From d2467189f5a757ca9354d25217f1248234814e44 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 17:11:39 +0100 Subject: [PATCH 388/527] Don't delay WebSocket flushing The native WebSocket is in a much better position to do queue management than us. We also failed to check the return value and set up a timer, causing stalls. This gets us in sync with websockify as of 40238b00. --- core/websock.js | 88 +++++-------------------------------------------- 1 file changed, 9 insertions(+), 79 deletions(-) diff --git a/core/websock.js b/core/websock.js index 1271d2f5..51d9b625 100644 --- a/core/websock.js +++ b/core/websock.js @@ -3,10 +3,8 @@ * Copyright (C) 2012 Joel Martin * Licensed under MPL 2.0 (see LICENSE.txt) * - * Websock is similar to the standard WebSocket object but Websock - * enables communication with raw TCP sockets (i.e. the binary stream) - * via websockify. This is accomplished by base64 encoding the data - * stream between Websock and websockify. + * Websock is similar to the standard WebSocket object but with extra + * buffer handling. * * Websock has built-in receive queue buffering; the message event * does not contain actual data but is simply a notification that @@ -16,7 +14,6 @@ /* [module] * import Util from "./util"; - * import Base64 from "./base64"; */ /*jslint browser: true, bitwise: true */ @@ -39,9 +36,6 @@ this._sQlen = 0; this._sQ = null; // Send queue - this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' - this.maxBufferedAmount = 200; - this._eventHandlers = { 'message': function () {}, 'open': function () {}, @@ -182,24 +176,16 @@ Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); } - if (this._websocket.bufferedAmount < this.maxBufferedAmount) { - if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { - this._websocket.send(this._encode_message()); - this._sQlen = 0; - } - - return true; - } else { - Util.Info("Delaying send, bufferedAmount: " + - this._websocket.bufferedAmount); - return false; + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; } }, send: function (arr) { this._sQ.set(arr, this._sQlen); this._sQlen += arr.length; - return this.flush(); + this.flush(); }, send_string: function (str) { @@ -222,80 +208,24 @@ this._sQ = new Uint8Array(this._sQbufferSize); }, - init: function (protocols, ws_schema) { + init: function () { this._allocate_buffers(); this._rQi = 0; this._websocket = null; - - // Check for full typed array support - var bt = false; - if (('Uint8Array' in window) && - ('set' in Uint8Array.prototype)) { - bt = true; - } - - // Check for full binary type support in WebSockets - // Inspired by: - // https://github.com/Modernizr/Modernizr/issues/370 - // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js - var wsbt = false; - try { - if (bt && ('binaryType' in WebSocket.prototype || - !!(new WebSocket(ws_schema + '://.').binaryType))) { - Util.Info("Detected binaryType support in WebSockets"); - wsbt = true; - } - } catch (exc) { - // Just ignore failed test localhost connection - } - - // Default protocols if not specified - if (typeof(protocols) === "undefined") { - protocols = 'binary'; - } - - if (Array.isArray(protocols) && protocols.indexOf('binary') > -1) { - protocols = 'binary'; - } - - if (!wsbt) { - throw new Error("noVNC no longer supports base64 WebSockets. " + - "Please use a browser which supports binary WebSockets."); - } - - if (protocols != 'binary') { - throw new Error("noVNC no longer supports base64 WebSockets. Please " + - "use the binary subprotocol instead."); - } - - return protocols; }, open: function (uri, protocols) { var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; - protocols = this.init(protocols, ws_schema); + this.init(); this._websocket = new WebSocket(uri, protocols); - - if (protocols.indexOf('binary') >= 0) { - this._websocket.binaryType = 'arraybuffer'; - } + this._websocket.binaryType = 'arraybuffer'; this._websocket.onmessage = this._recv_message.bind(this); this._websocket.onopen = (function () { Util.Debug('>> WebSock.onopen'); if (this._websocket.protocol) { - this._mode = this._websocket.protocol; Util.Info("Server choose sub-protocol: " + this._websocket.protocol); - } else { - this._mode = 'binary'; - Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); - } - - if (this._mode != 'binary') { - throw new Error("noVNC no longer supports base64 WebSockets. Please " + - "use the binary subprotocol instead."); - } this._eventHandlers.open(); From c4482d2de5623a0541ef78cfe5fcc5e7781e34af Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 17:15:27 +0100 Subject: [PATCH 389/527] Remove buffer limit tests We no longer limit writes to the socket. --- tests/test.websock.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/test.websock.js b/tests/test.websock.js index f708e04b..fb2f266d 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -170,8 +170,7 @@ describe('Websock', function() { }; }); - it('should actually send on the websocket if the websocket does not have too much buffered', function () { - sock.maxBufferedAmount = 10; + it('should actually send on the websocket', function () { sock._websocket.bufferedAmount = 8; sock._websocket.readyState = WebSocket.OPEN sock._sQ = new Uint8Array([1, 2, 3]); @@ -183,30 +182,14 @@ describe('Websock', function() { expect(sock._websocket.send).to.have.been.calledWith(encoded); }); - it('should return true if the websocket did not have too much buffered', function () { - sock.maxBufferedAmount = 10; - sock._websocket.bufferedAmount = 8; - - expect(sock.flush()).to.be.true; - }); - it('should not call send if we do not have anything queued up', function () { sock._sQlen = 0; - sock.maxBufferedAmount = 10; sock._websocket.bufferedAmount = 8; sock.flush(); expect(sock._websocket.send).not.to.have.been.called; }); - - it('should not send and return false if the websocket has too much buffered', function () { - sock.maxBufferedAmount = 10; - sock._websocket.bufferedAmount = 12; - - expect(sock.flush()).to.be.false; - expect(sock._websocket.send).to.not.have.been.called; - }); }); describe('send', function () { From ec7ba3eeae31468fab63b89664fde5b09b669272 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 3 Feb 2017 17:16:02 +0100 Subject: [PATCH 390/527] Remove binary protocol tests We require standard adherence now, so remove tests that assume the old system. --- tests/test.websock.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/test.websock.js b/tests/test.websock.js index fb2f266d..023c7726 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -249,10 +249,6 @@ describe('Websock', function() { expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary'); }); - it('should fail if we specify a protocol besides binary', function () { - expect(function () { sock.open('ws:///', 'base64'); }).to.throw(Error); - }); - // it('should initialize the event handlers')? }); @@ -312,17 +308,6 @@ describe('Websock', function() { expect(sock._recv_message).to.have.been.calledOnce; }); - it('should fail if a protocol besides binary is requested', function () { - sock._websocket.protocol = 'base64'; - expect(sock._websocket.onopen).to.throw(Error); - }); - - it('should assume binary if no protocol was available on opening', function () { - sock._websocket.protocol = null; - sock._websocket.onopen(); - expect(sock._mode).to.equal('binary'); - }); - it('should call the open event handler on opening', function () { sock._websocket.onopen(); expect(sock._eventHandlers.open).to.have.been.calledOnce; From 3f48c7017d5828733d6dbda83c7c22d09f98616e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 7 Feb 2017 16:09:10 +0100 Subject: [PATCH 391/527] Tweak spinner appearence Change the look of the "busy" spinner a bit. It's mostly used for connection stuff, so give it a more data flow feel. Also bling it up a bit with some fading. Perty sells. :) --- app/styles/base.css | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index c8b63b3b..d166bb34 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -52,24 +52,27 @@ html { .noVNC_spinner, .noVNC_spinner::before, .noVNC_spinner::after { width: 10px; height: 10px; - border-radius: 50%; - animation: noVNC_spinner 1.0s ease-in-out alternate infinite; + border-radius: 2px; + animation: noVNC_spinner 1.0s linear infinite; } .noVNC_spinner::before { content: ""; position: absolute; - left: -20px; - animation-delay: -0.2s; + left: 0px; + top: 0px; + animation-delay: -0.1s; } .noVNC_spinner::after { content: ""; position: absolute; - left: 20px; - animation-delay: 0.2s; + top: 0px; + left: 0px; + animation-delay: 0.1s; } @keyframes noVNC_spinner { - 0% { box-shadow: 0 10px 0 white; } - 100% { box-shadow: 0 30px 0 white; } + 0% { box-shadow: -60px 10px 0 rgba(255, 255, 255, 0); width: 20px; } + 25% { box-shadow: 20px 10px 0 rgba(255, 255, 255, 1); width: 10px; } + 50% { box-shadow: 60px 10px 0 rgba(255, 255, 255, 0); width: 10px; } } /* ---------------------------------------- From 0ae5b50a0e0d9393059c3bdc31365472fdd0b9e9 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Tue, 7 Feb 2017 16:44:16 +0100 Subject: [PATCH 392/527] Expect console.debug(), not console.log(), in test We use the more specific function now, even though it is usually an alias. --- tests/test.util.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test.util.js b/tests/test.util.js index 60cdb582..e6a79bb6 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -10,6 +10,7 @@ describe('Utils', function() { describe('logging functions', function () { beforeEach(function () { sinon.spy(console, 'log'); + sinon.spy(console, 'debug'); sinon.spy(console, 'warn'); sinon.spy(console, 'error'); sinon.spy(console, 'info'); @@ -17,6 +18,7 @@ describe('Utils', function() { afterEach(function () { console.log.restore(); + console.debug.restore(); console.warn.restore(); console.error.restore(); console.info.restore(); @@ -29,10 +31,10 @@ describe('Utils', function() { expect(console.log).to.not.have.been.called; }); - it('should use console.log for Debug', function () { + it('should use console.debug for Debug', function () { Util.init_logging('debug'); Util.Debug('dbg'); - expect(console.log).to.have.been.calledWith('dbg'); + expect(console.debug).to.have.been.calledWith('dbg'); }); it('should use console.info for Info', function () { From b345859ed63d0ead7c2e56727eaaccfbe4011a68 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 9 Feb 2017 14:20:37 +0100 Subject: [PATCH 393/527] More aggressive retaining of focus Try to keep the virtual keyboard up even more. Only release focus when it is absolutely necessary. --- app/ui.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/ui.js b/app/ui.js index 83233a7c..6c706fa8 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1479,15 +1479,21 @@ var UI; return; } - // Allow clicking on links - if (event.target.tagName === "A") { - return; - } - - // And form elements, except standard noVNC buttons - if ((event.target.form !== undefined) && - !event.target.classList.contains("noVNC_button")) { - return; + // Only allow focus to move to other elements that need + // focus to function properly + if (event.target.form !== undefined) { + switch (event.target.type) { + case 'text': + case 'email': + case 'search': + case 'password': + case 'tel': + case 'url': + case 'textarea': + case 'select-one': + case 'select-multiple': + return; + } } event.preventDefault(); From 86d15a492940013d766bc95dde43e23953395eaa Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 9 Feb 2017 15:51:22 +0100 Subject: [PATCH 394/527] Use setCapture() polyfill everywhere This makes sure we get consistent behaviour across all browsers. --- app/ui.js | 2 +- app/webutil.js | 94 ------------------------------------------- core/input/devices.js | 8 +--- core/util.js | 93 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 101 deletions(-) diff --git a/app/ui.js b/app/ui.js index 6c706fa8..29bfbd6e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -764,7 +764,7 @@ var UI; var handle = document.getElementById("noVNC_control_bar_handle"); var bounds = handle.getBoundingClientRect(); - WebUtil.setCapture(handle); + Util.setCapture(handle); UI.controlbarGrabbed = true; UI.controlbarDrag = false; diff --git a/app/webutil.js b/app/webutil.js index 6cacb93b..e6e6afb7 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -239,100 +239,6 @@ WebUtil.injectParamIfMissing = function (path, param, value) { } }; -// Emulate Element.setCapture() when not supported - -WebUtil._captureRecursion = false; -WebUtil._captureProxy = function (e) { - // Recursion protection as we'll see our own event - if (WebUtil._captureRecursion) return; - - // Clone the event as we cannot dispatch an already dispatched event - var newEv = new e.constructor(e.type, e); - - WebUtil._captureRecursion = true; - WebUtil._captureElem.dispatchEvent(newEv); - WebUtil._captureRecursion = false; - - // Implicitly release the capture on button release - if ((e.type === "mouseup") || (e.type === "touchend")) { - WebUtil.releaseCapture(); - } -}; - -WebUtil.setCapture = function (elem) { - if (elem.setCapture) { - - elem.setCapture(); - - // IE releases capture on 'click' events which might not trigger - elem.addEventListener('mouseup', WebUtil.releaseCapture); - elem.addEventListener('touchend', WebUtil.releaseCapture); - - } else { - // Safari on iOS 9 has a broken constructor for TouchEvent. - // We are fine in this case however, since Safari seems to - // have some sort of implicit setCapture magic anyway. - if (window.TouchEvent !== undefined) { - try { - new TouchEvent("touchstart"); - } catch (TypeError) { - return; - } - } - - var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - - if (captureElem === null) { - captureElem = document.createElement("div"); - captureElem.id = "noVNC_mouse_capture_elem"; - captureElem.style.position = "fixed"; - captureElem.style.top = "0px"; - captureElem.style.left = "0px"; - captureElem.style.width = "100%"; - captureElem.style.height = "100%"; - captureElem.style.zIndex = 10000; - captureElem.style.display = "none"; - document.body.appendChild(captureElem); - - captureElem.addEventListener('mousemove', WebUtil._captureProxy); - captureElem.addEventListener('mouseup', WebUtil._captureProxy); - - captureElem.addEventListener('touchmove', WebUtil._captureProxy); - captureElem.addEventListener('touchend', WebUtil._captureProxy); - } - - WebUtil._captureElem = elem; - captureElem.style.display = null; - - // We listen to events on window in order to keep tracking if it - // happens to leave the viewport - window.addEventListener('mousemove', WebUtil._captureProxy); - window.addEventListener('mouseup', WebUtil._captureProxy); - - window.addEventListener('touchmove', WebUtil._captureProxy); - window.addEventListener('touchend', WebUtil._captureProxy); - } -}; - -WebUtil.releaseCapture = function () { - if (document.releaseCapture) { - - document.releaseCapture(); - - } else { - var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - WebUtil._captureElem = null; - captureElem.style.display = "none"; - - window.removeEventListener('mousemove', WebUtil._captureProxy); - window.removeEventListener('mouseup', WebUtil._captureProxy); - - window.removeEventListener('touchmove', WebUtil._captureProxy); - window.removeEventListener('touchend', WebUtil._captureProxy); - } -}; - - // Dynamically load scripts without using document.write() // Reference: http://unixpapa.com/js/dyna.html // diff --git a/core/input/devices.js b/core/input/devices.js index cdb21ad2..c2cdb2c2 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -190,9 +190,7 @@ // private methods _captureMouse: function () { // capturing the mouse ensures we get the mouseup event - if (this._target.setCapture) { - this._target.setCapture(); - } + Util.setCapture(this._target); // some browsers give us mouseup events regardless, // so if we never captured the mouse, we can disregard the event @@ -200,9 +198,7 @@ }, _releaseMouse: function () { - if (this._target.releaseCapture) { - this._target.releaseCapture(); - } + Util.releaseCapture(this._target); this._mouseCaptured = false; }, diff --git a/core/util.js b/core/util.js index fa25a59e..e73eb9d0 100644 --- a/core/util.js +++ b/core/util.js @@ -517,4 +517,97 @@ Util.Localisation = { }, }; +// Emulate Element.setCapture() when not supported + +Util._captureRecursion = false; +Util._captureProxy = function (e) { + // Recursion protection as we'll see our own event + if (Util._captureRecursion) return; + + // Clone the event as we cannot dispatch an already dispatched event + var newEv = new e.constructor(e.type, e); + + Util._captureRecursion = true; + Util._captureElem.dispatchEvent(newEv); + Util._captureRecursion = false; + + // Implicitly release the capture on button release + if ((e.type === "mouseup") || (e.type === "touchend")) { + Util.releaseCapture(); + } +}; + +Util.setCapture = function (elem) { + if (elem.setCapture) { + + elem.setCapture(); + + // IE releases capture on 'click' events which might not trigger + elem.addEventListener('mouseup', Util.releaseCapture); + elem.addEventListener('touchend', Util.releaseCapture); + + } else { + // Safari on iOS 9 has a broken constructor for TouchEvent. + // We are fine in this case however, since Safari seems to + // have some sort of implicit setCapture magic anyway. + if (window.TouchEvent !== undefined) { + try { + new TouchEvent("touchstart"); + } catch (TypeError) { + return; + } + } + + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + + if (captureElem === null) { + captureElem = document.createElement("div"); + captureElem.id = "noVNC_mouse_capture_elem"; + captureElem.style.position = "fixed"; + captureElem.style.top = "0px"; + captureElem.style.left = "0px"; + captureElem.style.width = "100%"; + captureElem.style.height = "100%"; + captureElem.style.zIndex = 10000; + captureElem.style.display = "none"; + document.body.appendChild(captureElem); + + captureElem.addEventListener('mousemove', Util._captureProxy); + captureElem.addEventListener('mouseup', Util._captureProxy); + + captureElem.addEventListener('touchmove', Util._captureProxy); + captureElem.addEventListener('touchend', Util._captureProxy); + } + + Util._captureElem = elem; + captureElem.style.display = null; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', Util._captureProxy); + window.addEventListener('mouseup', Util._captureProxy); + + window.addEventListener('touchmove', Util._captureProxy); + window.addEventListener('touchend', Util._captureProxy); + } +}; + +Util.releaseCapture = function () { + if (document.releaseCapture) { + + document.releaseCapture(); + + } else { + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + Util._captureElem = null; + captureElem.style.display = "none"; + + window.removeEventListener('mousemove', Util._captureProxy); + window.removeEventListener('mouseup', Util._captureProxy); + + window.removeEventListener('touchmove', Util._captureProxy); + window.removeEventListener('touchend', Util._captureProxy); + } +}; + /* [module] export default Util; */ From 6ffdfd3aa74ad0338df5a05be9383a22dde1198b Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 9 Feb 2017 16:02:58 +0100 Subject: [PATCH 395/527] Improve check for when to ignore mouse events The browser tells us which element was under the cursor, so we don't have to calculate it ourselves. --- core/input/devices.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/input/devices.js b/core/input/devices.js index c2cdb2c2..e38f9c7e 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -330,12 +330,8 @@ if (!this._focused) { return true; } var evt = (e ? e : window.event); - var pos = Util.getEventPosition(e, this._target, this._scale); - /* Stop propagation if inside canvas area */ - if ((pos.realx >= 0) && (pos.realy >= 0) && - (pos.realx < this._target.offsetWidth) && - (pos.realy < this._target.offsetHeight)) { + if (evt.target == this._target) { //Util.Debug("mouse event disabled"); Util.stopEvent(e); return false; From bd7d89e7cc1d3a39ffbec7c9c678b86752f1bc82 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 10 Feb 2017 16:39:18 +0100 Subject: [PATCH 396/527] Fix resize on Chrome Chrome sucks at calculating the size and gets confused due to the scrollbars. Hide the scroll bars while resizing. --- app/ui.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index 29bfbd6e..98bb5f63 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1260,7 +1260,16 @@ var UI; // Gets the the size of the available viewport in the browser window screenSize: function() { var screen = document.getElementById('noVNC_screen'); - return {w: screen.offsetWidth, h: screen.offsetHeight}; + var width, height; + + screen.style.overflow = "hidden"; + + width = screen.offsetWidth; + height = screen.offsetHeight; + + screen.style.overflow = "auto"; + + return {w: width, h: height}; }, // Normally we only apply the current resize mode after a window resize From 95533c579ef84719a215b74798fe10412b786a3e Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 13 Feb 2017 13:58:10 +0100 Subject: [PATCH 397/527] Better selection of auth type If no authentication is required then we should pick the None option to avoid bothering the user. --- core/rfb.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index c6e19731..b8ff7820 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -792,25 +792,20 @@ "Security failure: " + reason); } - this._rfb_auth_scheme = 0; var types = this._sock.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); - for (var i = 0; i < types.length; i++) { - switch (types[i]) { - case 1: // None - case 2: // VNC Authentication - case 16: // Tight - case 22: // XVP - if (types[i] > this._rfb_auth_scheme) { - this._rfb_auth_scheme = types[i]; - } - break; - default: - break; - } - } - if (this._rfb_auth_scheme === 0) { + // Look for each auth in preferred order + this._rfb_auth_scheme = 0; + if (types.indexOf(1) !== -1) { + this._rfb_auth_scheme = 1; // None + } else if (types.indexOf(22) !== -1) { + this._rfb_auth_scheme = 22; // XVP + } else if (types.indexOf(16) !== -1) { + this._rfb_auth_scheme = 16; // Tight + } else if (types.indexOf(2) !== -1) { + this._rfb_auth_scheme = 2; // VNC Auth + } else { return this._fail("Unsupported server", "Unsupported security types: " + types); } From 0ee5ca6ebe3ff8fbb8112cd7e235fdbc98ff32bb Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 13 Feb 2017 15:00:32 +0100 Subject: [PATCH 398/527] Fix tests after changing auth negotiation PhantomJS has a very basic implementation of Uint8Array, so we need to help it out a bit. --- core/rfb.js | 19 +++++++++++++++---- tests/test.rfb.js | 14 +++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/core/rfb.js b/core/rfb.js index b8ff7820..e26e7b12 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -795,15 +795,26 @@ var types = this._sock.rQshiftBytes(num_types); Util.Debug("Server security types: " + types); + // Polyfill since IE and PhantomJS doesn't have + // TypedArray.includes() + function includes(item, array) { + for (var i = 0; i < array.length; i++) { + if (array[i] === item) { + return true; + } + } + return false; + } + // Look for each auth in preferred order this._rfb_auth_scheme = 0; - if (types.indexOf(1) !== -1) { + if (includes(1, types)) { this._rfb_auth_scheme = 1; // None - } else if (types.indexOf(22) !== -1) { + } else if (includes(22, types)) { this._rfb_auth_scheme = 22; // XVP - } else if (types.indexOf(16) !== -1) { + } else if (includes(16, types)) { this._rfb_auth_scheme = 16; // Tight - } else if (types.indexOf(2) !== -1) { + } else if (includes(2, types)) { this._rfb_auth_scheme = 2; // VNC Auth } else { return this._fail("Unsupported server", diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 229cfe58..f903d052 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -712,12 +712,20 @@ describe('Remote Frame Buffer Protocol Client', function() { expect(client._rfb_auth_scheme).to.equal(auth_scheme); }); + it('should prefer no authentication is possible', function () { + client._rfb_version = 3.7; + var auth_schemes = [2, 1, 3]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_auth_scheme).to.equal(1); + expect(client._sock).to.have.sent(new Uint8Array([1, 1])); + }); + it('should choose for the most prefered scheme possible for versions >= 3.7', function () { client._rfb_version = 3.7; - var auth_schemes = [2, 1, 2]; + var auth_schemes = [2, 22, 16]; client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_auth_scheme).to.equal(2); - expect(client._sock).to.have.sent(new Uint8Array([2])); + expect(client._rfb_auth_scheme).to.equal(22); + expect(client._sock).to.have.sent(new Uint8Array([22])); }); it('should fail if there are no supported schemes for versions >= 3.7', function () { From 1658466579ee638aa29aaa7adfc18bb64348737c Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 14 Feb 2017 16:20:18 +0100 Subject: [PATCH 399/527] Improve setCapture polyfill Fix some corner cases. Fixes issue #773 --- core/util.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/core/util.js b/core/util.js index e73eb9d0..30f75e1b 100644 --- a/core/util.js +++ b/core/util.js @@ -531,6 +531,14 @@ Util._captureProxy = function (e) { Util._captureElem.dispatchEvent(newEv); Util._captureRecursion = false; + // Avoid double events + e.stopPropagation(); + + // Respect the wishes of the redirected event handlers + if (newEv.defaultPrevented) { + e.preventDefault(); + } + // Implicitly release the capture on button release if ((e.type === "mouseup") || (e.type === "touchend")) { Util.releaseCapture(); @@ -547,6 +555,10 @@ Util.setCapture = function (elem) { elem.addEventListener('touchend', Util.releaseCapture); } else { + // Release any existing capture in case this method is + // called multiple times without coordination + Util.releaseCapture(); + // Safari on iOS 9 has a broken constructor for TouchEvent. // We are fine in this case however, since Safari seems to // have some sort of implicit setCapture magic anyway. @@ -572,6 +584,10 @@ Util.setCapture = function (elem) { captureElem.style.display = "none"; document.body.appendChild(captureElem); + // This is to make sure callers don't get confused by having + // our blocking element as the target + captureElem.addEventListener('contextmenu', Util._captureProxy); + captureElem.addEventListener('mousemove', Util._captureProxy); captureElem.addEventListener('mouseup', Util._captureProxy); @@ -598,8 +614,17 @@ Util.releaseCapture = function () { document.releaseCapture(); } else { + if (!Util._captureElem) { + return; + } + + // There might be events already queued, so we need to wait for + // them to flush. E.g. contextmenu in Microsoft Edge + // + // FIXME: What happens if setCapture is called before this fires? + window.setTimeout(function() { Util._captureElem = null; }); + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); - Util._captureElem = null; captureElem.style.display = "none"; window.removeEventListener('mousemove', Util._captureProxy); From b69dda9b19f538c648cb11faddc50f30b93bac47 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 14 Feb 2017 16:22:56 +0100 Subject: [PATCH 400/527] Clean up some mouse workarounds Adds comments that clarifies why they are needed. Also narrows the handler for contextmenu a bit. --- core/input/devices.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/core/input/devices.js b/core/input/devices.js index e38f9c7e..91a202c5 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -198,7 +198,7 @@ }, _releaseMouse: function () { - Util.releaseCapture(this._target); + Util.releaseCapture(); this._mouseCaptured = false; }, @@ -330,7 +330,12 @@ if (!this._focused) { return true; } var evt = (e ? e : window.event); - /* Stop propagation if inside canvas area */ + /* + * Stop propagation if inside canvas area + * Note: This is only needed for the 'click' event as it fails + * to fire properly for the target element so we have + * to listen on the document element instead. + */ if (evt.target == this._target) { //Util.Debug("mouse event disabled"); Util.stopEvent(e); @@ -357,9 +362,12 @@ c.addEventListener('mousemove', this._eventHandlers.mousemove); c.addEventListener('wheel', this._eventHandlers.mousewheel); - /* Work around right and middle click browser behaviors */ + /* Prevent middle-click pasting (see above for why we bind to document) */ document.addEventListener('click', this._eventHandlers.mousedisable); - document.body.addEventListener('contextmenu', this._eventHandlers.mousedisable); + + /* preventDefault() on mousedown doesn't stop this event for some + reason so we have to explicitly block it */ + c.addEventListener('contextmenu', this._eventHandlers.mousedisable); }, ungrab: function () { @@ -377,10 +385,9 @@ c.removeEventListener('mousemove', this._eventHandlers.mousemove); c.removeEventListener('wheel', this._eventHandlers.mousewheel); - /* Work around right and middle click browser behaviors */ document.removeEventListener('click', this._eventHandlers.mousedisable); - document.body.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); } }; From af1b2ae1e593aba5d14ac84ff41b7a95d29a661e Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 10:43:32 +0100 Subject: [PATCH 401/527] Remove Util.getEventPosition() It mostly dealt with scrolling which we don't use. It also made mistakes in some cases. Remove it and compute the coordinates directly in the calling code. --- core/input/devices.js | 31 ++++++++++++++++++++++++++++--- core/util.js | 35 ----------------------------------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/core/input/devices.js b/core/input/devices.js index 91a202c5..d9e52e2e 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -214,7 +214,7 @@ } var evt = (e ? e : window.event); - var pos = Util.getEventPosition(e, this._target, this._scale); + var pos = this._getMousePosition(evt); var bmask; if (e.touches || e.changedTouches) { @@ -286,7 +286,7 @@ } var evt = (e ? e : window.event); - var pos = Util.getEventPosition(e, this._target, this._scale); + var pos = this._getMousePosition(evt); if (this._onMouseButton) { if (evt.deltaX < 0) { @@ -318,7 +318,7 @@ } var evt = (e ? e : window.event); - var pos = Util.getEventPosition(e, this._target, this._scale); + var pos = this._getMousePosition(evt); if (this._onMouseMove) { this._onMouseMove(pos.x, pos.y); } @@ -345,6 +345,31 @@ return true; }, + // Return coordinates relative to target + _getMousePosition: function(e) { + e = Util.getPointerEvent(e); + var bounds = this._target.getBoundingClientRect(); + var x, y; + // Clip to target bounds + if (e.clientX < bounds.left) { + x = 0; + } else if (e.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = e.clientX - bounds.left; + } + if (e.clientY < bounds.top) { + y = 0; + } else if (e.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = e.clientY - bounds.top; + } + x = x / this._scale; + y = y / this._scale; + return {x:x, y:y}; + }, + // Public methods grab: function () { diff --git a/core/util.js b/core/util.js index 30f75e1b..a3500223 100644 --- a/core/util.js +++ b/core/util.js @@ -194,16 +194,6 @@ Util.decodeUTF8 = function (utf8string) { * Cross-browser routines */ -Util.getPosition = function(obj) { - "use strict"; - // NB(sross): the Mozilla developer reference seems to indicate that - // getBoundingClientRect includes border and padding, so the canvas - // style should NOT include either. - var objPosition = obj.getBoundingClientRect(); - return {'x': objPosition.left + window.pageXOffset, 'y': objPosition.top + window.pageYOffset, - 'width': objPosition.width, 'height': objPosition.height}; -}; - Util.getPointerEvent = function (e) { var evt; evt = (e ? e : window.event); @@ -211,31 +201,6 @@ Util.getPointerEvent = function (e) { return evt; }; -// Get mouse event position in DOM element -Util.getEventPosition = function (e, obj, scale) { - "use strict"; - var evt, docX, docY, pos; - evt = Util.getPointerEvent(e); - if (evt.pageX || evt.pageY) { - docX = evt.pageX; - docY = evt.pageY; - } else if (evt.clientX || evt.clientY) { - docX = evt.clientX + document.body.scrollLeft + - document.documentElement.scrollLeft; - docY = evt.clientY + document.body.scrollTop + - document.documentElement.scrollTop; - } - pos = Util.getPosition(obj); - if (typeof scale === "undefined") { - scale = 1; - } - var realx = docX - pos.x; - var realy = docY - pos.y; - var x = Math.max(Math.min(realx, pos.width - 1), 0); - var y = Math.max(Math.min(realy, pos.height - 1), 0); - return {'x': x / scale, 'y': y / scale, 'realx': realx / scale, 'realy': realy / scale}; -}; - Util.stopEvent = function (e) { e.stopPropagation(); e.preventDefault(); From a0e3ec0ae63ed9f7bcd264ccbdee15e3371b949f Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 10:48:56 +0100 Subject: [PATCH 402/527] Stop using window.event It's an old propriatary IE thing that isn't necessary. --- core/input/devices.js | 30 +++++++++++++----------------- core/util.js | 5 +---- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/core/input/devices.js b/core/input/devices.js index d9e52e2e..0029005c 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -213,8 +213,7 @@ this._notify(e); } - var evt = (e ? e : window.event); - var pos = this._getMousePosition(evt); + var pos = this._getMousePosition(e); var bmask; if (e.touches || e.changedTouches) { @@ -247,14 +246,14 @@ } bmask = this._touchButton; // If bmask is set - } else if (evt.which) { + } else if (e.which) { /* everything except IE */ - bmask = 1 << evt.button; + bmask = 1 << e.button; } else { /* IE including 9 */ - bmask = (evt.button & 0x1) + // Left - (evt.button & 0x2) * 2 + // Right - (evt.button & 0x4) / 2; // Middle + bmask = (e.button & 0x1) + // Left + (e.button & 0x2) * 2 + // Right + (e.button & 0x4) / 2; // Middle } if (this._onMouseButton) { @@ -285,22 +284,21 @@ this._notify(e); } - var evt = (e ? e : window.event); - var pos = this._getMousePosition(evt); + var pos = this._getMousePosition(e); if (this._onMouseButton) { - if (evt.deltaX < 0) { + if (e.deltaX < 0) { this._onMouseButton(pos.x, pos.y, 1, 1 << 5); this._onMouseButton(pos.x, pos.y, 0, 1 << 5); - } else if (evt.deltaX > 0) { + } else if (e.deltaX > 0) { this._onMouseButton(pos.x, pos.y, 1, 1 << 6); this._onMouseButton(pos.x, pos.y, 0, 1 << 6); } - if (evt.deltaY < 0) { + if (e.deltaY < 0) { this._onMouseButton(pos.x, pos.y, 1, 1 << 3); this._onMouseButton(pos.x, pos.y, 0, 1 << 3); - } else if (evt.deltaY > 0) { + } else if (e.deltaY > 0) { this._onMouseButton(pos.x, pos.y, 1, 1 << 4); this._onMouseButton(pos.x, pos.y, 0, 1 << 4); } @@ -317,8 +315,7 @@ this._notify(e); } - var evt = (e ? e : window.event); - var pos = this._getMousePosition(evt); + var pos = this._getMousePosition(e); if (this._onMouseMove) { this._onMouseMove(pos.x, pos.y); } @@ -329,14 +326,13 @@ _handleMouseDisable: function (e) { if (!this._focused) { return true; } - var evt = (e ? e : window.event); /* * Stop propagation if inside canvas area * Note: This is only needed for the 'click' event as it fails * to fire properly for the target element so we have * to listen on the document element instead. */ - if (evt.target == this._target) { + if (e.target == this._target) { //Util.Debug("mouse event disabled"); Util.stopEvent(e); return false; diff --git a/core/util.js b/core/util.js index a3500223..27fc1791 100644 --- a/core/util.js +++ b/core/util.js @@ -195,10 +195,7 @@ Util.decodeUTF8 = function (utf8string) { */ Util.getPointerEvent = function (e) { - var evt; - evt = (e ? e : window.event); - evt = (evt.changedTouches ? evt.changedTouches[0] : evt.touches ? evt.touches[0] : evt); - return evt; + return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; }; Util.stopEvent = function (e) { From 10d1ecc118a617f5d6e3011ae17afc90a9dfa486 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 10:52:18 +0100 Subject: [PATCH 403/527] Remove return value from event handlers It's not used in modern event handlers. --- core/input/devices.js | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/core/input/devices.js b/core/input/devices.js index 0029005c..164442f1 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -69,44 +69,32 @@ }, _handleKeyDown: function (e) { - if (!this._focused) { return true; } + if (!this._focused) { return; } if (this._handler.keydown(e)) { // Suppress bubbling/default actions Util.stopEvent(e); - return false; } else { // Allow the event to bubble and become a keyPress event which // will have the character code translated - return true; } }, _handleKeyPress: function (e) { - if (!this._focused) { return true; } + if (!this._focused) { return; } if (this._handler.keypress(e)) { // Suppress bubbling/default actions Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; } }, _handleKeyUp: function (e) { - if (!this._focused) { return true; } + if (!this._focused) { return; } if (this._handler.keyup(e)) { // Suppress bubbling/default actions Util.stopEvent(e); - return false; - } else { - // Allow the event to bubble and become a keyPress event which - // will have the character code translated - return true; } }, @@ -207,7 +195,7 @@ }, _handleMouseButton: function (e, down) { - if (!this._focused) { return true; } + if (!this._focused) { return; } if (this._notify) { this._notify(e); @@ -262,7 +250,6 @@ this._onMouseButton(pos.x, pos.y, down, bmask); } Util.stopEvent(e); - return false; }, _handleMouseDown: function (e) { @@ -278,7 +265,7 @@ }, _handleMouseWheel: function (e) { - if (!this._focused) { return true; } + if (!this._focused) { return; } if (this._notify) { this._notify(e); @@ -305,11 +292,10 @@ } Util.stopEvent(e); - return false; }, _handleMouseMove: function (e) { - if (! this._focused) { return true; } + if (! this._focused) { return; } if (this._notify) { this._notify(e); @@ -320,11 +306,10 @@ this._onMouseMove(pos.x, pos.y); } Util.stopEvent(e); - return false; }, _handleMouseDisable: function (e) { - if (!this._focused) { return true; } + if (!this._focused) { return; } /* * Stop propagation if inside canvas area @@ -335,10 +320,7 @@ if (e.target == this._target) { //Util.Debug("mouse event disabled"); Util.stopEvent(e); - return false; } - - return true; }, // Return coordinates relative to target From 8cbf1dd9d220950ea5163a3c2da2ebca0c7bc450 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 13:25:34 +0100 Subject: [PATCH 404/527] Set correct cursor style for setCapture() --- core/util.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/util.js b/core/util.js index 27fc1791..03e69256 100644 --- a/core/util.js +++ b/core/util.js @@ -507,6 +507,13 @@ Util._captureProxy = function (e) { } }; +// Follow cursor style of target element +Util._captureElemChanged = function() { + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); + captureElem.style.cursor = window.getComputedStyle(Util._captureElem).cursor; +}; +Util._captureObserver = new MutationObserver(Util._captureElemChanged); + Util.setCapture = function (elem) { if (elem.setCapture) { @@ -558,6 +565,11 @@ Util.setCapture = function (elem) { } Util._captureElem = elem; + + // Track cursor and get initial cursor + Util._captureObserver.observe(elem, {attributes:true}); + Util._captureElemChanged(); + captureElem.style.display = null; // We listen to events on window in order to keep tracking if it @@ -586,6 +598,8 @@ Util.releaseCapture = function () { // FIXME: What happens if setCapture is called before this fires? window.setTimeout(function() { Util._captureElem = null; }); + Util._captureObserver.disconnect(); + var captureElem = document.getElementById("noVNC_mouse_capture_elem"); captureElem.style.display = "none"; From 280676c7e9df28f703003f68340989d30b2c0f48 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 13:32:11 +0100 Subject: [PATCH 405/527] Properly encapsulate the scale in Display Other parts of the code shouldn't have to care about this. Let Display convert between canvas coordinates and framebuffer coordinates. --- app/ui.js | 8 +------- core/display.js | 6 ++---- core/input/devices.js | 4 ---- tests/test.display.js | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/ui.js b/app/ui.js index 98bb5f63..074fcd6c 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1227,7 +1227,6 @@ var UI; var display = UI.rfb.get_display(); var resizeMode = UI.getSetting('resize'); display.set_scale(1); - UI.rfb.get_mouse().set_scale(1); if (resizeMode === 'remote') { @@ -1247,12 +1246,7 @@ var UI; } else if (resizeMode === 'scale' || resizeMode === 'downscale') { var downscaleOnly = resizeMode === 'downscale'; - var scaleRatio = display.autoscale(screen.w, screen.h, downscaleOnly); - - if (!UI.rfb.get_view_only()) { - UI.rfb.get_mouse().set_scale(scaleRatio); - Util.Debug('Scaling by ' + UI.rfb.get_mouse().get_scale()); - } + display.autoscale(screen.w, screen.h, downscaleOnly); } } }, diff --git a/core/display.js b/core/display.js index 8da80317..57e07d57 100644 --- a/core/display.js +++ b/core/display.js @@ -193,11 +193,11 @@ }, absX: function (x) { - return x + this._viewportLoc.x; + return x / this._scale + this._viewportLoc.x; }, absY: function (y) { - return y + this._viewportLoc.y; + return y / this._scale + this._viewportLoc.y; }, resize: function (width, height) { @@ -589,8 +589,6 @@ } this._rescale(scaleRatio); - - return scaleRatio; // so that the mouse, etc scale can be set }, // Private Methods diff --git a/core/input/devices.js b/core/input/devices.js index 164442f1..2e41122e 100644 --- a/core/input/devices.js +++ b/core/input/devices.js @@ -161,7 +161,6 @@ Util.set_defaults(this, defaults, { 'target': document, 'focused': true, - 'scale': 1.0, 'touchButton': 1 }); @@ -343,8 +342,6 @@ } else { y = e.clientY - bounds.top; } - x = x / this._scale; - y = y / this._scale; return {x:x, y:y}; }, @@ -398,7 +395,6 @@ ['target', 'ro', 'dom'], // DOM element that captures mouse input ['notify', 'ro', 'func'], // Function to call to notify whenever a mouse event is received ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement - ['scale', 'rw', 'float'], // Viewport scale factor 0.0 - 1.0 ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release ['onMouseMove', 'rw', 'func'], // Handler for mouse movement diff --git a/tests/test.display.js b/tests/test.display.js index 5f4eed12..e4e33489 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -274,13 +274,17 @@ describe('Display/Canvas Helper', function () { }); it('should use width to determine scale when the current aspect ratio is wider than the target', function () { - expect(display.autoscale(9, 16)).to.equal(9 / 4); + display.autoscale(9, 16); + expect(display.absX(9)).to.equal(4); + expect(display.absY(18)).to.equal(8); expect(canvas.clientWidth).to.equal(9); expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) }); it('should use height to determine scale when the current aspect ratio is taller than the target', function () { - expect(display.autoscale(16, 9)).to.equal(3); // 9 / 3 + display.autoscale(16, 9); + expect(display.absX(9)).to.equal(3); + expect(display.absY(18)).to.equal(6); expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) expect(canvas.clientHeight).to.equal(9); @@ -293,11 +297,15 @@ describe('Display/Canvas Helper', function () { }); it('should not upscale when downscaleOnly is true', function () { - expect(display.autoscale(2, 2, true)).to.equal(0.5); + display.autoscale(2, 2, true); + expect(display.absX(9)).to.equal(18); + expect(display.absY(18)).to.equal(36); expect(canvas.clientWidth).to.equal(2); expect(canvas.clientHeight).to.equal(2); - expect(display.autoscale(16, 9, true)).to.equal(1.0); + display.autoscale(16, 9, true); + expect(display.absX(9)).to.equal(9); + expect(display.absY(18)).to.equal(18); expect(canvas.clientWidth).to.equal(4); expect(canvas.clientHeight).to.equal(3); }); From f3b7727535dbe5292dda867f88c43a19010dd344 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 14:48:34 +0100 Subject: [PATCH 406/527] Fix canvas size on IE --- app/styles/base.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/styles/base.css b/app/styles/base.css index d166bb34..aa7f8971 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -819,6 +819,8 @@ select:active { * settings and noVNC settings. */ #noVNC_canvas { margin: auto; + /* IE miscalculates width without this :( */ + flex-shrink: 0; } /*Default noVNC logo.*/ From b18ef8162ea5fd0a72f4aa910f7fc9236a8f81f1 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 15:14:08 +0100 Subject: [PATCH 407/527] Allow scroll bars on Safari and IE The previous problems were because we were scrolling the wrong element, so we can enable this feature now. --- app/ui.js | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/app/ui.js b/app/ui.js index 074fcd6c..7d4f0811 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1315,34 +1315,11 @@ var UI; enableDisableViewClip: function() { var resizeSetting = UI.getSetting('resize'); - if (UI.isSafari) { - // Safari auto-hides the scrollbars which makes them - // impossible to use in most cases - UI.setViewClip(true); - document.getElementById('noVNC_setting_clip').disabled = true; - } else if (resizeSetting === 'downscale' || resizeSetting === 'scale') { + if (resizeSetting === 'downscale' || resizeSetting === 'scale') { // Disable clipping if we are scaling UI.forceSetting('clip', false); UI.setViewClip(false); document.getElementById('noVNC_setting_clip').disabled = true; - } else if (document.msFullscreenElement) { - // The browser is IE and we are in fullscreen mode. - // - We need to force clipping while in fullscreen since - // scrollbars doesn't work. - var msg = _("Forcing clipping mode since " + - "scrollbars aren't supported " + - "by IE in fullscreen"); - Util.Debug(msg); - UI.showStatus(msg); - UI.rememberedClipSetting = UI.getSetting('clip'); - UI.setViewClip(true); - document.getElementById('noVNC_setting_clip').disabled = true; - } else if (document.body.msRequestFullscreen && - UI.rememberedClipSetting !== null) { - // Restore view clip to what it was before fullscreen on IE - UI.setViewClip(UI.rememberedClipSetting); - document.getElementById('noVNC_setting_clip').disabled = - UI.connected || Util.isTouchDevice; } else { document.getElementById('noVNC_setting_clip').disabled = UI.connected || Util.isTouchDevice; From 631428d9668e485736470b0647fd6514cbd503a3 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 15:24:53 +0100 Subject: [PATCH 408/527] Fix status class for IE IE apparently doesn't support removing several class specifiers at once, so switch to several calls. --- app/ui.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/ui.js b/app/ui.js index 7d4f0811..75eeab92 100644 --- a/app/ui.js +++ b/app/ui.js @@ -560,9 +560,9 @@ var UI; status_type = 'normal'; } - statusElem.classList.remove("noVNC_status_normal", - "noVNC_status_warn", - "noVNC_status_error"); + statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.remove("noVNC_status_error"); switch (status_type) { case 'warning': From 5f38376adbf5474f416442d1aa0aa06d942f6c93 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 15:37:55 +0100 Subject: [PATCH 409/527] Don't change stored setting when "forcing" Don't overwrite the user's configured choice. Instead ignore settings when they cannot be respected. --- app/ui.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/app/ui.js b/app/ui.js index 75eeab92..d4a7555d 100644 --- a/app/ui.js +++ b/app/ui.js @@ -133,9 +133,6 @@ var UI; document.documentElement.classList.add("noVNC_touch"); // Remove the address bar setTimeout(function() { window.scrollTo(0, 1); }, 100); - UI.forceSetting('clip', true); - } else { - UI.initSetting('clip', false); } // Restore control bar position @@ -228,6 +225,7 @@ var UI; UI.initSetting('encrypt', (window.location.protocol === "https:")); UI.initSetting('true_color', true); UI.initSetting('cursor', !Util.isTouchDevice); + UI.initSetting('clip', false); UI.initSetting('resize', 'off'); UI.initSetting('shared', true); UI.initSetting('view_only', false); @@ -849,12 +847,6 @@ var UI; return val; }, - // Force a setting to be a certain value - forceSetting: function(name, val) { - UI.updateSetting(name, val); - return val; - }, - // Read form control compatible setting from cookie getSetting: function(name) { var ctrl = document.getElementById('noVNC_setting_' + name); @@ -1298,6 +1290,15 @@ var UI; var cur_clip = display.get_viewport(); var new_clip = UI.getSetting('clip'); + var resizeSetting = UI.getSetting('resize'); + if (resizeSetting === 'downscale' || resizeSetting === 'scale') { + // Disable clipping if we are scaling + new_clip = false; + } else if (Util.isTouchDevice) { + // Touch devices usually have shit scrollbars + new_clip = true; + } + if (cur_clip !== new_clip) { display.set_viewport(new_clip); } @@ -1314,18 +1315,12 @@ var UI; // Handle special cases where clipping is forced on/off or locked enableDisableViewClip: function() { var resizeSetting = UI.getSetting('resize'); - if (resizeSetting === 'downscale' || resizeSetting === 'scale') { // Disable clipping if we are scaling - UI.forceSetting('clip', false); - UI.setViewClip(false); document.getElementById('noVNC_setting_clip').disabled = true; } else { document.getElementById('noVNC_setting_clip').disabled = UI.connected || Util.isTouchDevice; - if (Util.isTouchDevice) { - UI.setViewClip(true); - } } }, From 6bfd9dc96f083e435a8ba60426f8d66f857a5e89 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 15:38:58 +0100 Subject: [PATCH 410/527] Update clipping before changing scaling The scaling is relative the current viewport, so we need to make sure it is correct before changing the scale setting. --- app/ui.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/ui.js b/app/ui.js index d4a7555d..0ea85b33 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1220,6 +1220,9 @@ var UI; var resizeMode = UI.getSetting('resize'); display.set_scale(1); + // Make sure the viewport is adjusted first + UI.updateViewClip(); + if (resizeMode === 'remote') { // Request changing the resolution of the remote display to From b56772ec43773b67981027dc1774a584e1d8bb23 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Thu, 16 Feb 2017 15:53:48 +0100 Subject: [PATCH 411/527] Update drag button at right time It needs to update whenever the viewport or clipping changes, so let's make sure that actually happens and not trigger on mildly related events. --- app/ui.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index 0ea85b33..7854b5cb 100644 --- a/app/ui.js +++ b/app/ui.js @@ -266,7 +266,6 @@ var UI; addResizeHandlers: function() { window.addEventListener('resize', UI.applyResizeMode); window.addEventListener('resize', UI.updateViewClip); - window.addEventListener('resize', UI.updateViewDrag); }, addControlbarHandlers: function() { @@ -1313,6 +1312,10 @@ var UI; // the size of the browser window. display.viewportChangeSize(size.w, size.h); } + + // Changing the viewport may change the state of + // the dragging button + UI.updateViewDrag(); }, // Handle special cases where clipping is forced on/off or locked @@ -1664,7 +1667,6 @@ var UI; updateSessionSize: function(rfb, width, height) { UI.updateViewClip(); - UI.updateViewDrag(); }, updateDesktopName: function(rfb, name) { From e677b66cc77655ee9707818c5a189048691f78d3 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 16 Feb 2017 16:28:55 +0100 Subject: [PATCH 412/527] Proper workaround for Chrome's scrollbar bug Avoid applying this workaround more than necessary. Also comment a bit more why this hack is needed. --- app/ui.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/ui.js b/app/ui.js index 7854b5cb..ad80c63b 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1241,6 +1241,7 @@ var UI; } else if (resizeMode === 'scale' || resizeMode === 'downscale') { var downscaleOnly = resizeMode === 'downscale'; display.autoscale(screen.w, screen.h, downscaleOnly); + UI.fixScrollbars(); } } }, @@ -1248,16 +1249,7 @@ var UI; // Gets the the size of the available viewport in the browser window screenSize: function() { var screen = document.getElementById('noVNC_screen'); - var width, height; - - screen.style.overflow = "hidden"; - - width = screen.offsetWidth; - height = screen.offsetHeight; - - screen.style.overflow = "auto"; - - return {w: width, h: height}; + return {w: screen.offsetWidth, h: screen.offsetHeight}; }, // Normally we only apply the current resize mode after a window resize @@ -1311,6 +1303,7 @@ var UI; // When clipping is enabled, the screen is limited to // the size of the browser window. display.viewportChangeSize(size.w, size.h); + UI.fixScrollbars(); } // Changing the viewport may change the state of @@ -1667,6 +1660,19 @@ var UI; updateSessionSize: function(rfb, width, height) { UI.updateViewClip(); + UI.fixScrollbars(); + }, + + fixScrollbars: function() { + // This is a hack because Chrome screws up the calculation + // for when scrollbars are needed. So to fix it we temporarily + // toggle them off and on. + var screen = document.getElementById('noVNC_screen'); + screen.style.overflow = 'hidden'; + // Force Chrome to recalculate the layout by asking for + // an element's dimensions + screen.getBoundingClientRect(); + screen.style.overflow = null; }, updateDesktopName: function(rfb, name) { From c3325dc6f7ab0a7f63a6933ab160f78cd92193c5 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 17 Feb 2017 12:43:59 +0100 Subject: [PATCH 413/527] Fix translateDOM for placeholders Replaced 'in' with a comma, like in the surrounding code. --- core/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/util.js b/core/util.js index 03e69256..1130f825 100644 --- a/core/util.js +++ b/core/util.js @@ -452,7 +452,7 @@ Util.Localisation = { } // FIXME: Should update "lang" if (elem.hasAttribute("placeholder") && - isAnyOf(elem.tagName in ["INPUT", "TEXTAREA"])) { + isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { translateAttribute(elem, "placeholder"); } if (elem.hasAttribute("title")) { From 0298305e809bb77b1121ec3cd0096f239c0ca427 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 18 Feb 2017 16:30:35 +0100 Subject: [PATCH 414/527] Allow clipping to be changed while connected --- app/ui.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/ui.js b/app/ui.js index ad80c63b..2dd45363 100644 --- a/app/ui.js +++ b/app/ui.js @@ -421,6 +421,7 @@ var UI; UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); UI.addSettingChangeHandler('resize', UI.applyResizeMode); UI.addSettingChangeHandler('clip'); + UI.addSettingChangeHandler('clip', UI.updateViewClip); UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('view_only'); UI.addSettingChangeHandler('host'); @@ -1319,7 +1320,7 @@ var UI; document.getElementById('noVNC_setting_clip').disabled = true; } else { document.getElementById('noVNC_setting_clip').disabled = - UI.connected || Util.isTouchDevice; + Util.isTouchDevice; } }, From f78a652e862f3c5f582b2dffcd084711ca09e79c Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 18 Feb 2017 20:39:57 +0100 Subject: [PATCH 415/527] Don't send or recieve clipboard in view_only --- core/rfb.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/rfb.js b/core/rfb.js index e26e7b12..eb939712 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -343,7 +343,7 @@ }, clipboardPasteFrom: function (text) { - if (this._rfb_connection_state !== 'connected') { return; } + if (this._rfb_connection_state !== 'connected' || this._view_only) { return; } RFB.messages.clientCutText(this._sock, text); }, @@ -1189,6 +1189,8 @@ _handle_server_cut_text: function () { Util.Debug("ServerCutText"); + if (this._view_only) { return true; } + if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } this._sock.rQskipBytes(3); // Padding var length = this._sock.rQshift32(); From ef1e8bab2298492cc08c9e0b8df74c14c278cd3c Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 18 Feb 2017 20:43:25 +0100 Subject: [PATCH 416/527] Allow view_only to be changed while connected --- app/ui.js | 9 +++++++-- core/rfb.js | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index 2dd45363..e2a2b3d1 100644 --- a/app/ui.js +++ b/app/ui.js @@ -424,6 +424,7 @@ var UI; UI.addSettingChangeHandler('clip', UI.updateViewClip); UI.addSettingChangeHandler('shared'); UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -506,7 +507,6 @@ var UI; UI.enableDisableViewClip(); document.getElementById('noVNC_setting_shared').disabled = UI.connected; - document.getElementById('noVNC_setting_view_only').disabled = UI.connected; document.getElementById('noVNC_setting_host').disabled = UI.connected; document.getElementById('noVNC_setting_port').disabled = UI.connected; document.getElementById('noVNC_setting_path').disabled = UI.connected; @@ -1065,9 +1065,10 @@ var UI; UI.rfb.set_true_color(UI.getSetting('true_color')); UI.rfb.set_local_cursor(UI.getSetting('cursor')); UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_view_only(UI.getSetting('view_only')); UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + UI.updateViewOnly(); + UI.rfb.connect(host, port, password, path); }, @@ -1655,6 +1656,10 @@ var UI; } }, + updateViewOnly: function() { + UI.rfb.set_view_only(UI.getSetting('view_only')); + }, + updateLogging: function() { WebUtil.init_logging(UI.getSetting('logging')); }, diff --git a/core/rfb.js b/core/rfb.js index eb939712..2de974b7 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1490,6 +1490,21 @@ } }; + RFB.prototype.set_view_only = function (view_only) { + this._view_only = view_only; + + if (this._rfb_connection_state === "connecting" || + this._rfb_connection_state === "connected") { + if (view_only) { + this._keyboard.ungrab(); + this._mouse.ungrab(); + } else { + this._keyboard.grab(); + this._mouse.grab(); + } + } + }; + RFB.prototype.get_display = function () { return this._display; }; RFB.prototype.get_keyboard = function () { return this._keyboard; }; RFB.prototype.get_mouse = function () { return this._mouse; }; From 3a535adab0b66410ae8fea49791d273f13a7f89f Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sat, 18 Feb 2017 22:01:12 +0100 Subject: [PATCH 417/527] Allow auto reconnect to be changed while connected --- app/ui.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index e2a2b3d1..8be07b2c 100644 --- a/app/ui.js +++ b/app/ui.js @@ -511,8 +511,6 @@ var UI; document.getElementById('noVNC_setting_port').disabled = UI.connected; document.getElementById('noVNC_setting_path').disabled = UI.connected; document.getElementById('noVNC_setting_repeaterID').disabled = UI.connected; - document.getElementById('noVNC_setting_reconnect').disabled = UI.connected; - document.getElementById('noVNC_setting_reconnect_delay').disabled = UI.connected; if (UI.connected) { UI.updateViewClip(); From fb49f91b00a2fe3a9e1a71cce05c6b9527306341 Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Sun, 19 Feb 2017 01:52:47 +0100 Subject: [PATCH 418/527] Allow local cursor to be changed while connected --- app/ui.js | 9 +++++++-- core/rfb.js | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/ui.js b/app/ui.js index 8be07b2c..db1bfc9e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -417,6 +417,7 @@ var UI; UI.addSettingChangeHandler('encrypt'); UI.addSettingChangeHandler('true_color'); UI.addSettingChangeHandler('cursor'); + UI.addSettingChangeHandler('cursor', UI.updateLocalCursor); UI.addSettingChangeHandler('resize'); UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); UI.addSettingChangeHandler('resize', UI.applyResizeMode); @@ -499,7 +500,7 @@ var UI; document.getElementById('noVNC_setting_encrypt').disabled = UI.connected; document.getElementById('noVNC_setting_true_color').disabled = UI.connected; if (Util.browserSupportsCursorURIs()) { - document.getElementById('noVNC_setting_cursor').disabled = UI.connected; + document.getElementById('noVNC_setting_cursor').disabled = false; } else { UI.updateSetting('cursor', !Util.isTouchDevice); document.getElementById('noVNC_setting_cursor').disabled = true; @@ -1061,10 +1062,10 @@ var UI; UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_true_color(UI.getSetting('true_color')); - UI.rfb.set_local_cursor(UI.getSetting('cursor')); UI.rfb.set_shared(UI.getSetting('shared')); UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); + UI.updateLocalCursor(); UI.updateViewOnly(); UI.rfb.connect(host, port, password, path); @@ -1654,6 +1655,10 @@ var UI; } }, + updateLocalCursor: function() { + UI.rfb.set_local_cursor(UI.getSetting('cursor')); + }, + updateViewOnly: function() { UI.rfb.set_view_only(UI.getSetting('view_only')); }, diff --git a/core/rfb.js b/core/rfb.js index 2de974b7..d094db91 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -1488,6 +1488,12 @@ this._display.disableLocalCursor(); } } + + // Need to send an updated list of encodings if we are connected + if (this._rfb_connection_state === "connected") { + RFB.messages.clientEncodings(this._sock, this._encodings, cursor, + this._true_color); + } }; RFB.prototype.set_view_only = function (view_only) { From a7ca8e5c1a709b1ca70b173d4f610cf5fe3f8aba Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Mon, 20 Feb 2017 16:44:54 +0100 Subject: [PATCH 419/527] Fix wrong colours on local cursor The RGB order was backwards compared to the pixel format we've requested, resulting in the red and blue channels getting swapped. --- core/display.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/display.js b/core/display.js index 57e07d57..f7c437b4 100644 --- a/core/display.js +++ b/core/display.js @@ -841,9 +841,9 @@ cur.push(alpha); // alpha } else { idx = ((w0 * y) + x) * 4; - cur.push(pixels[idx + 2]); // blue + cur.push(pixels[idx]); // blue cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx]); // red + cur.push(pixels[idx + 2]); // red cur.push(alpha); // alpha } } From 6c857dc50c266a0599bc151a3d0a64bda69807de Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 17 Feb 2017 15:17:50 +0100 Subject: [PATCH 420/527] Set input type number for port & reconnect_delay --- app/styles/base.css | 5 +++-- vnc.html | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index aa7f8971..1ff8cf84 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -80,7 +80,8 @@ html { * ---------------------------------------- */ -input[type=input], input[type=password], input:not([type]), textarea { +input[type=input], input[type=password], input[type=number], +input:not([type]), textarea { /* Disable default rendering */ -webkit-appearance: none; -moz-appearance: none; @@ -138,7 +139,7 @@ input[type=submit]::-moz-focus-inner { input[type=input]:disabled, input[type=password]:disabled, input:not([type]):disabled, input[type=button]:disabled, -input[type=submit]:disabled, +input[type=submit]:disabled, input[type=number]:disabled, textarea:disabled, select:disabled { color: rgb(128, 128, 128); background: rgb(240, 240, 240); diff --git a/vnc.html b/vnc.html index c6f0d25b..6567a013 100644 --- a/vnc.html +++ b/vnc.html @@ -222,7 +222,7 @@
  • - +
  • @@ -236,7 +236,7 @@
  • - +

  • From 24584cca897937d21891bf43a983fe679f61dcfd Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Fri, 17 Feb 2017 16:16:29 +0100 Subject: [PATCH 421/527] Disable labels for settings as well Makes the settings UI easier to read. --- app/styles/base.css | 4 ++ app/ui.js | 90 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/app/styles/base.css b/app/styles/base.css index 1ff8cf84..6bb23a47 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -41,6 +41,10 @@ html { display: none; } +.noVNC_disabled { + color: rgb(128, 128, 128); +} + /* ---------------------------------------- * Spinner * ---------------------------------------- diff --git a/app/ui.js b/app/ui.js index ad80c63b..e70419b3 100644 --- a/app/ui.js +++ b/app/ui.js @@ -233,6 +233,29 @@ var UI; UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); UI.initSetting('reconnect_delay', 5000); + + UI.setupSettingLabels(); + }, + + // Adds a link to the label elements on the corresponding input elements + setupSettingLabels: function() { + var labels = document.getElementsByTagName('LABEL'); + for (var i = 0; i < labels.length; i++) { + var htmlFor = labels[i].htmlFor; + if (htmlFor != '') { + var elem = document.getElementById(htmlFor); + if (elem) elem.label = labels[i]; + } else { + // If 'for' isn't set, use the first input element child + var children = labels[i].children; + for (var j = 0; j < children.length; j++) { + if (children[j].form !== undefined) { + children[j].label = labels[i]; + break; + } + } + } + } }, initRFB: function() { @@ -494,32 +517,40 @@ var UI; // Disable/enable controls depending on connection state updateVisualState: function() { //Util.Debug(">> updateVisualState"); - document.getElementById('noVNC_setting_encrypt').disabled = UI.connected; - document.getElementById('noVNC_setting_true_color').disabled = UI.connected; - if (Util.browserSupportsCursorURIs()) { - document.getElementById('noVNC_setting_cursor').disabled = UI.connected; - } else { - UI.updateSetting('cursor', !Util.isTouchDevice); - document.getElementById('noVNC_setting_cursor').disabled = true; - } UI.enableDisableViewClip(); - document.getElementById('noVNC_setting_shared').disabled = UI.connected; - document.getElementById('noVNC_setting_view_only').disabled = UI.connected; - document.getElementById('noVNC_setting_host').disabled = UI.connected; - document.getElementById('noVNC_setting_port').disabled = UI.connected; - document.getElementById('noVNC_setting_path').disabled = UI.connected; - document.getElementById('noVNC_setting_repeaterID').disabled = UI.connected; - document.getElementById('noVNC_setting_reconnect').disabled = UI.connected; - document.getElementById('noVNC_setting_reconnect_delay').disabled = UI.connected; if (UI.connected) { + UI.disableSetting('encrypt'); + UI.disableSetting('true_color'); + UI.disableSetting('cursor'); + UI.disableSetting('shared'); + UI.disableSetting('view_only'); + UI.disableSetting('host'); + UI.disableSetting('port'); + UI.disableSetting('path'); + UI.disableSetting('repeaterID'); + UI.disableSetting('reconnect'); + UI.disableSetting('reconnect_delay'); UI.updateViewClip(); UI.setMouseButton(1); // Hide the controlbar after 2 seconds UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); } else { + UI.enableSetting('encrypt'); + UI.enableSetting('true_color'); + if (Util.browserSupportsCursorURIs() && !Util.isTouchDevice) { + UI.enableSetting('cursor'); + } + UI.enableSetting('shared'); + UI.enableSetting('view_only'); + UI.enableSetting('host'); + UI.enableSetting('port'); + UI.enableSetting('path'); + UI.enableSetting('repeaterID'); + UI.enableSetting('reconnect'); + UI.enableSetting('reconnect_delay'); UI.updateXvpButton(0); UI.keepControlbar(); } @@ -860,6 +891,21 @@ var UI; return val; }, + // These helpers compensate for the lack of parent-selectors and + // previous-sibling-selectors in CSS which are needed when we want to + // disable the labels that belong to disabled input elements. + disableSetting: function(name) { + var ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = true; + ctrl.label.classList.add('noVNC_disabled'); + }, + + enableSetting: function(name) { + var ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = false; + ctrl.label.classList.remove('noVNC_disabled'); + }, + /* ------^------- * /SETTINGS * ============== @@ -890,7 +936,7 @@ var UI; UI.updateSetting('cursor'); } else { UI.updateSetting('cursor', !Util.isTouchDevice); - document.getElementById('noVNC_setting_cursor').disabled = true; + UI.disableSetting('cursor'); } UI.updateSetting('clip'); UI.updateSetting('resize'); @@ -1314,12 +1360,12 @@ var UI; // Handle special cases where clipping is forced on/off or locked enableDisableViewClip: function() { var resizeSetting = UI.getSetting('resize'); - if (resizeSetting === 'downscale' || resizeSetting === 'scale') { - // Disable clipping if we are scaling - document.getElementById('noVNC_setting_clip').disabled = true; + // Disable clipping if we are scaling, connected or on touch + if (resizeSetting === 'downscale' || resizeSetting === 'scale' || + UI.connected || Util.isTouchDevice) { + UI.disableSetting('clip'); } else { - document.getElementById('noVNC_setting_clip').disabled = - UI.connected || Util.isTouchDevice; + UI.enableSetting('clip'); } }, From 90ecc739dff20be8a56ad9f395f4e3ca2aca20ce Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 23 Feb 2017 14:26:50 +0100 Subject: [PATCH 422/527] Protect against race in setCapture() polyfill It might take a long time for a timer to fire, long enough for a new grab to be initiated. Clearing out the capture element would then cause a crash. --- core/util.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/util.js b/core/util.js index 03e69256..280c4154 100644 --- a/core/util.js +++ b/core/util.js @@ -514,6 +514,8 @@ Util._captureElemChanged = function() { }; Util._captureObserver = new MutationObserver(Util._captureElemChanged); +Util._captureIndex = 0; + Util.setCapture = function (elem) { if (elem.setCapture) { @@ -565,6 +567,7 @@ Util.setCapture = function (elem) { } Util._captureElem = elem; + Util._captureIndex++; // Track cursor and get initial cursor Util._captureObserver.observe(elem, {attributes:true}); @@ -594,9 +597,13 @@ Util.releaseCapture = function () { // There might be events already queued, so we need to wait for // them to flush. E.g. contextmenu in Microsoft Edge - // - // FIXME: What happens if setCapture is called before this fires? - window.setTimeout(function() { Util._captureElem = null; }); + window.setTimeout(function(expected) { + // Only clear it if it's the expected grab (i.e. no one + // else has initiated a new grab) + if (Util._captureIndex === expected) { + Util._captureElem = null; + } + }, 0, Util._captureIndex); Util._captureObserver.disconnect(); From 6e8a8c8df186b528faddd3f48035db100f4d1373 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 23 Feb 2017 16:29:16 +0100 Subject: [PATCH 423/527] Remove 512x512 icon It is not a launcher icon, but rather used for display on Google Play. As such it is not needed and removing it mitigates the issue of some buggy browsers downloading each and every listed icon. --- app/images/icons/Makefile | 3 +-- app/images/icons/novnc-512x512.png | Bin 16284 -> 0 bytes vnc.html | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 app/images/icons/novnc-512x512.png diff --git a/app/images/icons/Makefile b/app/images/icons/Makefile index 1b3d7199..be564b43 100644 --- a/app/images/icons/Makefile +++ b/app/images/icons/Makefile @@ -10,8 +10,7 @@ ANDROID_LAUNCHER := \ novnc-72x72.png \ novnc-96x96.png \ novnc-144x144.png \ - novnc-192x192.png \ - novnc-512x512.png + novnc-192x192.png IPHONE_LAUNCHER := \ novnc-60x60.png \ diff --git a/app/images/icons/novnc-512x512.png b/app/images/icons/novnc-512x512.png deleted file mode 100644 index f6cfd2463d301e4322e27eaa21e36cecefd40062..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16284 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