diff options
author | Solly <directxman12+github@gmail.com> | 2014-09-15 16:50:20 -0400 |
---|---|---|
committer | Solly <directxman12+github@gmail.com> | 2014-09-15 16:50:20 -0400 |
commit | 0ca7cf4867f1dae585d254f16ad62c552648265f (patch) | |
tree | 99095fa63bae8f562d889f0ce155014cb915f3f1 | |
parent | 91127741bea2238277a426a382fec3740103d606 (diff) | |
parent | e6af0f60b061933dee1a8637aeb7bba7dd65b133 (diff) | |
download | novnc-0ca7cf4867f1dae585d254f16ad62c552648265f.tar.gz |
Merge pull request #368 from DirectXMan12/refactor/cleanup
Cleanup and test all the things (plus ditching Crockford)!
-rw-r--r-- | .travis.yml | 13 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | include/base64.js | 194 | ||||
-rw-r--r-- | include/des.js | 347 | ||||
-rw-r--r-- | include/display.js | 1390 | ||||
-rw-r--r-- | include/input.js | 739 | ||||
-rw-r--r-- | include/keyboard.js | 29 | ||||
-rw-r--r-- | include/rfb.js | 3592 | ||||
-rw-r--r-- | include/ui.js | 1928 | ||||
-rw-r--r-- | include/util.js | 572 | ||||
-rw-r--r-- | include/websock.js | 658 | ||||
-rw-r--r-- | include/webutil.js | 104 | ||||
-rw-r--r-- | karma.conf.js | 191 | ||||
-rw-r--r-- | package.json | 50 | ||||
-rw-r--r-- | tests/fake.websocket.js | 96 | ||||
-rw-r--r-- | tests/run_from_console.casper.js | 2 | ||||
-rwxr-xr-x | tests/run_from_console.js | 12 | ||||
-rw-r--r-- | tests/test.base64.js | 33 | ||||
-rw-r--r-- | tests/test.display.js | 332 | ||||
-rw-r--r-- | tests/test.helper.js | 4 | ||||
-rw-r--r-- | tests/test.keyboard.js | 5 | ||||
-rw-r--r-- | tests/test.rfb.js | 1696 | ||||
-rw-r--r-- | tests/test.util.js | 105 | ||||
-rw-r--r-- | tests/test.websock.js | 480 |
24 files changed, 7784 insertions, 4789 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6c594a8 --- /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 @@ -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/include/base64.js b/include/base64.js index 5a6890a..651fbad 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 : '=', + + 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. + + 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]; + } -/* 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); - 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; + // 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]; } - - // 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; + 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; } - } - /* 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; -} + // 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 of Base64 namespace */ diff --git a/include/des.js b/include/des.js index 1f95285..ecbc819 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<<m)) !== 0) ? 1: 0; - } + 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<<m)) !== 0) ? 1: 0; + } - for (i = 0; i < 16; ++i) { - m = i << 1; - n = m + 1; - kn[m] = kn[n] = 0; - for (o=28; o<59; o+=28) { - for (j = o-28; j < o; ++j) { - l = j + totrot[i]; - if (l < o) { - pcr[j] = pc1m[l]; - } else { - pcr[j] = pc1m[l - 28]; + for (i = 0; i < 16; ++i) { + m = i << 1; + n = m + 1; + kn[m] = kn[n] = 0; + for (o = 28; o < 59; o += 28) { + for (j = o - 28; j < o; ++j) { + l = j + totrot[i]; + if (l < o) { + pcr[j] = pc1m[l]; + } else { + pcr[j] = pc1m[l - 28]; + } } } - } - for (j = 0; j < 24; ++j) { - if (pcr[PC2[j]] !== 0) { - kn[m] |= 1<<(23-j); - } - if (pcr[PC2[j + 24]] !== 0) { - kn[n] |= 1<<(23-j); + for (j = 0; j < 24; ++j) { + if (pcr[PC2[j]] !== 0) { + kn[m] |= 1 << (23 - j); + } + if (pcr[PC2[j + 24]] !== 0) { + kn[n] |= 1 << (23 - j); + } } } - } - // 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; + // 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); + 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 + // 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; } - 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))); -} + // 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 diff --git a/include/display.js b/include/display.js index 9f2d6b8..e8ac63d 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 that = {}, // Public API methods - conf = {}, // Configuration attributes - - // Private Display namespace variables - c_ctx = null, - c_forceCanvas = false, - - // Queued drawing actions for in-order rendering - renderQ = [], - - // Predefine function variables (jslint) - imageDataGet, rgbImageData, bgrxImageData, cmapImageData, - setFillColor, rescale, scan_renderQ, - - // 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}, - - c_prevStyle = "", - tile = null, - tile16x16 = null, - tile_x = 0, - tile_y = 0; - - -// 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'], - - ['render_mode', 'ro', 'str', '', 'Canvas rendering mode (read-only)'], - - ['prefer_js', 'rw', 'str', null, 'Prefer Javascript over canvas methods'], - ['cursor_uri', 'rw', 'raw', null, 'Can we render cursor using data URI'] - ]); +var Display; -// Override some specific getters/setters -that.get_context = function () { return c_ctx; }; +(function () { + "use strict"; -that.set_scale = function(scale) { rescale(scale); }; + Display = function (defaults) { + this._drawCtx = null; + this._c_forceCanvas = false; -that.set_width = function (val) { that.resize(val, fb_height); }; -that.get_width = function() { return fb_width; }; + this._renderQ = []; // queue drawing actions for in-oder rendering -that.set_height = function (val) { that.resize(fb_width, val); }; -that.get_height = function() { return fb_height; }; + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 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 }; + this._prevDrawStyle = ""; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; -// -// Private functions -// + Util.set_defaults(this, defaults, { + 'true_color': true, + 'colourMap': [], + 'scale': 1.0, + 'viewport': false, + 'render_mode': '' + }); -// Create the public API interface -function constructor() { - Util.Debug(">> Display.constructor"); + Util.Debug(">> Display.constructor"); - var c, func, i, curDat, curSave, - has_imageData = false, UE = Util.Engine; + if (!this._target) { + throw new Error("Target must be set"); + } - if (! conf.target) { throw("target must be set"); } + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } - if (typeof conf.target === 'string') { - throw("target must be a DOM element"); - } + if (!this._target.getContext) { + throw new Error("no getContext method"); + } - c = conf.target; + if (!this._drawCtx) { + this._drawCtx = this._target.getContext('2d'); + } - if (! c.getContext) { throw("no getContext method"); } + 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); } - if (! c_ctx) { c_ctx = c.getContext('2d'); } + this.clear(); - 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); } + // Check canvas features + if ('createImageData' in this._drawCtx) { + this._render_mode = 'canvas rendering'; + } else { + throw new Error("Canvas does not support createImageData"); + } - that.clear(); + if (this._prefer_js === null) { + Util.Info("Prefering javascript operations"); + this._prefer_js = true; + } - // 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; - } + // 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; + } - // Initialize cached tile imageData - tile16x16 = c_ctx.createImageData(16, 16); + Util.Debug("<< Display.constructor"); + }; - /* - * 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; + 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; } - Util.Info("Data URI scheme cursor supported"); - } else { - if (conf.cursor_uri === null) { - conf.cursor_uri = false; + + 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); + } } - 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; + var vx2 = vp.x + vp.w - 1; + var vy2 = vp.y + vp.h - 1; + // Position change - 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); - } + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; + } - c.width = v.w; - c.height = v.h; + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); + } - if (saveImg) { - c_ctx.putImageData(saveImg, 0, 0); - } - } - - vx2 = v.x + v.w - 1; - vy2 = v.y + v.h - 1; - - - // Position change - - if ((deltaX < 0) && ((v.x + deltaX) < 0)) { - deltaX = - v.x; - } - if ((vx2 + deltaX) >= fb_width) { - deltaX -= ((vx2 + deltaX) - fb_width + 1); - } - - if ((v.y + deltaY) < 0) { - deltaY = - v.y; - } - if ((vy2 + deltaY) >= fb_height) { - deltaY -= ((vy2 + deltaY) - fb_height + 1); - } - - 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); + 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 { - // We need to wait for this image to 'load' - // to keep things in-order - ready = false; + bgr = this._colourMap[color[0]]; } - break; - } - if (ready) { - a = renderQ.shift(); - } - } - if (renderQ.length > 0) { - requestAnimFrame(scan_renderQ); - } -}; + 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 + }, -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; - } + 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); + } + }, - 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); - } -}; + 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(); + } + }, -that.defaultCursor = function() { - conf.target.style.cursor = "default"; -}; + changeCursor: function (pixels, mask, hotx, hoty, w, h) { + if (this._cursor_uri === false) { + Util.Warn("changeCursor called but no cursor data URI support"); + return; + } -return constructor(); // Return the public API interface + 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; + } + } -} // End of Display() + 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; + } -/* 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 ); - }; + 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)'; + }, - 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 + _setFillColor: function (color) { + var bgr; + if (this._true_color) { + bgr = color; } 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 + 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 + ]); + + // 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 { + w = h; // increase w to make it square + } + + var cur = []; + + // 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); + }; + + 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 + + // 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); + // 42: Size of Image + 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[]) + 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 { - 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/include/input.js b/include/input.js index 392b410..5d9e209 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; + +(function () { + "use strict"; + + // + // Keyboard event handler + // + + Keyboard = function (defaults) { + this._keyDownList = []; // List of depressed keys + // (even if they are happy) + + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true + }); + + // create the keyboard handler + this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(), + VerifyCharModifier( /* jshint newcap: false */ + TrackKeyState( + EscapeModifiers(this._handleRfbEvent.bind(this)) + ) + ) + ); /* jshint newcap: true */ + + // 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 + + _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'); + } + }, -// -// Keyboard event handler -// + _handleKeyDown: function (e) { + if (!this._focused) { return true; } -function Keyboard(defaults) { -"use strict"; + 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; + } + }, -var that = {}, // Public API methods - conf = {}, // Configuration attributes + _handleKeyPress: function (e) { + if (!this._focused) { return true; } - keyDownList = []; // List of depressed keys - // (even if they are happy) + 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; + } + }, -// 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'], + _handleKeyUp: function (e) { + if (!this._focused) { return true; } - ['onKeyPress', 'rw', 'func', null, 'Handler for key press/release'] - ]); + 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(); -// -// 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)'] + //Util.Debug(">> Keyboard.ungrab"); + }, + + sync: function (e) { + this._handler.syncModifiers(e); + } + }; + + Util.make_properties(Keyboard, [ + ['target', 'wo', 'dom'], // DOM element that captures keyboard input + ['focused', 'rw', 'bool'], // Capture and send key events + + ['onKeyPress', 'rw', 'func'] // Handler for key press/release ]); -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); + // + // Mouse event handler + // + + Mouse = function (defaults) { + this._mouseCaptured = false; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + + // Configuration attributes + Util.set_defaults(this, defaults, { + 'target': document, + 'focused': true, + 'scale': 1.0, + 'touchButton': 1 + }); + + 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) + }; + }; + + Mouse.prototype = { + // private methods + _captureMouse: function () { + // capturing the mouse ensures we get the mouseup event + if (this._target.setCapture) { + this._target.setCapture(); + } + + // some browsers give us mouseup events regardless, + // so if we never captured the mouse, we can disregard the event + this._mouseCaptured = true; + }, + + _releaseMouse: function () { + if (this._target.releaseCapture) { + this._target.releaseCapture(); + } + this._mouseCaptured = false; + }, + + _resetDoubleClickTimer: function () { + this._doubleClickTimer = null; + }, - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. + _handleMouseButton: function (e, down) { + if (!this._focused) { return true; } - var xs = lastTouchPos.x - pos.x; - var ys = lastTouchPos.y - pos.y; - var d = Math.sqrt((xs * xs) + (ys * ys)); + if (this._notify) { + this._notify(e); + } - // 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; + var evt = (e ? e : window.event); + var pos = Util.getEventPosition(e, this._target, this._scale); + + var bmask; + 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 (this._doubleClickTimer === null) { + this._lastTouchPos = pos; + } else { + clearTimeout(this._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 = this._lastTouchPos.x - pos.x; + var ys = this._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 = 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 } - doubleClickTimer = setTimeout(resetDoubleClickTimer, 500); + + 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); + } - 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); -} - -function onMouseUp(e) { - if (!mouseCaptured) { - return; - } - - onMouseButton(e, 0); - releaseMouse(); -} - -function onMouseWheel(e) { - var evt, pos, bmask, wheelData; - if (! conf.focused) { - return true; - } - if (conf.notify) { - conf.notify(e); - } - - 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; -} - -function onMouseMove(e) { - var evt, pos; - if (! conf.focused) { - return true; - } - if (conf.notify) { - conf.notify(e); - } - - 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; -} - -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; -} - -// -// Public API interface functions -// - -that.grab = function() { - //Util.Debug(">> Mouse.grab"); - var c = conf.target; - - 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); - } - - /* Work around right and middle click browser behaviors */ - Util.addEvent(document, 'click', onMouseDisable); - Util.addEvent(document.body, 'contextmenu', onMouseDisable); - - //Util.Debug("<< Mouse.grab"); -}; - -that.ungrab = function() { - //Util.Debug(">> Mouse.ungrab"); - var c = conf.target; - - 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); - } - - /* Work around right and middle click browser behaviors */ - Util.removeEvent(document, 'click', onMouseDisable); - Util.removeEvent(document.body, 'contextmenu', onMouseDisable); - - //Util.Debug(">> Mouse.ungrab"); -}; - -return that; // Return the public API interface - -} // End of Mouse() + }; + + 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) + ]); +})(); diff --git a/include/keyboard.js b/include/keyboard.js index c89411c..6044321 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/include/rfb.js b/include/rfb.js index 3b748ba..9e96f71 100644 --- a/include/rfb.js +++ b/include/rfb.js @@ -10,1972 +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'] - ]); +(function () { + "use strict"; + RFB = function (defaults) { + if (!defaults) { + defaults = {}; + } + this._rfb_host = ''; + this._rfb_port = 5900; + this._rfb_password = ''; + this._rfb_path = ''; + + this._rfb_state = 'disconnected'; + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_auth_scheme = ''; + + this._rfb_tightvnc = false; + this._rfb_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 + }; -// 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 += ")"; + 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 (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); + + try { + this._display = new Display({target: this._target}); + } catch (exc) { + Util.Error("Display exception: " + exc); + this._updateState('fatal', "No working Display"); } - }); - 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 <a href='http://get.adobe.com/flashplayer'>Adobe Flash<\/a> is required"); - } else if (document.location.href.substr(0, 7) === "file://") { - updateState('fatal', - "'file://' URL is incompatible with Adobe Flash"); + + this._keyboard = new Keyboard({target: this._focusContainer, + onKeyPress: this._handleKeyPress.bind(this)}); + + this._mouse = new Mouse({target: this._target, + onMouseButton: this._handleMouseButton.bind(this), + onMouseMove: this._handleMouseMove.bind(this), + notify: this._keyboard.sync.bind(this._keyboard)}); + + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', function () { + if (this._rfb_state === 'connect') { + this._updateState('ProtocolVersion', "Starting VNC handshake"); + } else { + this._fail("Got unexpected WebSocket connection"); + } + }.bind(this)); + this._sock.on('close', function (e) { + Util.Warn("WebSocket on-close event"); + var msg = ""; + if (e.code) { + msg = " (code: " + e.code; + if (e.reason) { + msg += ", reason: " + e.reason; + } + msg += ")"; + } + if (this._rfb_state === 'disconnect') { + this._updateState('disconnected', 'VNC disconnected' + msg); + } else if (this._rfb_state === 'ProtocolVersion') { + this._fail('Failed to connect to server' + msg); + } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) { + Util.Error("Received onclose while disconnected" + msg); + } else { + this._fail("Server disconnected" + msg); + } + }.bind(this)); + this._sock.on('error', function (e) { + Util.Warn("WebSocket on-error event"); + }); + + this._init_vars(); + + var rmode = this._display.get_render_mode(); + if (Websock_native) { + Util.Info("Using native WebSockets"); + this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); } else { - 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 = []; - - // 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"); + Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); + if (!Util.Flash || Util.Flash.version < 9) { + this._updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> 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); + } } - } -}; -// -// Utility routines -// + Util.Debug("<< RFB.constructor"); + }; + RFB.prototype = { + // Public methods + connect: function (host, port, password, path) { + this._rfb_host = host; + this._rfb_port = port; + this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_path = (path !== undefined) ? path : ""; -/* - * 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; - } + if (!this._rfb_host || !this._rfb_port) { + return this._fail("Must set host and port"); + } - if (msgTimer) { - clearTimeout(msgTimer); - msgTimer = null; - } + 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); + }, - 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(); + 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'; } - } - ws.close(); - } - - if (oldstate === 'fatal') { - Util.Error("Fatal error, cannot continue"); - } - - 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'"); - } + uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path; + Util.Info("connecting to " + uri); - break; + 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"); + } + } - case 'connect': + 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"); + } + }, - init_vars(); - connect(); - // WebSocket.onopen transitions to 'ProtocolVersion' - break; + /* + * 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}) { - case 'disconnect': + if (this._sendTimer) { + clearInterval(this._sendTimer); + this._sendTimer = null; + } - if (! test_mode) { - disconnTimer = setTimeout(function () { - fail("Disconnect timeout"); - }, conf.disconnectTimeout * 1000); - } + if (this._msgTimer) { + clearInterval(this._msgTimer); + this._msgTimer = null; + } - print_stats(); + 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(); + } + } - // WebSocket.onclose transitions to 'disconnected' - break; + this._sock.close(); + } + if (oldstate === 'fatal') { + Util.Error('Fatal error, cannot continue'); + } - 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."); - } + 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); + } - // 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); + if (oldstate === 'failed' && state === 'disconnected') { + // do disconnect action, but stay in failed state + this._rfb_state = 'failed'; } else { - Util.Debug("More data to process, existing timer"); + this._rfb_state = state; } - } - break; - default: - init_msg(); - break; - } -}; - - -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; - } else { - viewportDragging = false; - } - } - if (conf.view_only) { return; } // View only, skip mouse events + if (this._disconnTimer && this._rfb_state !== 'disconnect') { + Util.Debug("Clearing disconnect timer"); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + } - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y)) ); - ws.send(mouse_arr); - mouse_arr = []; -}; + 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."); + } -mouseMove = function(x, y) { - //Util.Debug('>> mouseMove ' + x + "," + y); - var deltaX, deltaY; + // Make sure we transition to disconnected + setTimeout(function () { + this._updateState('disconnected'); + }.bind(this), 50); - 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}; + break; - display.viewportChange(deltaX, deltaY); + default: + // No state change action to take + } - // Skip sending mouse events - return; - } + if (oldstate === 'failed' && state === 'disconnected') { + this._onUpdateState(this, state, oldstate); + } else { + this._onUpdateState(this, state, oldstate, statusMsg); + } + }, - if (conf.view_only) { return; } // View only, skip mouse events + _fail: function (msg) { + this._updateState('failed', msg); + return false; + }, - mouse_arr = mouse_arr.concat( - pointerEvent(display.absX(x), display.absY(y))); - - checkEvents(); -}; + _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; + } + }, -// -// Server message handlers -// + _checkEvents: function () { + if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) { + this._sock.send(this._mouse_arr); + this._mouse_arr = []; + } + }, -// RFB/VNC initialisation message handler -init_msg = function() { - //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']"); + _handleKeyPress: function (keysym, down) { + if (this._view_only) { return; } // View only, skip keyboard, events + this._sock.send(RFB.messages.keyEvent(keysym, down)); + }, - 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; + _handleMouseButton: function (x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMaks ^= bmask; + } - //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0)); - switch (rfb_state) { + if (this._viewportDrag) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = {'x': x, 'y': y}; - 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"; + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + } } - 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); - } + 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 = []; + }, - cversion = "00" + parseInt(rfb_version,10) + - ".00" + ((rfb_version * 10) % 10); - ws.send_string("RFB " + cversion + "\n"); - updateState('Security', "Sent ProtocolVersion: " + cversion); - break; - - 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); - } - 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]; + _handleMouseMove: function (x, y) { + if (this._viewportDragging) { + var deltaX = this._viewportDragPos.x - x; + var deltaY = this._viewportDragPos.y - y; + this._viewportDragPos = {'x': x, 'y': y}; + + this._display.viewportChange(deltaX, deltaY); + + // Skip sending mouse events + return; + } + + if (this._view_only) { return; } // View only, skip mouse events + + this._mouse_arr = this._mouse_arr.concat( + RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask)); + + this._checkEvents(); + }, + + // Message Handlers + + _negotiate_protocol_version: function () { + if (this._sock.rQlen() < 12) { + return this._fail("Incomplete protocol version"); + } + + var sversion = this._sock.rQshiftStr(12).substr(4, 7); + Util.Info("Server ProtocolVersion: " + sversion); + var is_repeater = 0; + switch (sversion) { + case "000.000": // UltraVNC repeater + is_repeater = 1; + break; + case "003.003": + case "003.006": // UltraVNC + case "003.889": // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case "003.007": + this._rfb_version = 3.7; + break; + case "003.008": + case "004.000": // Intel AMT KVM + case "004.001": // RealVNC 4.6 + this._rfb_version = 3.8; + break; + default: + return this._fail("Invalid server version " + sversion); + } + + if (is_repeater) { + var repeaterID = this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += "\0"; } + this._sock.send_string(repeaterID); + return true; } - if (rfb_auth_scheme === 0) { - return fail("Unsupported security types: " + types); + + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; } - - 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'); - return; + + // 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); } - // 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; + + 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); } - xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + + + 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 (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; } - if (numTunnels != 0) - { - fail("Protocol requested tunnels, not currently supported. numTunnels: " + numTunnels); - return; + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue } + } - var clientSupportedTypes = { - 'STDVNOAUTH__': 1, - 'STDVVNCAUTH_': 2 - }; + // 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; } - var serverSupportedTypes = []; + var clientSupportedTypes = { + 'STDVNOAUTH__': 1, + 'STDVVNCAUTH_': 2 + }; - if (ws.rQwait("sub auth count", 4)) { return false; } - var subAuthCount = ws.rQshift32(); - //console.log("Sub auth count: "+subAuthCount); - for (var i=0;i<subAuthCount;i++) - { + var serverSupportedTypes = []; - if (ws.rQwait("sub auth capabilities "+i, 16)) { return false; } - var capNum = ws.rQshift32(); - var capabilities = ws.rQshiftStr(12); - //console.log("queue: "+ws.rQlen()); - //console.log("auth type: "+capNum+": "+capabilities); + for (var i = 0; i < subAuthCount; i++) { + var capNum = this._sock.rQshift32(); + var capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } - 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); + } } + } - for (var authType in clientSupportedTypes) - { - if (serverSupportedTypes.indexOf(authType) != -1) - { - //console.log("selected authType "+authType); - ws.send([0,0,0,clientSupportedTypes[authType]]); - - 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; - } + this._fail("No supported sub-auth types!"); + }, + + _negotiate_authentication: function () { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + if (this._sock.rQwait("auth reason", 4)) { return false; } + var strlen = this._sock.rQshift32(); + var reason = this._sock.rQshiftStr(strlen); + return this._fail("Auth failure: " + reason); + + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._updateState('SecurityResult'); + return true; } + this._updateState('ClientInitialisation', "No auth required"); + return this._init_msg(); + + case 22: // XVP auth + return this._negotiate_xvp_auth(); + + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); + + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); + + default: + return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme); + } + }, + + _handle_security_result: function () { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } + switch (this._sock.rQshift32()) { + case 0: // OK + this._updateState('ClientInitialisation', 'Authentication OK'); + return this._init_msg(); + case 1: // failed + if (this._rfb_version >= 3.8) { + var length = this._sock.rQshift32(); + if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } + var reason = this._sock.rQshiftStr(length); + return this._fail(reason); + } else { + return this._fail("Authentication failure"); + } + return false; + case 2: + return this._fail("Too many auth attempts"); + } + }, + + _negotiate_server_init: function () { + if (this._sock.rQwait("server initialization", 24)) { return false; } + + /* 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); + } - return; - default: - fail("Unsupported auth scheme: " + rfb_auth_scheme); - return; - } - updateState('ClientInitialisation', "No auth required"); - init_msg(); // Recursive fallthrough (workaround JSLint complaint) - break; - - 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)) { + 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; - - case 'ServerInitialisation' : - if (ws.rQwait("server initialization", 24)) { return false; } - - /* Screen size */ - fb_width = ws.rQshift16(); - fb_height = ws.rQshift16(); - - /* 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; - } + this._timing.last_fbu = (new Date()).getTime(); + + ret = this._encHandlers[this._FBU.encoding](); + + now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); - if (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<numServerMessages;i++) - { - var srvMsg = ws.rQshiftStr(16); - //console.log("server message: "+srvMsg); - } - for (var i=0;i<numClientMessages;i++) - { - var clientMsg = ws.rQshiftStr(16); - //console.log("client message: "+clientMsg); - } - for (var i=0;i<numEncodings;i++) - { - var encoding = ws.rQshiftStr(16); - //console.log("encoding: "+encoding); + if (ret) { + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || + this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Util.Info("Timing of full FBU, curr: " + + this._timing.cur_fbu + ", total: " + + this._timing.full_fbu_total + ", cnt: " + + this._timing.full_fbu_cnt + ", avg: " + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); + } + + if (this._timing.fbu_rt_start > 0) { + var fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Util.Info("full FBU round-trip, cur: " + + fbu_rt_diff + ", total: " + + this._timing.fbu_rt_total + ", cnt: " + + this._timing.fbu_rt_cnt + ", avg: " + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; + } + } + + if (!ret) { return ret; } // need more data } - } - display.set_true_color(conf.true_color); - conf.onFBResize(that, fb_width, fb_height); - display.resize(fb_width, fb_height); - keyboard.grab(); - mouse.grab(); + 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]}); - if (conf.true_color) { - fb_Bpp = 4; - fb_depth = 3; - } else { - fb_Bpp = 1; - fb_depth = 1; - } + return true; // We finished this FBU + }, + }; - response = pixelFormat(); - response = response.concat(clientEncodings()); - response = response.concat(fbUpdateRequests()); // initial fbu-request - timing.fbu_rt_start = (new Date()).getTime(); - timing.pixels = 0; - ws.send(response); - - checkEvents(); - - if (conf.encrypt) { - updateState('normal', "Connected (encrypted) to: " + fb_name); - } else { - updateState('normal', "Connected (unencrypted) to: " + fb_name); - } - break; - } - //Util.Debug("<< init_msg"); -}; - - -/* Normal RFB/VNC server message handler */ -normal_msg = function() { - //Util.Debug(">> 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)); - } - } + 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 + ]); - 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); - */ + RFB.prototype.set_local_cursor = function (cursor) { + if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { + this._local_cursor = false; + } else { + if (this._display.get_cursor_uri()) { + this._local_cursor = true; } else { - fail("Disconnected: unsupported encoding " + - FBU.encoding); - return false; + Util.Warn("Browser does not support local cursor"); } } + }; - timing.last_fbu = (new Date()).getTime(); + RFB.prototype.get_display = function () { return this._display; }; + RFB.prototype.get_keyboard = function () { return this._keyboard; }; + RFB.prototype.get_mouse = function () { return this._mouse; }; + + // Class Methods + RFB.messages = { + keyEvent: function (keysym, down) { + var arr = [4]; + arr.push8(down); + arr.push16(0); + arr.push32(keysym); + return arr; + }, + + pointerEvent: function (x, y, mask) { + var arr = [5]; // msg-type + arr.push8(mask); + arr.push16(x); + arr.push16(y); + return arr; + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText: function (text) { + var arr = [6]; // msg-type + arr.push8(0); // padding + arr.push8(0); // padding + arr.push8(0); // padding + arr.push32(text.length); + var n = text.length; + for (var i = 0; i < n; i++) { + arr.push(text.charCodeAt(i)); + } - ret = encHandlers[FBU.encoding](); + 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]); + } + } - now = (new Date()).getTime(); - timing.cur_fbu += (now - timing.last_fbu); + var arr = [2]; // msg-type + arr.push8(0); // padding - if (ret) { - encStats[FBU.encoding][0] += 1; - encStats[FBU.encoding][1] += 1; - timing.pixels += FBU.width * FBU.height; - } + 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 = []; - 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; + 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; } - 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; + }; + + RFB.genDES = function (password, challenge) { + var passwd = []; + for (var i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); } - 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; - } + return (new DES(passwd)).encrypt(challenge); + }; + + RFB.extract_data_uri = function (arr) { + return ";base64," + Base64.encode(arr); + }; + + RFB.encodingHandlers = { + RAW: function () { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; } - } - /* - 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"); + 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 { - display.fillRect(x, y, w, h, FBU.background); + this._FBU.rects--; + this._FBU.bytes = 0; } - } 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; + + 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 { - color = FBU.foreground; + 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; } - xy = rQ[rQi]; - rQi += 1; - sx = (xy >> 4); - sy = (xy & 0x0f); - wh = rQ[rQi]; - rQi += 1; - sw = (wh >> 4) + 1; - sh = (wh & 0x0f) + 1; + 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--; + } - display.subTile(sx, sy, sw, sh, color); + 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; } } - 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]; -}; + return [header, data]; + }, -function display_tight(isTightPNG) { - //Util.Debug(">> display_tight"); + display_tight: function (isTightPNG) { + if (this._fb_depth === 1) { + this._fail("Tight protocol handler only implements true color mode"); + } - if (fb_depth === 1) { - 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 ctl, cmode, clength, color, img, data; - var filterId = -1, resetStreams = 0, streamId = -1; - var rQ = ws.get_rQ(), rQi = ws.get_rQi(); + 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); + } + } - FBU.bytes = 1; // compression-control byte - if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; } + var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + if (uncompressed.status !== 0) { + Util.Error("Invalid data in zlib stream"); + } - var checksum = function(data) { - var sum=0, i; - for (i=0; i<data.length;i++) { - sum += data[i]; - if (sum > 65536) sum -= 65536; - } - return sum; - } + 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]; + } + } - 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]; + 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]; + } } } - 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]; + + 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)); } - } - } 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]; + + 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"); } - } - return dest; - }; - 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]; - } else { - clength = getTightCLength(ws.rQslice(3 + paletteSize, - 3 + paletteSize + 3)); - } - 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]); + 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 (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); - } + 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; + } - // 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)); - } - FBU.bytes = 1 + clength[0] + clength[1]; - if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; } - // Shift ctl, clength off - ws.rQshiftBytes(1 + clength[0]); + this._FBU.bytes = 0; + this._FBU.rects--; - if (raw) { - data = ws.rQshiftBytes(clength[1]); - } else { - data = decompress(ws.rQshiftBytes(clength[1])); - } + return true; + }, - 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"); -}; + TIGHT: function () { return this._encHandlers.display_tight(false); }, + TIGHT_PNG: function () { return this._encHandlers.display_tight(true); }, -/* - * Client message routines - */ + last_rect: function () { + this._FBU.rects = 0; + return true; + }, -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<encodings.length; i += 1) { - if ((encodings[i][0] === "Cursor") && - (! conf.local_cursor)) { - Util.Debug("Skipping Cursor pseudo-encoding"); - - // TODO: remove this when we have tight+non-true-color - } else if ((encodings[i][0] === "TIGHT") && - (! conf.true_color)) { - Util.Warn("Skipping tight, only support with true color"); - } else { - //Util.Debug("Adding encoding: " + encodings[i][0]); - encList.push(encodings[i][1]); - } - } - - arr = [2]; // msg-type - arr.push8(0); // padding - - arr.push16(encList.length); // encoding count - for (i=0; i < encList.length; i += 1) { - arr.push32(encList[i]); - } - //Util.Debug("<< clientEncodings: " + arr); - return arr; -}; - -fbUpdateRequest = function(incremental, x, y, xw, yw) { - //Util.Debug(">> 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"); - }; -}; + 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; } -return constructor(); // Return the public API interface + this._display.changeCursor(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); -} // End of RFB() + 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"); + } + }; +})(); diff --git a/include/ui.js b/include/ui.js index f6afccc..e869aa6 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 */ - -// 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 = { - -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, - -// 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); -}, - -// Render default UI and initialize settings menu -start: function(callback) { - var html = '', i, sheet, sheets, llevels, port, autoconnect; - - UI.isTouchDevice = 'ontouchstart' in document.documentElement; - - // 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); - } - - // 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) { +/* jslint white: false, browser: true */ +/* global window, $D, Util, WebUtil, RFB, Display */ + +var UI; + +(function () { + "use strict"; + + // 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 = { + + 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, + + // 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); + }, + + // Render default UI and initialize settings menu + start: function(callback) { + UI.isTouchDevice = 'ontouchstart' in document.documentElement; + + // 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); + } + + // 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]); + } + + // 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 + 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 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 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 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(); + } + }, + + // 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; + }, + + // 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"); + }, + + + + 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(); - } - } - - // 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; -}, + return false; + }, -// Update cookie and form control setting. If value is not set, then -// updates from control to current cookie setting. -updateSetting: function(name, value) { + sendCtrlAltDel: function() { + UI.rfb.sendCtrlAltDel(); + }, - var i, ctrl = $D('noVNC_' + name); - // Save the cookie for this session - if (typeof value !== 'undefined') { - WebUtil.writeSetting(name, value); - } + xvpShutdown: function() { + UI.rfb.xvpShutdown(); + }, - // Update the settings control - value = UI.getSetting(name); + xvpReboot: function() { + UI.rfb.xvpReboot(); + }, - if (ctrl.type === 'checkbox') { - ctrl.checked = value; + xvpReset: function() { + UI.rfb.xvpReset(); + }, - } 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; + setMouseButton: function(num) { + if (typeof num === 'undefined') { + // Disable mouse buttons + num = -1; } - } - } 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) { - var val; - - // 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; -}, - -// 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 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 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 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(); - } -}, - -// 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; -}, - -// 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"); -}, - - - -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(); -}, - -xvpShutdown: function() { - UI.rfb.xvpShutdown(); -}, - -xvpReboot: function() { - UI.rfb.xvpReboot(); -}, - -xvpReset: function() { - UI.rfb.xvpReset(); -}, - -setMouseButton: function(num) { - var b, blist = [0, 1,2,4], button; - - if (typeof num === 'undefined') { - // Disable mouse buttons - num = -1; - } - if (UI.rfb) { - UI.rfb.get_mouse().set_touchButton(num); - } - - 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) { - 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': + 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"; + } + } + }, + + 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(); + + $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(); - - 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() { - 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(); - }; -}, + 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"); + } -setResize: function () { - window.onResize = function() { - UI.setBarPosition(); - }; -}, + 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')); -//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); -}, + UI.rfb.connect(host, port, password, path); -setBarPosition: function() { - $D('noVNC-control-bar').style.top = (window.pageYOffset) + 'px'; - $D('noVNC_mobile_buttons').style.left = (window.pageXOffset) + 'px'; + //Close dialog. + setTimeout(UI.setBarPosition, 100); + $D('noVNC_logo').style.display = "none"; + }, - var vncwidth = $D('noVNC_screen').style.offsetWidth; - $D('noVNC-control-bar').style.width = vncwidth + 'px'; -} + 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; + 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'; + } + + }; +})(); diff --git a/include/util.js b/include/util.js index 05c1ac3..c145d5a 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,88 +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(); - - 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); - } +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); + } + } - 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; - - 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; - }; +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; + } + + 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 +//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 * ------------------------------------------------------ @@ -112,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 { @@ -122,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; @@ -149,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; + var getter; + if (type === 'arr') { + getter = function (idx) { + if (typeof idx !== 'undefined') { + return this['_' + name][idx]; + } else { + return this['_' + name]; + } + }; + } else { + getter = function () { + return this['_' + name]; + }; + } - // Default getter function - getter = function (idx) { - if ((type in {'arr':1, 'array':1}) && - (typeof idx !== 'undefined')) { - return cfg[v][idx]; + 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 cfg[v]; + return function (val, idx) { + if (typeof idx !== 'undefined') { + this['_' + name][idx] = val; + } else { + this['_' + name] = val; + } + }; } }; - // 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 setter; + if (type === 'bool') { + setter = make_setter(function (val) { + if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) { + return false; } else { - val = true; + return true; } - } else if (type in {'integer':1, 'int':1}) { - val = parseInt(val, 10); - } else if (type === 'str') { - val = String(val); - } else if (type === 'func') { + }); + } 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) { - val = function () {}; + return function () {}; + } else { + return val; } - } - if (typeof idx !== 'undefined') { - cfg[v][idx] = val; - } else { - cfg[v] = val; - } - }; - - // Set the description - api[v + '_description'] = desc; + }); + } else if (type === 'arr' || type === 'dom' || type == 'raw') { + setter = make_setter(); + } else { + throw new Error('Unknown property type ' + type); // some sanity checking + } - // Set the getter function - if (typeof api['get_' + v] === 'undefined') { - api['get_' + v] = getter; + // set the getter + if (typeof proto['get_' + name] === 'undefined') { + proto['get_' + name] = 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); - }; + // 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); + }; + } } - // Set the default value - if (typeof defaults[v] !== 'undefined') { - defval = defaults[v]; - } else if ((type in {'arr':1, 'array':1}) && - (! (defval instanceof Array))) { - defval = []; + // 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 + }; +}; + +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]); } - // Coerce existing setting to the right type - //Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]); - setter(defval); }; -// Set group of configuration defaults -Util.conf_defaults = function(cfg, api, defaults, arr) { +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)); -} +}; @@ -250,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<files.length; f++) { + + 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(); + } + } + } + }; + + 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 @@ -300,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; @@ -342,7 +469,7 @@ Util.getPosition = (function() { } return [myScrollLeft, myScrollTop]; - }; + } return function (obj) { var curleft = 0, curtop = 0, scr = obj, fixed = false; @@ -362,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}; }; @@ -371,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); @@ -390,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; } @@ -433,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; }()), - - '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); -} +(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); + var detectTrident = function () { + if (!window.ActiveXObject) { + return false; + } else { + if (window.XMLHttpRequest) { + return (document.querySelectorAll) ? 6 : 5; + } else { + return 4; + } + } + }; + + // '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(){ +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/include/websock.js b/include/websock.js index 0e4718a..1b89a91 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,342 @@ 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"; - -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 - - eventHandlers = { - 'message' : function() {}, - 'open' : function() {}, - 'close' : function() {}, - 'error' : function() {} - }, - - test_mode = false; - - -// -// Queue public functions -// - -function get_sQ() { - return sQ; + "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.maxBufferedAmount = 200; + + 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; + }, + + get_rQ: function () { + return this._rQ; + }, + + get_rQi: function () { + return this._rQi; + }, + + set_rQi: function (val) { + this._rQi = val; + }, + + // Receive Queue + rQlen: function () { + return this._rQ.length - this._rQi; + }, + + rQpeek8: function () { + return this._rQ[this._rQi]; + }, + + rQshift8: function () { + 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); + } else { + this._rQi--; + this._rQ[this._rQi] = num; + } + }, + + rQshift16: function () { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + rQshift32: function () { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + }, + + 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); + }, + + rQslice: function (start, end) { + if (end) { + return this._rQ.slice(this._rQi + start, this._rQi + end); + } else { + return this._rQ.slice(this._rQi + start); + } + }, + + // 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; + }, -function rQlen() { - return rQ.length - rQi; -} + // Send Queue -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; - } + flush: function () { + if (this._websocket.bufferedAmount !== 0) { + Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); + } -} -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); -} + if (this._websocket.bufferedAmount < this.maxBufferedAmount) { + if (this._sQ.length > 0) { + this._websocket.send(this._encode_message()); + this._sQ = []; + } -function rQslice(start, end) { - if (end) { - return rQ.slice(rQi + start, rQi + end); - } else { - return rQ.slice(rQi + start); - } -} + return true; + } else { + Util.Info("Delaying send, bufferedAmount: " + + this._websocket.bufferedAmount); + return false; + } + }, + + send: function (arr) { + this._sQ = this._sQ.concat(arr); + return this.flush(); + }, + + 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 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"); + // 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 } - rQi -= goback; - } - //Util.Debug(" waiting for " + (num-rQlen) + - // " " + msg + " byte(s)"); - return true; // true means need more data - } - return false; -} -// -// Private utility routines -// - -function encode_message() { - if (mode === 'binary') { - // Put in a binary arraybuffer - return (new Uint8Array(sQ)).buffer; - } else { - // base64 encode - return Base64.encode(sQ); - } -} + // Default protocols if not specified + if (typeof(protocols) === "undefined") { + if (wsbt) { + protocols = ['binary', 'base64']; + } else { + protocols = 'base64'; + } + } -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); -} + 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."); + } + } + } -// -// 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; - } -} + return protocols; + }, -// overridable for testing -function send(arr) { - //Util.Debug(">> send_array: " + arr); - sQ = sQ.concat(arr); - return flush(); -} + open: function (uri, protocols) { + var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; + protocols = this.init(protocols, ws_schema); -function send_string(str) { - //Util.Debug(">> send_string: " + str); - api.send(str.split('').map( - function (chr) { return chr.charCodeAt(0); } ) ); -} + this._websocket = new WebSocket(uri, protocols); -// -// 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; + if (protocols.indexOf('binary') >= 0) { + this._websocket.binaryType = 'arraybuffer'; } - } 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"); -} - -// Set event handlers -function on(evt, handler) { - eventHandlers[evt] = handler; -} - -function init(protocols, ws_schema) { - rQ = []; - rQi = 0; - sQ = []; - websocket = null; - - var bt = false, - wsbt = false, - try_binary = false; - - // 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 - } - - // Default protocols if not specified - if (typeof(protocols) === "undefined") { - if (wsbt) { - protocols = ['binary', 'base64']; - } else { - protocols = 'base64'; - } - } - - // 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"); + 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 { - new_protocols.push(protocols[i]); + 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; }; } - if (new_protocols.length > 0) { - protocols = new_protocols; + }, + + // 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"; + } - return protocols; -} + if (typeof exc.description !== 'undefined') { + exception_str += " description: " + exc.description + "\n"; + } -function open(uri, protocols) { - var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; - protocols = init(protocols, ws_schema); + if (typeof exc.stack !== 'undefined') { + exception_str += exc.stack; + } - 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"); - }; -} + if (exception_str.length > 0) { + Util.Error("recv_message, caught exception: " + exception_str); + } else { + Util.Error("recv_message, caught exception: " + exc); + } -function close() { - if (websocket) { - if ((websocket.readyState === WebSocket.OPEN) || - (websocket.readyState === WebSocket.CONNECTING)) { - Util.Info("Closing WebSocket connection"); - websocket.close(); + if (typeof exc.name !== 'undefined') { + this._eventHandlers.error(exc.name + ": " + exc.message); + } else { + this._eventHandlers.error(exc); + } + } } - 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/include/webutil.js b/include/webutil.js index e95fc80..e674bf9 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= ""; } - - // Print the properties of the passed-in object - for (i in obj) { - if ((depth > 1) && (typeof obj[i] === "object")) { + "use strict"; + if (! depth) { depth = 2; } + if (! parent) { parent = ""; } + + // 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/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..fca5970 --- /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 0000000..3f1b29b --- /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 <github@martintribe.org> (https://github.com/kanaka)", + "contributors": [ + "Solly Ross <sross@redhat.com> (https://github.com/directxman12)", + "Peter Åstrand <astrand@cendio.se> (https://github.com/astrand)", + "Samuel Mannehed <samuel@cendio.se> (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/fake.websocket.js b/tests/fake.websocket.js new file mode 100644 index 0000000..749c0ea --- /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/run_from_console.casper.js b/tests/run_from_console.casper.js index 7cb4b7c..57ed2be 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 0d4cc8f..bfdd1b6 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: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, 'node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>", + header: "<html>\n<head>\n<meta charset='utf-8' />\n<link rel='stylesheet' href='" + path.resolve(__dirname, '../node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>", script_tag: function(p) { return "<script src='" + p + "'></script>"; }, footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run(function () { window.__mocha_done = true; });\n</script>\n</body>\n</html>" }; - 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<script>mocha.setup('bdd');</script>"; diff --git a/tests/test.base64.js b/tests/test.base64.js new file mode 100644 index 0000000..b2646a0 --- /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); + }); + }); +}); diff --git a/tests/test.display.js b/tests/test.display.js new file mode 100644 index 0000000..c4535a0 --- /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); + }); + }); +}); diff --git a/tests/test.helper.js b/tests/test.helper.js index d9e8e14..98009d2 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 80d1fee..2ac65af 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) { diff --git a/tests/test.rfb.js b/tests/test.rfb.js new file mode 100644 index 0000000..595548e --- /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 + }); + }); +}); diff --git a/tests/test.util.js b/tests/test.util.js new file mode 100644 index 0000000..7d6524a --- /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 +}); diff --git a/tests/test.websock.js b/tests/test.websock.js new file mode 100644 index 0000000..7d242d3 --- /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]); + }); + + }); + + }); +}); |