Merge pull request #7 from ColinHuang/master

upgrade noVNC to 0.5.1
This commit is contained in:
DoroWu 2015-03-02 16:47:09 +08:00
commit c225ef1f40
36 changed files with 9030 additions and 5468 deletions

5
noVNC/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.pyc
*.o
tests/data_*.js
utils/rebind.so
node_modules

3
noVNC/.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "include/web-socket-js-project"]
path = include/web-socket-js-project
url = https://github.com/gimite/web-socket-js.git

16
noVNC/.travis.yml Normal file
View file

@ -0,0 +1,16 @@
language: node_js
node_js:
- '0.11.13'
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
global:
- secure: QE5GqGd2hrpQsIgd8dlv3oRUUHqZayomzzQjNXOB81VQi241uz/ru+3GtBZLB5WLZCq/Gj89vbLnR0LN4ixlmPaWv3/WJQGyDGuRD/vMnccVl+rBUP/Hh2zdYwiISIGcrywNAE+KLus/lyt/ahVgzbaRaDSzrM1HaZFT/rndGck=
- secure: g75sdctEwj0hoLW0Y08Tdv8s5scNzplB6a9EtaJ2vJD9S/bK+AsPqbWesGv1UlrFPCWdbV7Vg61vkmoUjcmb5xhqFIjcM9TlYJoKWeOTsOmnQoSIkIq6gMF1k02+LmKInbPgIzrp3m3jluS1qaOs/EzFpDnJp9hWBiAfXa12Jxk=
before_script: npm install -g karma-cli

54
noVNC/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,54 @@
How to contribute to noVNC
==========================
We accept code via pull requests on GitHub. There are several guidelines that
we expect contributors submitting code requests to follow. If you have issues
following any of these guidelines, feel free to drop us a line by leaving a
comment in the code request or sending us an email.
Contributing Guidelines
-----------------------
* While we don't have an official coding style guide, please try to follow
the general coding style of the existing code.
** Use four spaces instead of tabs
** prefix private variables and functions with an `_`
* Please try to include unit tests for your code. For instance, if you
introduce a new encoding, add a test to `tests/test.rfb.js` under the
"Encoding Handlers" section (basically, input a small pattern in your
encoding and make sure the pattern gets displayed correctly). If you
fix a bug, try to add a unit test that would have caught that bug
(if possible -- some bugs, especially visual ones, are hard to test for).
* Squash your commits down in to a clean commit history. For instance, there
should not be "cleanup" commits where you fix issues in previous commits in
the same pull request. Before you go to commit, use `git rebase -i` to
squash these changes into the relevant commits. For instance, a good commit
history might look like "Added support for FOO encoding, Added support for
BAR message, Placed Button in UI to Trigger BAR" (where each comma denotes
a separate commit).
* Add both a title and description to your commit, if possible. Place more
detail on what you did in the description.
Running the unit tests
----------------------
There are two ways to run the unit tests. For both ways, you should first run
`npm install` (not as root).
The first way to run the tests is to run `npm test`. This will run all the
tests in the headless PhantomJS browser (which uses WebKit).
The second way to run the tests is using the `tests/run_from_console.js` file.
This way is a bit more flexible, and can provide more information about what
went wrong. To run all the tests, simply run `tests/run_from_console.js`.
To run a specific test file, you can use the `-t path/to/test/file.js` option.
If you wish to simply generate the HTML for the test, use the `-g` option, and
the path to the temporary HTML file will be written to standard out. To open
this file in your default browser automatically, pass the `-o` option as well.
More information can be found by passing the `--help` or `-h` option.
Thanks, and happy coding!

View file

@ -1,5 +1,6 @@
## noVNC: HTML5 VNC Client ## noVNC: HTML5 VNC Client
[![Build Status](https://travis-ci.org/kanaka/noVNC.svg?branch=master)](https://travis-ci.org/kanaka/noVNC)
### Description ### Description
@ -28,7 +29,7 @@ discussion group</a>
Bugs and feature requests can be submitted via [github Bugs and feature requests can be submitted via [github
issues](https://github.com/kanaka/noVNC/issues). If you are looking issues](https://github.com/kanaka/noVNC/issues). If you are looking
for a place to start contributing to noVNC, a good place to start for a place to start contributing to noVNC, a good place to start
would be the issues that are have marked as would be the issues that are marked as
["patchwelcome"](https://github.com/kanaka/noVNC/issues?labels=patchwelcome). ["patchwelcome"](https://github.com/kanaka/noVNC/issues?labels=patchwelcome).
If you want to show appreciation for noVNC you could donate to a great If you want to show appreciation for noVNC you could donate to a great

View file

@ -4,11 +4,10 @@
// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js // 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 */ /*global console */
var Base64 = { var Base64 = {
/* Convert data (an array of integers) to a Base64 string. */ /* Convert data (an array of integers) to a Base64 string. */
toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
base64Pad : '=', base64Pad : '=',
@ -17,20 +16,19 @@ encode: function (data) {
"use strict"; "use strict";
var result = ''; var result = '';
var toBase64Table = Base64.toBase64Table; var toBase64Table = Base64.toBase64Table;
var length = data.length var length = data.length;
var lengthpad = (length % 3); var lengthpad = (length % 3);
var i = 0, j = 0;
// Convert every three bytes to 4 ascii characters. // Convert every three bytes to 4 ascii characters.
/* BEGIN LOOP */
for (i = 0; i < (length - 2); i += 3) { for (var i = 0; i < (length - 2); i += 3) {
result += toBase64Table[data[i] >> 2]; result += toBase64Table[data[i] >> 2];
result += toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; 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 + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
result += toBase64Table[data[i + 2] & 0x3f]; result += toBase64Table[data[i + 2] & 0x3f];
} }
/* END LOOP */
// Convert the remaining 1 or 2 bytes, pad out to 4 characters. // Convert the remaining 1 or 2 bytes, pad out to 4 characters.
var j = 0;
if (lengthpad === 2) { if (lengthpad === 2) {
j = length - lengthpad; j = length - lengthpad;
result += toBase64Table[data[j] >> 2]; result += toBase64Table[data[j] >> 2];
@ -49,6 +47,7 @@ encode: function (data) {
}, },
/* Convert Base64 data to a string */ /* Convert Base64 data to a string */
/* jshint -W013 */
toBinaryTable : [ 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,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
@ -59,13 +58,14 @@ toBinaryTable : [
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, -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 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
], ],
/* jshint +W013 */
decode: function (data, offset) { decode: function (data, offset) {
"use strict"; "use strict";
offset = typeof(offset) !== 'undefined' ? offset : 0; offset = typeof(offset) !== 'undefined' ? offset : 0;
var toBinaryTable = Base64.toBinaryTable; var toBinaryTable = Base64.toBinaryTable;
var base64Pad = Base64.base64Pad; var base64Pad = Base64.base64Pad;
var result, result_length, idx, i, c, padding; var result, result_length;
var leftbits = 0; // number of bits decoded, but yet to be appended var leftbits = 0; // number of bits decoded, but yet to be appended
var leftdata = 0; // bits decoded, but yet to be appended var leftdata = 0; // bits decoded, but yet to be appended
var data_length = data.indexOf('=') - offset; var data_length = data.indexOf('=') - offset;
@ -77,10 +77,9 @@ decode: function (data, offset) {
result = new Array(result_length); result = new Array(result_length);
// Convert one by one. // Convert one by one.
/* BEGIN LOOP */ for (var idx = 0, i = offset; i < data.length; i++) {
for (idx = 0, i = offset; i < data.length; i++) { var c = toBinaryTable[data.charCodeAt(i) & 0x7f];
c = toBinaryTable[data.charCodeAt(i) & 0x7f]; var padding = (data.charAt(i) === base64Pad);
padding = (data.charAt(i) === base64Pad);
// Skip illegal characters and whitespace // Skip illegal characters and whitespace
if (c === -1) { if (c === -1) {
console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i); console.error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
@ -101,15 +100,14 @@ decode: function (data, offset) {
leftdata &= (1 << leftbits) - 1; leftdata &= (1 << leftbits) - 1;
} }
} }
/* END LOOP */
// If there are any bits left, the base64 string was corrupted // If there are any bits left, the base64 string was corrupted
if (leftbits) { if (leftbits) {
throw {name: 'Base64-Error', err = new Error('Corrupted base64 string');
message: 'Corrupted base64 string'}; err.name = 'Base64-Error';
throw err;
} }
return result; return result;
} }
}; /* End of Base64 namespace */ }; /* End of Base64 namespace */

View file

@ -75,12 +75,13 @@
* fine Java utilities: http://www.acme.com/java/ * fine Java utilities: http://www.acme.com/java/
*/ */
"use strict"; /* jslint white: false */
/*jslint white: false, bitwise: false, plusplus: false */
function DES(passwd) { function DES(passwd) {
"use strict";
// Tables, permutations, S-boxes, etc. // 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, 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, 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 ], 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
@ -88,6 +89,7 @@ var PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8, z = 0x0, a,b,c,d,e,f, SP1,SP2,SP3,SP4,SP5,SP6,SP7,SP8,
keys = []; keys = [];
// jshint -W015
a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; 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, 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, 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,
@ -128,6 +130,7 @@ 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, 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, 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]; 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. // Set the key.
function setKeys(keyBlock) { function setKeys(keyBlock) {

File diff suppressed because it is too large Load diff

View file

@ -5,60 +5,60 @@
* Licensed under MPL 2.0 or any later version (see LICENSE.txt) * 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 */ /*global window, Util */
var Keyboard, Mouse;
(function () {
"use strict";
// //
// Keyboard event handler // Keyboard event handler
// //
function Keyboard(defaults) { Keyboard = function (defaults) {
"use strict"; this._keyDownList = []; // List of depressed keys
var that = {}, // Public API methods
conf = {}, // Configuration attributes
keyDownList = []; // List of depressed keys
// (even if they are happy) // (even if they are happy)
// Configuration attributes Util.set_defaults(this, defaults, {
Util.conf_defaults(conf, that, defaults, [ 'target': document,
['target', 'wo', 'dom', document, 'DOM element that captures keyboard input'], 'focused': true
['focused', 'rw', 'bool', true, 'Capture and send key events'], });
['onKeyPress', 'rw', 'func', null, 'Handler for key press/release']
]);
//
// 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 // create the keyboard handler
var k = KeyEventDecoder(kbdUtil.ModifierSync(), this._handler = new KeyEventDecoder(kbdUtil.ModifierSync(),
VerifyCharModifier( VerifyCharModifier( /* jshint newcap: false */
TrackKeyState( TrackKeyState(
EscapeModifiers(onRfbEvent) EscapeModifiers(this._handleRfbEvent.bind(this))
) )
) )
); ); /* jshint newcap: true */
function onKeyDown(e) { // keep these here so we can refer to them later
if (! conf.focused) { this._eventHandlers = {
return true; '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');
} }
if (k.keydown(e)) { },
_handleKeyDown: function (e) {
if (!this._focused) { return true; }
if (this._handler.keydown(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
@ -67,12 +67,12 @@ function onKeyDown(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onKeyPress(e) {
if (! conf.focused) { _handleKeyPress: function (e) {
return true; if (!this._focused) { return true; }
}
if (k.keypress(e)) { if (this._handler.keypress(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
@ -81,13 +81,12 @@ function onKeyPress(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onKeyUp(e) { _handleKeyUp: function (e) {
if (! conf.focused) { if (!this._focused) { return true; }
return true;
} if (this._handler.keyup(e)) {
if (k.keyup(e)) {
// Suppress bubbling/default actions // Suppress bubbling/default actions
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
@ -96,153 +95,147 @@ function onKeyUp(e) {
// will have the character code translated // will have the character code translated
return true; return true;
} }
} },
function onOther(e) { _allKeysUp: function () {
k.syncModifiers(e);
}
function allKeysUp() {
Util.Debug(">> Keyboard.allKeysUp"); Util.Debug(">> Keyboard.allKeysUp");
this._handler.releaseAll();
k.releaseAll();
Util.Debug("<< Keyboard.allKeysUp"); Util.Debug("<< Keyboard.allKeysUp");
} },
// // Public methods
// Public API interface functions
//
that.grab = function() { grab: function () {
//Util.Debug(">> Keyboard.grab"); //Util.Debug(">> Keyboard.grab");
var c = conf.target; var c = this._target;
Util.addEvent(c, 'keydown', onKeyDown); Util.addEvent(c, 'keydown', this._eventHandlers.keydown);
Util.addEvent(c, 'keyup', onKeyUp); Util.addEvent(c, 'keyup', this._eventHandlers.keyup);
Util.addEvent(c, 'keypress', onKeyPress); Util.addEvent(c, 'keypress', this._eventHandlers.keypress);
// Release (key up) if window loses focus // Release (key up) if window loses focus
Util.addEvent(window, 'blur', allKeysUp); Util.addEvent(window, 'blur', this._eventHandlers.blur);
//Util.Debug("<< Keyboard.grab"); //Util.Debug("<< Keyboard.grab");
}; },
that.ungrab = function() { ungrab: function () {
//Util.Debug(">> Keyboard.ungrab"); //Util.Debug(">> Keyboard.ungrab");
var c = conf.target; var c = this._target;
Util.removeEvent(c, 'keydown', onKeyDown); Util.removeEvent(c, 'keydown', this._eventHandlers.keydown);
Util.removeEvent(c, 'keyup', onKeyUp); Util.removeEvent(c, 'keyup', this._eventHandlers.keyup);
Util.removeEvent(c, 'keypress', onKeyPress); Util.removeEvent(c, 'keypress', this._eventHandlers.keypress);
Util.removeEvent(window, 'blur', allKeysUp); Util.removeEvent(window, 'blur', this._eventHandlers.blur);
// Release (key up) all keys that are in a down state // Release (key up) all keys that are in a down state
allKeysUp(); this._allKeysUp();
//Util.Debug(">> Keyboard.ungrab"); //Util.Debug(">> Keyboard.ungrab");
},
sync: function (e) {
this._handler.syncModifiers(e);
}
}; };
that.sync = function(e) { Util.make_properties(Keyboard, [
k.syncModifiers(e); ['target', 'wo', 'dom'], // DOM element that captures keyboard input
} ['focused', 'rw', 'bool'], // Capture and send key events
return that; // Return the public API interface
} // End of Keyboard()
['onKeyPress', 'rw', 'func'] // Handler for key press/release
]);
// //
// Mouse event handler // Mouse event handler
// //
function Mouse(defaults) { Mouse = function (defaults) {
"use strict"; this._mouseCaptured = false;
var that = {}, // Public API methods this._doubleClickTimer = null;
conf = {}, // Configuration attributes this._lastTouchPos = null;
mouseCaptured = false;
var doubleClickTimer = null,
lastTouchPos = null;
// Configuration attributes // Configuration attributes
Util.conf_defaults(conf, that, defaults, [ Util.set_defaults(this, defaults, {
['target', 'ro', 'dom', document, 'DOM element that captures mouse input'], 'target': document,
['notify', 'ro', 'func', null, 'Function to call to notify whenever a mouse event is received'], 'focused': true,
['focused', 'rw', 'bool', true, 'Capture and send mouse clicks/movement'], 'scale': 1.0,
['scale', 'rw', 'float', 1.0, 'Viewport scale factor 0.0 - 1.0'], 'touchButton': 1
});
['onMouseButton', 'rw', 'func', null, 'Handler for mouse button click/release'], this._eventHandlers = {
['onMouseMove', 'rw', 'func', null, 'Handler for mouse movement'], 'mousedown': this._handleMouseDown.bind(this),
['touchButton', 'rw', 'int', 1, 'Button mask (1, 2, 4) for touch devices (0 means ignore clicks)'] 'mouseup': this._handleMouseUp.bind(this),
]); 'mousemove': this._handleMouseMove.bind(this),
'mousewheel': this._handleMouseWheel.bind(this),
'mousedisable': this._handleMouseDisable.bind(this)
};
};
function captureMouse() { Mouse.prototype = {
// private methods
_captureMouse: function () {
// capturing the mouse ensures we get the mouseup event // capturing the mouse ensures we get the mouseup event
if (conf.target.setCapture) { if (this._target.setCapture) {
conf.target.setCapture(); this._target.setCapture();
} }
// some browsers give us mouseup events regardless, // some browsers give us mouseup events regardless,
// so if we never captured the mouse, we can disregard the event // so if we never captured the mouse, we can disregard the event
mouseCaptured = true; this._mouseCaptured = true;
},
_releaseMouse: function () {
if (this._target.releaseCapture) {
this._target.releaseCapture();
}
this._mouseCaptured = false;
},
_resetDoubleClickTimer: function () {
this._doubleClickTimer = null;
},
_handleMouseButton: function (e, down) {
if (!this._focused) { return true; }
if (this._notify) {
this._notify(e);
} }
function releaseMouse() { var evt = (e ? e : window.event);
if (conf.target.releaseCapture) { var pos = Util.getEventPosition(e, this._target, this._scale);
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);
var bmask;
if (e.touches || e.changedTouches) { if (e.touches || e.changedTouches) {
// Touch device // Touch device
// When two touches occur within 500 ms of each other and are // When two touches occur within 500 ms of each other and are
// closer than 20 pixels together a double click is triggered. // closer than 20 pixels together a double click is triggered.
if (down == 1) { if (down == 1) {
if (doubleClickTimer == null) { if (this._doubleClickTimer === null) {
lastTouchPos = pos; this._lastTouchPos = pos;
} else { } else {
clearTimeout(doubleClickTimer); clearTimeout(this._doubleClickTimer);
// When the distance between the two touches is small enough // When the distance between the two touches is small enough
// force the position of the latter touch to the position of // force the position of the latter touch to the position of
// the first. // the first.
var xs = lastTouchPos.x - pos.x; var xs = this._lastTouchPos.x - pos.x;
var ys = lastTouchPos.y - pos.y; var ys = this._lastTouchPos.y - pos.y;
var d = Math.sqrt((xs * xs) + (ys * ys)); var d = Math.sqrt((xs * xs) + (ys * ys));
// The goal is to trigger on a certain physical width, the // The goal is to trigger on a certain physical width, the
// devicePixelRatio brings us a bit closer but is not optimal. // devicePixelRatio brings us a bit closer but is not optimal.
if (d < 20 * window.devicePixelRatio) { if (d < 20 * window.devicePixelRatio) {
pos = lastTouchPos; pos = this._lastTouchPos;
} }
} }
doubleClickTimer = setTimeout(resetDoubleClickTimer, 500); this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
} }
bmask = conf.touchButton; bmask = this._touchButton;
// If bmask is set // If bmask is set
} else if (evt.which) { } else if (evt.which) {
/* everything except IE */ /* everything except IE */
@ -253,149 +246,143 @@ function onMouseButton(e, down) {
(evt.button & 0x2) * 2 + // Right (evt.button & 0x2) * 2 + // Right
(evt.button & 0x4) / 2; // Middle (evt.button & 0x4) / 2; // Middle
} }
//Util.Debug("mouse " + pos.x + "," + pos.y + " down: " + down +
// " bmask: " + bmask + "(evt.button: " + evt.button + ")"); if (this._onMouseButton) {
if (conf.onMouseButton) {
Util.Debug("onMouseButton " + (down ? "down" : "up") + Util.Debug("onMouseButton " + (down ? "down" : "up") +
", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
conf.onMouseButton(pos.x, pos.y, down, bmask); this._onMouseButton(pos.x, pos.y, down, bmask);
} }
Util.stopEvent(e); Util.stopEvent(e);
return false; 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);
} }
function onMouseDown(e) { var evt = (e ? e : window.event);
captureMouse(); var pos = Util.getEventPosition(e, this._target, this._scale);
onMouseButton(e, 1); var wheelData = evt.detail ? evt.detail * -1 : evt.wheelDelta / 40;
} var bmask;
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) { if (wheelData > 0) {
bmask = 1 << 3; bmask = 1 << 3;
} else { } else {
bmask = 1 << 4; bmask = 1 << 4;
} }
//Util.Debug('mouse scroll by ' + wheelData + ':' + pos.x + "," + pos.y);
if (conf.onMouseButton) { if (this._onMouseButton) {
conf.onMouseButton(pos.x, pos.y, 1, bmask); this._onMouseButton(pos.x, pos.y, 1, bmask);
conf.onMouseButton(pos.x, pos.y, 0, bmask); this._onMouseButton(pos.x, pos.y, 0, bmask);
} }
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
},
_handleMouseMove: function (e) {
if (! this._focused) { return true; }
if (this._notify) {
this._notify(e);
} }
function onMouseMove(e) { var evt = (e ? e : window.event);
var evt, pos; var pos = Util.getEventPosition(e, this._target, this._scale);
if (! conf.focused) { if (this._onMouseMove) {
return true; this._onMouseMove(pos.x, pos.y);
}
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); Util.stopEvent(e);
return false; 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);
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 */ /* Stop propagation if inside canvas area */
if ((pos.realx >= 0) && (pos.realy >= 0) && if ((pos.realx >= 0) && (pos.realy >= 0) &&
(pos.realx < conf.target.offsetWidth) && (pos.realx < this._target.offsetWidth) &&
(pos.realy < conf.target.offsetHeight)) { (pos.realy < this._target.offsetHeight)) {
//Util.Debug("mouse event disabled"); //Util.Debug("mouse event disabled");
Util.stopEvent(e); Util.stopEvent(e);
return false; return false;
} }
//Util.Debug("mouse event not disabled");
return true; return true;
} },
//
// Public API interface functions
//
that.grab = function() { // Public methods
//Util.Debug(">> Mouse.grab"); grab: function () {
var c = conf.target; var c = this._target;
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in document.documentElement) {
Util.addEvent(c, 'touchstart', onMouseDown); Util.addEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.addEvent(window, 'touchend', onMouseUp); Util.addEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchend', onMouseUp); Util.addEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.addEvent(c, 'touchmove', onMouseMove); Util.addEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else { } else {
Util.addEvent(c, 'mousedown', onMouseDown); Util.addEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.addEvent(window, 'mouseup', onMouseUp); Util.addEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mouseup', onMouseUp); Util.addEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.addEvent(c, 'mousemove', onMouseMove); Util.addEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', Util.addEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel); this._eventHandlers.mousewheel);
} }
/* Work around right and middle click browser behaviors */ /* Work around right and middle click browser behaviors */
Util.addEvent(document, 'click', onMouseDisable); Util.addEvent(document, 'click', this._eventHandlers.mousedisable);
Util.addEvent(document.body, 'contextmenu', onMouseDisable); Util.addEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
},
//Util.Debug("<< Mouse.grab"); ungrab: function () {
}; var c = this._target;
that.ungrab = function() {
//Util.Debug(">> Mouse.ungrab");
var c = conf.target;
if ('ontouchstart' in document.documentElement) { if ('ontouchstart' in document.documentElement) {
Util.removeEvent(c, 'touchstart', onMouseDown); Util.removeEvent(c, 'touchstart', this._eventHandlers.mousedown);
Util.removeEvent(window, 'touchend', onMouseUp); Util.removeEvent(window, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchend', onMouseUp); Util.removeEvent(c, 'touchend', this._eventHandlers.mouseup);
Util.removeEvent(c, 'touchmove', onMouseMove); Util.removeEvent(c, 'touchmove', this._eventHandlers.mousemove);
} else { } else {
Util.removeEvent(c, 'mousedown', onMouseDown); Util.removeEvent(c, 'mousedown', this._eventHandlers.mousedown);
Util.removeEvent(window, 'mouseup', onMouseUp); Util.removeEvent(window, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mouseup', onMouseUp); Util.removeEvent(c, 'mouseup', this._eventHandlers.mouseup);
Util.removeEvent(c, 'mousemove', onMouseMove); Util.removeEvent(c, 'mousemove', this._eventHandlers.mousemove);
Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel', Util.removeEvent(c, (Util.Engine.gecko) ? 'DOMMouseScroll' : 'mousewheel',
onMouseWheel); this._eventHandlers.mousewheel);
} }
/* Work around right and middle click browser behaviors */ /* Work around right and middle click browser behaviors */
Util.removeEvent(document, 'click', onMouseDisable); Util.removeEvent(document, 'click', this._eventHandlers.mousedisable);
Util.removeEvent(document.body, 'contextmenu', onMouseDisable); Util.removeEvent(document.body, 'contextmenu', this._eventHandlers.mousedisable);
//Util.Debug(">> Mouse.ungrab"); }
}; };
return that; // Return the public API interface 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
} // End of Mouse() ['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)
]);
})();

View file

@ -15,23 +15,23 @@ var kbdUtil = (function() {
var sub = substitutions[cp]; var sub = substitutions[cp];
return sub ? sub : cp; return sub ? sub : cp;
}; }
function isMac() { function isMac() {
return navigator && !!(/macintosh/i).exec(navigator.appVersion); return navigator && !!(/mac/i).exec(navigator.platform);
} }
function isWindows() { function isWindows() {
return navigator && !!(/windows/i).exec(navigator.appVersion); return navigator && !!(/win/i).exec(navigator.platform);
} }
function isLinux() { function isLinux() {
return navigator && !!(/linux/i).exec(navigator.appVersion); return navigator && !!(/linux/i).exec(navigator.platform);
} }
// Return true if a modifier which is not the specified char modifier (and is not shift) is down // Return true if a modifier which is not the specified char modifier (and is not shift) is down
function hasShortcutModifier(charModifier, currentModifiers) { function hasShortcutModifier(charModifier, currentModifiers) {
var mods = {}; var mods = {};
for (var key in currentModifiers) { for (var key in currentModifiers) {
if (parseInt(key) !== 0xffe1) { if (parseInt(key) !== XK_Shift_L) {
mods[key] = currentModifiers[key]; mods[key] = currentModifiers[key];
} }
} }
@ -65,24 +65,18 @@ var kbdUtil = (function() {
// Helper object tracking modifier key state // Helper object tracking modifier key state
// and generates fake key events to compensate if it gets out of sync // and generates fake key events to compensate if it gets out of sync
function ModifierSync(charModifier) { function ModifierSync(charModifier) {
var ctrl = 0xffe3;
var alt = 0xffe9;
var altGr = 0xfe03;
var shift = 0xffe1;
var meta = 0xffe7;
if (!charModifier) { if (!charModifier) {
if (isMac()) { if (isMac()) {
// on Mac, Option (AKA Alt) is used as a char modifier // on Mac, Option (AKA Alt) is used as a char modifier
charModifier = [alt]; charModifier = [XK_Alt_L];
} }
else if (isWindows()) { else if (isWindows()) {
// on Windows, Ctrl+Alt is used as a char modifier // on Windows, Ctrl+Alt is used as a char modifier
charModifier = [alt, ctrl]; charModifier = [XK_Alt_L, XK_Control_L];
} }
else if (isLinux()) { else if (isLinux()) {
// on Linux, AltGr is used as a char modifier // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
charModifier = [altGr]; charModifier = [XK_ISO_Level3_Shift];
} }
else { else {
charModifier = []; charModifier = [];
@ -90,11 +84,11 @@ var kbdUtil = (function() {
} }
var state = {}; var state = {};
state[ctrl] = false; state[XK_Control_L] = false;
state[alt] = false; state[XK_Alt_L] = false;
state[altGr] = false; state[XK_ISO_Level3_Shift] = false;
state[shift] = false; state[XK_Shift_L] = false;
state[meta] = false; state[XK_Meta_L] = false;
function sync(evt, keysym) { function sync(evt, keysym) {
var result = []; var result = [];
@ -102,25 +96,30 @@ var kbdUtil = (function() {
return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
} }
if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { if (evt.ctrlKey !== undefined &&
state[ctrl] = evt.ctrlKey; evt.ctrlKey !== state[XK_Control_L] && keysym !== XK_Control_L) {
result.push(syncKey(ctrl)); state[XK_Control_L] = evt.ctrlKey;
result.push(syncKey(XK_Control_L));
} }
if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { if (evt.altKey !== undefined &&
state[alt] = evt.altKey; evt.altKey !== state[XK_Alt_L] && keysym !== XK_Alt_L) {
result.push(syncKey(alt)); state[XK_Alt_L] = evt.altKey;
result.push(syncKey(XK_Alt_L));
} }
if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { if (evt.altGraphKey !== undefined &&
state[altGr] = evt.altGraphKey; evt.altGraphKey !== state[XK_ISO_Level3_Shift] && keysym !== XK_ISO_Level3_Shift) {
result.push(syncKey(altGr)); state[XK_ISO_Level3_Shift] = evt.altGraphKey;
result.push(syncKey(XK_ISO_Level3_Shift));
} }
if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { if (evt.shiftKey !== undefined &&
state[shift] = evt.shiftKey; evt.shiftKey !== state[XK_Shift_L] && keysym !== XK_Shift_L) {
result.push(syncKey(shift)); state[XK_Shift_L] = evt.shiftKey;
result.push(syncKey(XK_Shift_L));
} }
if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { if (evt.metaKey !== undefined &&
state[meta] = evt.metaKey; evt.metaKey !== state[XK_Meta_L] && keysym !== XK_Meta_L) {
result.push(syncKey(meta)); state[XK_Meta_L] = evt.metaKey;
result.push(syncKey(XK_Meta_L));
} }
return result; return result;
} }
@ -152,11 +151,14 @@ var kbdUtil = (function() {
// Get a key ID from a keyboard event // Get a key ID from a keyboard event
// May be a string or an integer depending on the available properties // May be a string or an integer depending on the available properties
function getKey(evt){ function getKey(evt){
if (evt.key) { if ('keyCode' in evt && 'key' in evt) {
return evt.key; return evt.key + ':' + evt.keyCode;
}
else if ('keyCode' in evt) {
return evt.keyCode;
} }
else { else {
return evt.keyCode; return evt.key;
} }
} }
@ -170,7 +172,10 @@ var kbdUtil = (function() {
else if (evt.charCode) { else if (evt.charCode) {
codepoint = evt.charCode; codepoint = evt.charCode;
} }
else if (evt.keyCode && evt.type === 'keypress') {
// IE10 stores the char code as keyCode, and has no other useful properties
codepoint = evt.keyCode;
}
if (codepoint) { if (codepoint) {
var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
if (res) { if (res) {
@ -205,21 +210,21 @@ var kbdUtil = (function() {
return shiftPressed ? keycode : keycode + 32; // A-Z return shiftPressed ? keycode : keycode + 32; // A-Z
} }
if (keycode >= 0x60 && keycode <= 0x69) { if (keycode >= 0x60 && keycode <= 0x69) {
return 0xffb0 + (keycode - 0x60); // numpad 0-9 return XK_KP_0 + (keycode - 0x60); // numpad 0-9
} }
switch(keycode) { switch(keycode) {
case 0x20: return 0x20; // space case 0x20: return XK_space;
case 0x6a: return 0xffaa; // multiply case 0x6a: return XK_KP_Multiply;
case 0x6b: return 0xffab; // add case 0x6b: return XK_KP_Add;
case 0x6c: return 0xffac; // separator case 0x6c: return XK_KP_Separator;
case 0x6d: return 0xffad; // subtract case 0x6d: return XK_KP_Subtract;
case 0x6e: return 0xffae; // decimal case 0x6e: return XK_KP_Decimal;
case 0x6f: return 0xffaf; // divide case 0x6f: return XK_KP_Divide;
case 0xbb: return 0x2b; // + case 0xbb: return XK_plus;
case 0xbc: return 0x2c; // , case 0xbc: return XK_comma;
case 0xbd: return 0x2d; // - case 0xbd: return XK_minus;
case 0xbe: return 0x2e; // . case 0xbe: return XK_period;
} }
return nonCharacterKey({keyCode: keycode}); return nonCharacterKey({keyCode: keycode});
@ -233,37 +238,38 @@ var kbdUtil = (function() {
var keycode = evt.keyCode; var keycode = evt.keyCode;
if (keycode >= 0x70 && keycode <= 0x87) { if (keycode >= 0x70 && keycode <= 0x87) {
return 0xffbe + keycode - 0x70; // F1-F24 return XK_F1 + keycode - 0x70; // F1-F24
} }
switch (keycode) { switch (keycode) {
case 8 : return 0xFF08; // BACKSPACE case 8 : return XK_BackSpace;
case 13 : return 0xFF0D; // ENTER case 13 : return XK_Return;
case 9 : return 0xFF09; // TAB case 9 : return XK_Tab;
case 27 : return 0xFF1B; // ESCAPE case 27 : return XK_Escape;
case 46 : return 0xFFFF; // DELETE case 46 : return XK_Delete;
case 36 : return 0xFF50; // HOME case 36 : return XK_Home;
case 35 : return 0xFF57; // END case 35 : return XK_End;
case 33 : return 0xFF55; // PAGE_UP case 33 : return XK_Page_Up;
case 34 : return 0xFF56; // PAGE_DOWN case 34 : return XK_Page_Down;
case 45 : return 0xFF63; // INSERT case 45 : return XK_Insert;
case 37 : return 0xFF51; // LEFT case 37 : return XK_Left;
case 38 : return 0xFF52; // UP case 38 : return XK_Up;
case 39 : return 0xFF53; // RIGHT case 39 : return XK_Right;
case 40 : return 0xFF54; // DOWN case 40 : return XK_Down;
case 16 : return 0xFFE1; // SHIFT
case 17 : return 0xFFE3; // CONTROL
case 18 : return 0xFFE9; // Left ALT (Mac Option)
case 224 : return 0xFE07; // Meta case 16 : return XK_Shift_L;
case 225 : return 0xFE03; // AltGr case 17 : return XK_Control_L;
case 91 : return 0xFFEC; // Super_L (Win Key) case 18 : return XK_Alt_L; // also: Option-key on Mac
case 92 : return 0xFFED; // Super_R (Win Key)
case 93 : return 0xFF67; // Menu (Win Menu), Mac Command case 224 : return XK_Meta_L;
case 225 : return XK_ISO_Level3_Shift; // AltGr
case 91 : return XK_Super_L; // also: Windows-key
case 92 : return XK_Super_R; // also: Windows-key
case 93 : return XK_Menu; // also: Windows-Menu, Command on Mac
default: return null; default: return null;
} }
} }
@ -381,17 +387,22 @@ function VerifyCharModifier(next) {
if (timer) { if (timer) {
return; return;
} }
var delayProcess = function () {
clearTimeout(timer);
timer = null;
process();
};
while (queue.length !== 0) { while (queue.length !== 0) {
var cur = queue[0]; var cur = queue[0];
queue = queue.splice(1); queue = queue.splice(1);
switch (cur.type) { switch (cur.type) {
case 'stall': case 'stall':
// insert a delay before processing available events. // insert a delay before processing available events.
timer = setTimeout(function() { /* jshint loopfunc: true */
clearTimeout(timer); timer = setTimeout(delayProcess, 5);
timer = null; /* jshint loopfunc: false */
process();
}, 5);
return; return;
case 'keydown': case 'keydown':
// is the next element a keypress? Then we should merge the two // is the next element a keypress? Then we should merge the two
@ -483,23 +494,25 @@ function TrackKeyState(next) {
var item = state.splice(idx, 1)[0]; var item = state.splice(idx, 1)[0];
// for each keysym tracked by this key entry, clone the current event and override the keysym // for each keysym tracked by this key entry, clone the current event and override the keysym
for (var key in item.keysyms) {
var clone = (function(){ var clone = (function(){
function Clone(){} function Clone(){}
return function (obj) { Clone.prototype=obj; return new Clone(); }; return function (obj) { Clone.prototype=obj; return new Clone(); };
}()); }());
for (var key in item.keysyms) {
var out = clone(evt); var out = clone(evt);
out.keysym = item.keysyms[key]; out.keysym = item.keysyms[key];
next(out); next(out);
} }
break; break;
case 'releaseall': case 'releaseall':
/* jshint shadow: true */
for (var i = 0; i < state.length; ++i) { for (var i = 0; i < state.length; ++i) {
for (var key in state[i].keysyms) { for (var key in state[i].keysyms) {
var keysym = state[i].keysyms[key]; var keysym = state[i].keysyms[key];
next({keyId: 0, keysym: keysym, type: 'keyup'}); next({keyId: 0, keysym: keysym, type: 'keyup'});
} }
} }
/* jshint shadow: false */
state = []; state = [];
} }
}; };
@ -521,8 +534,10 @@ function EscapeModifiers(next) {
// send the character event // send the character event
next(evt); next(evt);
// redo modifiers // redo modifiers
/* jshint shadow: true */
for (var i = 0; i < evt.escape.length; ++i) { for (var i = 0; i < evt.escape.length; ++i) {
next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
} }
/* jshint shadow: false */
}; };
} }

View file

@ -170,6 +170,8 @@ XK_Super_R = 0xffec, /* Right super */
XK_Hyper_L = 0xffed, /* Left hyper */ XK_Hyper_L = 0xffed, /* Left hyper */
XK_Hyper_R = 0xffee, /* Right hyper */ XK_Hyper_R = 0xffee, /* Right hyper */
XK_ISO_Level3_Shift = 0xfe03, /* AltGr */
/* /*
* Latin 1 * Latin 1
* (ISO/IEC 8859-1 = Unicode U+0020..U+00FF) * (ISO/IEC 8859-1 = Unicode U+0020..U+00FF)

File diff suppressed because it is too large Load diff

View file

@ -7,17 +7,22 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict";
/* jslint white: false, browser: true */ /* jslint white: false, browser: true */
/* global window, $D, Util, WebUtil, RFB, Display */ /* global window, $D, Util, WebUtil, RFB, Display */
var UI;
(function () {
"use strict";
// Load supporting scripts // Load supporting scripts
window.onscriptsload = function () { UI.load(); }; window.onscriptsload = function () { UI.load(); };
window.onload = function () { UI.keyboardinputReset(); };
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "keyboard.js", "input.js", "display.js",
"jsunzip.js", "rfb.js", "keysym.js"]); "jsunzip.js", "rfb.js", "keysym.js"]);
var UI = { UI = {
rfb_state : 'loaded', rfb_state : 'loaded',
settingsOpen : false, settingsOpen : false,
@ -26,6 +31,8 @@ popupStatusOpen : false,
clipboardOpen: false, clipboardOpen: false,
keyboardVisible: false, keyboardVisible: false,
hideKeyboardTimeout: null, hideKeyboardTimeout: null,
lastKeyboardinput: null,
defaultKeyboardinputLen: 100,
extraKeysVisible: false, extraKeysVisible: false,
ctrlOn: false, ctrlOn: false,
altOn: false, altOn: false,
@ -39,19 +46,18 @@ load: function (callback) {
// Render default UI and initialize settings menu // Render default UI and initialize settings menu
start: function(callback) { start: function(callback) {
var html = '', i, sheet, sheets, llevels, port, autoconnect;
UI.isTouchDevice = 'ontouchstart' in document.documentElement; UI.isTouchDevice = 'ontouchstart' in document.documentElement;
// Stylesheet selection dropdown // Stylesheet selection dropdown
sheet = WebUtil.selectStylesheet(); var sheet = WebUtil.selectStylesheet();
sheets = WebUtil.getStylesheets(); var sheets = WebUtil.getStylesheets();
var i;
for (i = 0; i < sheets.length; i += 1) { for (i = 0; i < sheets.length; i += 1) {
UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title); UI.addOption($D('noVNC_stylesheet'),sheets[i].title, sheets[i].title);
} }
// Logging selection dropdown // Logging selection dropdown
llevels = ['error', 'warn', 'info', 'debug']; var llevels = ['error', 'warn', 'info', 'debug'];
for (i = 0; i < llevels.length; i += 1) { for (i = 0; i < llevels.length; i += 1) {
UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]); UI.addOption($D('noVNC_logging'),llevels[i], llevels[i]);
} }
@ -67,7 +73,7 @@ start: function(callback) {
// if port == 80 (or 443) then it won't be present and should be // if port == 80 (or 443) then it won't be present and should be
// set manually // set manually
port = window.location.port; var port = window.location.port;
if (!port) { if (!port) {
if (window.location.protocol.substring(0,5) == 'https') { if (window.location.protocol.substring(0,5) == 'https') {
port = 443; port = 443;
@ -89,13 +95,13 @@ start: function(callback) {
UI.initSetting('path', 'websockify'); UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', ''); UI.initSetting('repeaterID', '');
UI.rfb = RFB({'target': $D('noVNC_canvas'), UI.rfb = new RFB({'target': $D('noVNC_canvas'),
'onUpdateState': UI.updateState, 'onUpdateState': UI.updateState,
'onXvpInit': UI.updateXvpVisualState, 'onXvpInit': UI.updateXvpVisualState,
'onClipboard': UI.clipReceive, 'onClipboard': UI.clipReceive,
'onDesktopName': UI.updateDocumentTitle}); 'onDesktopName': UI.updateDocumentTitle});
autoconnect = WebUtil.getQueryVar('autoconnect', false); var autoconnect = WebUtil.getQueryVar('autoconnect', false);
if (autoconnect === 'true' || autoconnect == '1') { if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = true; autoconnect = true;
UI.connect(); UI.connect();
@ -105,14 +111,6 @@ start: function(callback) {
UI.updateVisualState(); 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 // Show mouse selector buttons on touch screen devices
if (UI.isTouchDevice) { if (UI.isTouchDevice) {
// Show mobile buttons // Show mobile buttons
@ -211,8 +209,8 @@ addMouseHandlers: function() {
// Read form control compatible setting from cookie // Read form control compatible setting from cookie
getSetting: function(name) { getSetting: function(name) {
var val, ctrl = $D('noVNC_' + name); var ctrl = $D('noVNC_' + name);
val = WebUtil.readSetting(name); var val = WebUtil.readSetting(name);
if (val !== null && ctrl.type === 'checkbox') { if (val !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) {
val = false; val = false;
@ -227,7 +225,6 @@ getSetting: function(name) {
// updates from control to current cookie setting. // updates from control to current cookie setting.
updateSetting: function(name, value) { updateSetting: function(name, value) {
var i, ctrl = $D('noVNC_' + name);
// Save the cookie for this session // Save the cookie for this session
if (typeof value !== 'undefined') { if (typeof value !== 'undefined') {
WebUtil.writeSetting(name, value); WebUtil.writeSetting(name, value);
@ -236,11 +233,12 @@ updateSetting: function(name, value) {
// Update the settings control // Update the settings control
value = UI.getSetting(name); value = UI.getSetting(name);
var ctrl = $D('noVNC_' + name);
if (ctrl.type === 'checkbox') { if (ctrl.type === 'checkbox') {
ctrl.checked = value; ctrl.checked = value;
} else if (typeof ctrl.options !== 'undefined') { } else if (typeof ctrl.options !== 'undefined') {
for (i = 0; i < ctrl.options.length; i += 1) { for (var i = 0; i < ctrl.options.length; i += 1) {
if (ctrl.options[i].value === value) { if (ctrl.options[i].value === value) {
ctrl.selectedIndex = i; ctrl.selectedIndex = i;
break; break;
@ -273,15 +271,12 @@ saveSetting: function(name) {
// Initial page load read/initialization of settings // Initial page load read/initialization of settings
initSetting: function(name, defVal) { initSetting: function(name, defVal) {
var val;
// Check Query string followed by cookie // Check Query string followed by cookie
val = WebUtil.getQueryVar(name); var val = WebUtil.getQueryVar(name);
if (val === null) { if (val === null) {
val = WebUtil.readSetting(name, defVal); val = WebUtil.readSetting(name, defVal);
} }
UI.updateSetting(name, val); UI.updateSetting(name, val);
//Util.Debug("Setting '" + name + "' initialized to '" + val + "'");
return val; return val;
}, },
@ -527,8 +522,6 @@ xvpReset: function() {
}, },
setMouseButton: function(num) { setMouseButton: function(num) {
var b, blist = [0, 1,2,4], button;
if (typeof num === 'undefined') { if (typeof num === 'undefined') {
// Disable mouse buttons // Disable mouse buttons
num = -1; num = -1;
@ -537,25 +530,20 @@ setMouseButton: function(num) {
UI.rfb.get_mouse().set_touchButton(num); UI.rfb.get_mouse().set_touchButton(num);
} }
for (b = 0; b < blist.length; b++) { var blist = [0, 1,2,4];
button = $D('noVNC_mouse_button' + blist[b]); for (var b = 0; b < blist.length; b++) {
var button = $D('noVNC_mouse_button' + blist[b]);
if (blist[b] === num) { if (blist[b] === num) {
button.style.display = ""; button.style.display = "";
} else { } else {
button.style.display = "none"; button.style.display = "none";
/*
button.style.backgroundColor = "black";
button.style.color = "lightgray";
button.style.backgroundColor = "";
button.style.color = "";
*/
} }
} }
}, },
updateState: function(rfb, state, oldstate, msg) { updateState: function(rfb, state, oldstate, msg) {
var s, sb, c, d, cad, vd, klass;
UI.rfb_state = state; UI.rfb_state = state;
var klass;
switch (state) { switch (state) {
case 'failed': case 'failed':
case 'fatal': case 'fatal':
@ -566,7 +554,7 @@ updateState: function(rfb, state, oldstate, msg) {
break; break;
case 'disconnected': case 'disconnected':
$D('noVNC_logo').style.display = "block"; $D('noVNC_logo').style.display = "block";
// Fall through /* falls through */
case 'loaded': case 'loaded':
klass = "noVNC_status_normal"; klass = "noVNC_status_normal";
break; break;
@ -661,32 +649,27 @@ updateXvpVisualState: function(ver) {
} }
}, },
// Display the desktop name in the document title // Display the desktop name in the document title
updateDocumentTitle: function(rfb, name) { updateDocumentTitle: function(rfb, name) {
document.title = name + " - noVNC"; document.title = name + " - noVNC";
}, },
clipReceive: function(rfb, text) { clipReceive: function(rfb, text) {
Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "..."); Util.Debug(">> UI.clipReceive: " + text.substr(0,40) + "...");
$D('noVNC_clipboard_text').value = text; $D('noVNC_clipboard_text').value = text;
Util.Debug("<< UI.clipReceive"); Util.Debug("<< UI.clipReceive");
}, },
connect: function() { connect: function() {
var host, port, password, path;
UI.closeSettingsMenu(); UI.closeSettingsMenu();
UI.toggleConnectPanel(); UI.toggleConnectPanel();
host = $D('noVNC_host').value; var host = $D('noVNC_host').value;
port = $D('noVNC_port').value; var port = $D('noVNC_port').value;
password = $D('noVNC_password').value; var password = $D('noVNC_password').value;
path = $D('noVNC_path').value; var path = $D('noVNC_path').value;
if ((!host) || (!port)) { if ((!host) || (!port)) {
throw("Must set host and port"); throw new Error("Must set host and port");
} }
UI.rfb.set_encrypt(UI.getSetting('encrypt')); UI.rfb.set_encrypt(UI.getSetting('encrypt'));
@ -734,18 +717,16 @@ clipSend: function() {
Util.Debug("<< UI.clipSend"); Util.Debug("<< UI.clipSend");
}, },
// Enable/disable and configure viewport clipping // Enable/disable and configure viewport clipping
setViewClip: function(clip) { setViewClip: function(clip) {
var display, cur_clip, pos, new_w, new_h; var display;
if (UI.rfb) { if (UI.rfb) {
display = UI.rfb.get_display(); display = UI.rfb.get_display();
} else { } else {
return; return;
} }
cur_clip = display.get_viewport(); var cur_clip = display.get_viewport();
if (typeof(clip) !== 'boolean') { if (typeof(clip) !== 'boolean') {
// Use current setting // Use current setting
@ -765,9 +746,9 @@ setViewClip: function(clip) {
if (UI.getSetting('clip')) { if (UI.getSetting('clip')) {
// If clipping, update clipping settings // If clipping, update clipping settings
$D('noVNC_canvas').style.position = 'absolute'; $D('noVNC_canvas').style.position = 'absolute';
pos = Util.getPosition($D('noVNC_canvas')); var pos = Util.getPosition($D('noVNC_canvas'));
new_w = window.innerWidth - pos.x; var new_w = window.innerWidth - pos.x;
new_h = window.innerHeight - pos.y; var new_h = window.innerHeight - pos.y;
display.set_viewport(true); display.set_viewport(true);
display.viewportChange(0, 0, new_w, new_h); display.viewportChange(0, 0, new_w, new_h);
} }
@ -801,13 +782,13 @@ setViewDrag: function(drag) {
// On touch devices, show the OS keyboard // On touch devices, show the OS keyboard
showKeyboard: function() { showKeyboard: function() {
var kbi, skb, l; var kbi = $D('keyboardinput');
kbi = $D('keyboardinput'); var skb = $D('showKeyboard');
skb = $D('showKeyboard'); var l = kbi.value.length;
l = kbi.value.length;
if(UI.keyboardVisible === false) { if(UI.keyboardVisible === false) {
kbi.focus(); kbi.focus();
kbi.setSelectionRange(l, l); // Move the caret to the end try { kbi.setSelectionRange(l, l); } // Move the caret to the end
catch (err) {} // setSelectionRange is undefined in Google Chrome
UI.keyboardVisible = true; UI.keyboardVisible = true;
skb.className = "noVNC_status_button_selected"; skb.className = "noVNC_status_button_selected";
} else if(UI.keyboardVisible === true) { } else if(UI.keyboardVisible === true) {
@ -828,33 +809,74 @@ keepKeyboard: function() {
} }
}, },
// When keypress events are left uncought, catch the input events from keyboardinputReset: function() {
// the keyboardinput element instead and send the corresponding key events. 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) { keyInput: function(event) {
var elem, input, len; var newValue = event.target.value;
elem = $D('keyboardinput'); var oldValue = UI.lastKeyboardinput;
input = event.target.value;
len = (elem.selectionStart > input.length) ? elem.selectionStart : input.length;
if (len < 1) { // something removed? var newLen;
UI.rfb.sendKey(0xff08); // send BACKSPACE try {
} else if (len > 1) { // new input? // Try to check caret position since whitespace at the end
for (var i = len-1; i > 0; i -= 1) { // will not be considered by value.length in some browsers
// HTML does not consider trailing whitespaces as a part of the string newLen = Math.max(event.target.selectionStart, newValue.length);
// and they are therefore undefined. } catch (err) {
if (input[len-i] !== undefined) { // selectionStart is undefined in Google Chrome
UI.rfb.sendKey(input.charCodeAt(len-i)); // send charCode newLen = newValue.length;
}
var oldLen = oldValue.length;
var backspaces;
var inputs = newLen - oldLen;
if (inputs < 0) {
backspaces = -inputs;
} else { } else {
UI.rfb.sendKey(0x0020); // send SPACE 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;
} }
} }
// In order to be able to delete text which has been written in // Send the key events
// another session there has to always be text in the for (i = 0; i < backspaces; i++) {
// keyboardinput element with which backspace can interact. UI.rfb.sendKey(XK_BackSpace);
// We also need to reset the input field text to avoid overflow. }
elem.value = "x"; 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() { keyInputBlur: function() {
@ -938,8 +960,7 @@ setResize: function () {
}, },
//Helper to add options to dropdown. //Helper to add options to dropdown.
addOption: function(selectbox,text,value ) addOption: function(selectbox, text, value) {
{
var optn = document.createElement("OPTION"); var optn = document.createElement("OPTION");
optn.text = text; optn.text = text;
optn.value = value; optn.value = value;
@ -955,7 +976,4 @@ setBarPosition: function() {
} }
}; };
})();

View file

@ -6,9 +6,8 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict"; /* jshint white: false, nonstandard: true */
/*jslint bitwise: false, white: false */ /*global window, console, document, navigator, ActiveXObject, INCLUDE_URI */
/*global window, console, document, navigator, ActiveXObject */
// Globals defined here // Globals defined here
var Util = {}; var Util = {};
@ -18,50 +17,152 @@ var Util = {};
* Make arrays quack * Make arrays quack
*/ */
Array.prototype.push8 = function (num) { var addFunc = function (cl, name, func) {
this.push(num & 0xFF); if (!cl.prototype[name]) {
Object.defineProperty(cl.prototype, name, { enumerable: false, value: func });
}
}; };
Array.prototype.push16 = function (num) { addFunc(Array, 'push8', function (num) {
"use strict";
this.push(num & 0xFF);
});
addFunc(Array, 'push16', function (num) {
"use strict";
this.push((num >> 8) & 0xFF, this.push((num >> 8) & 0xFF,
(num ) & 0xFF ); num & 0xFF);
}; });
Array.prototype.push32 = function (num) {
addFunc(Array, 'push32', function (num) {
"use strict";
this.push((num >> 24) & 0xFF, this.push((num >> 24) & 0xFF,
(num >> 16) & 0xFF, (num >> 16) & 0xFF,
(num >> 8) & 0xFF, (num >> 8) & 0xFF,
(num ) & 0xFF ); num & 0xFF);
}; });
// IE does not support map (even in IE9) // IE does not support map (even in IE9)
//This prototype is provided by the Mozilla foundation and //This prototype is provided by the Mozilla foundation and
//is distributed under the MIT license. //is distributed under the MIT license.
//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license //http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
if (!Array.prototype.map) addFunc(Array, 'map', function (fun /*, thisp*/) {
{ "use strict";
Array.prototype.map = function(fun /*, thisp*/)
{
var len = this.length; var len = this.length;
if (typeof fun != "function") if (typeof fun != "function") {
throw new TypeError(); throw new TypeError();
}
var res = new Array(len); var res = new Array(len);
var thisp = arguments[1]; var thisp = arguments[1];
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++) {
{ if (i in this) {
if (i in this)
res[i] = fun.call(thisp, this[i], i, 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
addFunc(Array, 'indexOf', function (elt /*, from*/) {
"use strict";
var len = this.length >>> 0;
var from = Number(arguments[1]) || 0;
from = (from < 0) ? Math.ceil(from) : Math.floor(from);
if (from < 0) {
from += len;
} }
for (; from < len; from++) {
if (from in this &&
this[from] === elt) {
return from;
}
}
return -1;
});
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
})();
}
// PhantomJS 1.x doesn't support bind,
// so leave this in until PhantomJS 2.0 is released
//This prototype is provided by the Mozilla foundation and
//is distributed under the MIT license.
//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
addFunc(Function, 'bind', function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - " +
"what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis ? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
});
// //
// requestAnimationFrame shim with setTimeout fallback // requestAnimationFrame shim with setTimeout fallback
// //
window.requestAnimFrame = (function () { window.requestAnimFrame = (function () {
"use strict";
return window.requestAnimationFrame || return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
@ -84,6 +185,7 @@ window.requestAnimFrame = (function(){
Util._log_level = 'warn'; Util._log_level = 'warn';
Util.init_logging = function (level) { Util.init_logging = function (level) {
"use strict";
if (typeof level === 'undefined') { if (typeof level === 'undefined') {
level = Util._log_level; level = Util._log_level;
} else { } else {
@ -94,26 +196,34 @@ Util.init_logging = function (level) {
window.console = { window.console = {
'log' : window.opera.postError, 'log' : window.opera.postError,
'warn' : window.opera.postError, 'warn' : window.opera.postError,
'error': window.opera.postError }; 'error': window.opera.postError
};
} else { } else {
window.console = { window.console = {
'log' : function (m) {}, 'log' : function (m) {},
'warn' : function (m) {}, 'warn' : function (m) {},
'error': function(m) {}}; 'error': function (m) {}
};
} }
} }
Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {}; Util.Debug = Util.Info = Util.Warn = Util.Error = function (msg) {};
/* jshint -W086 */
switch (level) { switch (level) {
case 'debug': Util.Debug = function (msg) { console.log(msg); }; case 'debug':
case 'info': Util.Info = function (msg) { console.log(msg); }; Util.Debug = function (msg) { console.log(msg); };
case 'warn': Util.Warn = function (msg) { console.warn(msg); }; case 'info':
case 'error': Util.Error = function (msg) { console.error(msg); }; 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': case 'none':
break; break;
default: default:
throw("invalid logging type '" + level + "'"); throw new Error("invalid logging type '" + level + "'");
} }
/* jshint +W086 */
}; };
Util.get_logging = function () { Util.get_logging = function () {
return Util._log_level; return Util._log_level;
@ -121,87 +231,139 @@ Util.get_logging = function () {
// Initialize logging level // Initialize logging level
Util.init_logging(); Util.init_logging();
Util.make_property = function (proto, name, mode, type) {
"use strict";
// Set configuration default for Crockford style function namespaces var getter;
Util.conf_default = function(cfg, api, defaults, v, mode, type, defval, desc) { if (type === 'arr') {
var getter, setter;
// Default getter function
getter = function (idx) { getter = function (idx) {
if ((type in {'arr':1, 'array':1}) &&
(typeof idx !== 'undefined')) {
return cfg[v][idx];
} else {
return cfg[v];
}
};
// Default setter function
setter = function (val, idx) {
if (type in {'boolean':1, 'bool':1}) {
if ((!val) || (val in {'0':1, 'no':1, 'false':1})) {
val = false;
} else {
val = true;
}
} else if (type in {'integer':1, 'int':1}) {
val = parseInt(val, 10);
} else if (type === 'str') {
val = String(val);
} else if (type === 'func') {
if (!val) {
val = function () {};
}
}
if (typeof idx !== 'undefined') { if (typeof idx !== 'undefined') {
cfg[v][idx] = val; return this['_' + name][idx];
} else { } else {
cfg[v] = val; return this['_' + name];
} }
}; };
} else {
// Set the description getter = function () {
api[v + '_description'] = desc; return this['_' + name];
// Set the getter function
if (typeof api['get_' + v] === 'undefined') {
api['get_' + v] = getter;
}
// Set the setter function with extra sanity checks
if (typeof api['set_' + v] === 'undefined') {
api['set_' + v] = function (val, idx) {
if (mode in {'RO':1, 'ro':1}) {
throw(v + " is read-only");
} else if ((mode in {'WO':1, 'wo':1}) &&
(typeof cfg[v] !== 'undefined')) {
throw(v + " can only be set once");
}
setter(val, idx);
}; };
} }
// Set the default value var make_setter = function (process_val) {
if (typeof defaults[v] !== 'undefined') { if (process_val) {
defval = defaults[v]; return function (val, idx) {
} else if ((type in {'arr':1, 'array':1}) && if (typeof idx !== 'undefined') {
(! (defval instanceof Array))) { this['_' + name][idx] = process_val(val);
defval = []; } else {
this['_' + name] = process_val(val);
}
};
} else {
return function (val, idx) {
if (typeof idx !== 'undefined') {
this['_' + name][idx] = val;
} else {
this['_' + name] = val;
}
};
} }
// Coerce existing setting to the right type
//Util.Debug("v: " + v + ", defval: " + defval + ", defaults[v]: " + defaults[v]);
setter(defval);
}; };
// Set group of configuration defaults var setter;
Util.conf_defaults = function(cfg, api, defaults, arr) { if (type === 'bool') {
setter = make_setter(function (val) {
if (!val || (val in {'0': 1, 'no': 1, 'false': 1})) {
return false;
} else {
return true;
}
});
} else if (type === 'int') {
setter = make_setter(function (val) { return parseInt(val, 10); });
} else if (type === 'float') {
setter = make_setter(parseFloat);
} else if (type === 'str') {
setter = make_setter(String);
} else if (type === 'func') {
setter = make_setter(function (val) {
if (!val) {
return function () {};
} else {
return val;
}
});
} else if (type === 'arr' || type === 'dom' || type == 'raw') {
setter = make_setter();
} else {
throw new Error('Unknown property type ' + type); // some sanity checking
}
// set the getter
if (typeof proto['get_' + name] === 'undefined') {
proto['get_' + name] = getter;
}
// set the setter if needed
if (typeof proto['set_' + name] === 'undefined') {
if (mode === 'rw') {
proto['set_' + name] = setter;
} else if (mode === 'wo') {
proto['set_' + name] = function (val, idx) {
if (typeof this['_' + name] !== 'undefined') {
throw new Error(name + " can only be set once");
}
setter.call(this, val, idx);
};
}
}
// make a special setter that we can use in set defaults
proto['_raw_set_' + name] = function (val, idx) {
setter.call(this, val, idx);
//delete this['_init_set_' + name]; // remove it after use
};
};
Util.make_properties = function (constructor, arr) {
"use strict";
for (var i = 0; i < arr.length; i++) {
Util.make_property(constructor.prototype, arr[i][0], arr[i][1], arr[i][2]);
}
};
Util.set_defaults = function (obj, conf, defaults) {
var defaults_keys = Object.keys(defaults);
var conf_keys = Object.keys(conf);
var keys_obj = {};
var i; var i;
for (i = 0; i < arr.length; i++) { for (i = 0; i < defaults_keys.length; i++) { keys_obj[defaults_keys[i]] = 1; }
Util.conf_default(cfg, api, defaults, arr[i][0], arr[i][1], for (i = 0; i < conf_keys.length; i++) { keys_obj[conf_keys[i]] = 1; }
arr[i][2], arr[i][3], arr[i][4]); var keys = Object.keys(keys_obj);
for (i = 0; i < keys.length; i++) {
var setter = obj['_raw_set_' + keys[i]];
if (!setter) {
Util.Warn('Invalid property ' + keys[i]);
continue;
}
if (keys[i] in conf) {
setter.call(obj, conf[keys[i]]);
} else {
setter.call(obj, defaults[keys[i]]);
}
} }
}; };
/*
* Decode from UTF-8
*/
Util.decodeUTF8 = function (utf8string) {
"use strict";
return decodeURIComponent(escape(utf8string));
};
/* /*
* Cross-browser routines * Cross-browser routines
@ -216,18 +378,15 @@ Util.conf_defaults = function(cfg, api, defaults, arr) {
// window.onscriptsloaded handler is called (if set). // 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/"; return (typeof INCLUDE_URI !== "undefined") ? INCLUDE_URI : "include/";
} };
Util._loading_scripts = []; Util._loading_scripts = [];
Util._pending_scripts = []; Util._pending_scripts = [];
Util.load_scripts = function (files) { Util.load_scripts = function (files) {
"use strict";
var head = document.getElementsByTagName('head')[0], script, var head = document.getElementsByTagName('head')[0], script,
ls = Util._loading_scripts, ps = Util._pending_scripts; ls = Util._loading_scripts, ps = Util._pending_scripts;
for (var f=0; f<files.length; f++) {
script = document.createElement('script'); var loadFunc = function (e) {
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' || while (ls.length > 0 && (ls[0].readyState === 'loaded' ||
ls[0].readyState === 'complete')) { ls[0].readyState === 'complete')) {
// For IE, append the script to trigger execution // For IE, append the script to trigger execution
@ -250,6 +409,13 @@ Util.load_scripts = function(files) {
} }
} }
}; };
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 = loadFunc;
// In-order script execution tricks // In-order script execution tricks
if (Util.Engine.trident) { if (Util.Engine.trident) {
// For IE wait until readyState is 'loaded' before // For IE wait until readyState is 'loaded' before
@ -264,23 +430,80 @@ Util.load_scripts = function(files) {
} }
ps.push(script); ps.push(script);
} }
} };
// Get DOM element position on page // Get DOM element position on page
Util.getPosition = function (obj) { // This solution is based based on http://www.greywyvern.com/?post=331
var x = 0, y = 0; // Thanks to Brian Huisman AKA GreyWyvern!
if (obj.offsetParent) { Util.getPosition = (function () {
do { "use strict";
x += obj.offsetLeft; function getStyle(obj, styleProp) {
y += obj.offsetTop; var y;
obj = obj.offsetParent; if (obj.currentStyle) {
} while (obj); y = obj.currentStyle[styleProp];
} else if (window.getComputedStyle)
y = window.getComputedStyle(obj, null)[styleProp];
return y;
} }
return {'x': x, 'y': y};
function scrollDist() {
var myScrollTop = 0, myScrollLeft = 0;
var html = document.getElementsByTagName('html')[0];
// get the scrollTop part
if (html.scrollTop && document.documentElement.scrollTop) {
myScrollTop = html.scrollTop;
} else if (html.scrollTop || document.documentElement.scrollTop) {
myScrollTop = html.scrollTop + document.documentElement.scrollTop;
} else if (document.body.scrollTop) {
myScrollTop = document.body.scrollTop;
} else {
myScrollTop = 0;
}
// get the scrollLeft part
if (html.scrollLeft && document.documentElement.scrollLeft) {
myScrollLeft = html.scrollLeft;
} else if (html.scrollLeft || document.documentElement.scrollLeft) {
myScrollLeft = html.scrollLeft + document.documentElement.scrollLeft;
} else if (document.body.scrollLeft) {
myScrollLeft = document.body.scrollLeft;
} else {
myScrollLeft = 0;
}
return [myScrollLeft, myScrollTop];
}
return function (obj) {
var curleft = 0, curtop = 0, scr = obj, fixed = false;
while ((scr = scr.parentNode) && scr != document.body) {
curleft -= scr.scrollLeft || 0;
curtop -= scr.scrollTop || 0;
if (getStyle(scr, "position") == "fixed") {
fixed = true;
}
}
if (fixed && !window.opera) {
var scrDist = scrollDist();
curleft += scrDist[0];
curtop += scrDist[1];
}
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while ((obj = obj.offsetParent));
return {'x': curleft, 'y': curtop};
}; };
})();
// Get mouse event position in DOM element // Get mouse event position in DOM element
Util.getEventPosition = function (e, obj, scale) { Util.getEventPosition = function (e, obj, scale) {
"use strict";
var evt, docX, docY, pos; var evt, docX, docY, pos;
//if (!e) evt = window.event; //if (!e) evt = window.event;
evt = (e ? e : window.event); evt = (e ? e : window.event);
@ -308,6 +531,7 @@ Util.getEventPosition = function (e, obj, scale) {
// Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events // Event registration. Based on: http://www.scottandrew.com/weblog/articles/cbs-events
Util.addEvent = function (obj, evType, fn) { Util.addEvent = function (obj, evType, fn) {
"use strict";
if (obj.attachEvent) { if (obj.attachEvent) {
var r = obj.attachEvent("on" + evType, fn); var r = obj.attachEvent("on" + evType, fn);
return r; return r;
@ -315,11 +539,12 @@ Util.addEvent = function (obj, evType, fn){
obj.addEventListener(evType, fn, false); obj.addEventListener(evType, fn, false);
return true; return true;
} else { } else {
throw("Handler could not be attached"); throw new Error("Handler could not be attached");
} }
}; };
Util.removeEvent = function (obj, evType, fn) { Util.removeEvent = function (obj, evType, fn) {
"use strict";
if (obj.detachEvent) { if (obj.detachEvent) {
var r = obj.detachEvent("on" + evType, fn); var r = obj.detachEvent("on" + evType, fn);
return r; return r;
@ -327,11 +552,12 @@ Util.removeEvent = function(obj, evType, fn){
obj.removeEventListener(evType, fn, false); obj.removeEventListener(evType, fn, false);
return true; return true;
} else { } 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(); } if (e.stopPropagation) { e.stopPropagation(); }
else { e.cancelBubble = true; } else { e.cancelBubble = true; }
@ -343,31 +569,78 @@ Util.stopEvent = function(e) {
// Set browser engine versions. Based on mootools. // Set browser engine versions. Based on mootools.
Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)}; Util.Features = {xpath: !!(document.evaluate), air: !!(window.runtime), query: !!(document.querySelector)};
(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 = { Util.Engine = {
// Version detection break in Opera 11.60 (errors on arguments.callee.caller reference) // Version detection break in Opera 11.60 (errors on arguments.callee.caller reference)
//'presto': (function() { //'presto': (function() {
// return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()), // return (!window.opera) ? false : ((arguments.callee.caller) ? 960 : ((document.getElementsByClassName) ? 950 : 925)); }()),
'presto': (function() { return (!window.opera) ? false : true; }()), 'presto': detectPresto(),
'trident': detectTrident(),
'trident': (function() { 'webkit': detectInitialWebkit(),
return (!window.ActiveXObject) ? false : ((window.XMLHttpRequest) ? ((document.querySelectorAll) ? 6 : 5) : 4); }()), 'gecko': detectGecko(),
'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) { if (Util.Engine.webkit) {
// Extract actual webkit version if available // Extract actual webkit version if available
Util.Engine.webkit = (function(v) { Util.Engine.webkit = detectActualWebkit(Util.Engine.webkit);
var re = new RegExp('WebKit/([0-9\.]*) ');
v = (navigator.userAgent.match(re) || ['', v])[1];
return parseFloat(v, 10);
})(Util.Engine.webkit);
} }
})();
Util.Flash = (function () { Util.Flash = (function () {
"use strict";
var v, version; var v, version;
try { try {
v = navigator.plugins['Shockwave Flash'].description; v = navigator.plugins['Shockwave Flash'].description;

View file

@ -14,7 +14,7 @@
* read binary data off of the receive queue. * read binary data off of the receive queue.
*/ */
/*jslint browser: true, bitwise: false, plusplus: false */ /*jslint browser: true, bitwise: true */
/*global Util, Base64 */ /*global Util, Base64 */
@ -43,251 +43,193 @@ if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
} }
Util.load_scripts(["web-socket-js/swfobject.js", Util.load_scripts(["web-socket-js/swfobject.js",
"web-socket-js/web_socket.js"]); "web-socket-js/web_socket.js"]);
}()); })();
} }
function Websock() { function Websock() {
"use strict"; "use strict";
var api = {}, // Public API this._websocket = null; // WebSocket object
websocket = null, // WebSocket object this._rQ = []; // Receive queue
mode = 'base64', // Current WebSocket mode: 'binary', 'base64' this._rQi = 0; // Receive queue index
rQ = [], // Receive queue this._rQmax = 10000; // Max receive queue size before compacting
rQi = 0, // Receive queue index this._sQ = []; // Send queue
rQmax = 10000, // Max receive queue size before compacting
sQ = [], // Send queue
eventHandlers = { this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64'
this.maxBufferedAmount = 200;
this._eventHandlers = {
'message': function () {}, 'message': function () {},
'open': function () {}, 'open': function () {},
'close': function () {}, 'close': function () {},
'error': function () {} 'error': function () {}
};
}
(function () {
"use strict";
Websock.prototype = {
// Getters and Setters
get_sQ: function () {
return this._sQ;
}, },
test_mode = false; get_rQ: function () {
return this._rQ;
},
get_rQi: function () {
return this._rQi;
},
// set_rQi: function (val) {
// Queue public functions this._rQi = val;
// },
function get_sQ() { // Receive Queue
return sQ; rQlen: function () {
} return this._rQ.length - this._rQi;
},
function get_rQ() { rQpeek8: function () {
return rQ; return this._rQ[this._rQi];
} },
function get_rQi() {
return rQi;
}
function set_rQi(val) {
rQi = val;
}
function rQlen() { rQshift8: function () {
return rQ.length - rQi; return this._rQ[this._rQi++];
} },
function rQpeek8() { rQskip8: function () {
return (rQ[rQi] ); this._rQi++;
} },
function rQshift8() {
return (rQ[rQi++] ); rQskipBytes: function (num) {
} this._rQi += num;
function rQunshift8(num) { },
if (rQi === 0) {
rQ.unshift(num); rQunshift8: function (num) {
if (this._rQi === 0) {
this._rQ.unshift(num);
} else { } else {
rQi -= 1; this._rQi--;
rQ[rQi] = num; this._rQ[this._rQi] = num;
} }
},
} rQshift16: function () {
function rQshift16() { return (this._rQ[this._rQi++] << 8) +
return (rQ[rQi++] << 8) + this._rQ[this._rQi++];
(rQ[rQi++] ); },
}
function rQshift32() { rQshift32: function () {
return (rQ[rQi++] << 24) + return (this._rQ[this._rQi++] << 24) +
(rQ[rQi++] << 16) + (this._rQ[this._rQi++] << 16) +
(rQ[rQi++] << 8) + (this._rQ[this._rQi++] << 8) +
(rQ[rQi++] ); this._rQ[this._rQi++];
} },
function rQshiftStr(len) {
if (typeof(len) === 'undefined') { len = rQlen(); } rQshiftStr: function (len) {
var arr = rQ.slice(rQi, rQi + len); if (typeof(len) === 'undefined') { len = this.rQlen(); }
rQi += len; var arr = this._rQ.slice(this._rQi, this._rQi + len);
this._rQi += len;
return String.fromCharCode.apply(null, arr); return String.fromCharCode.apply(null, arr);
} },
function rQshiftBytes(len) {
if (typeof(len) === 'undefined') { len = rQlen(); }
rQi += len;
return rQ.slice(rQi-len, rQi);
}
function rQslice(start, end) { 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) { if (end) {
return rQ.slice(rQi + start, rQi + end); return this._rQ.slice(this._rQi + start, this._rQi + end);
} else { } else {
return rQ.slice(rQi + start); return this._rQ.slice(this._rQi + start);
}
} }
},
// Check to see if we must wait for 'num' bytes (default to FBU.bytes) // 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 // to be available in the receive queue. Return true if we need to
// wait (and possibly print a debug message), otherwise false. // wait (and possibly print a debug message), otherwise false.
function rQwait(msg, num, goback) { rQwait: function (msg, num, goback) {
var rQlen = rQ.length - rQi; // Skip rQlen() function call var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call
if (rQlen < num) { if (rQlen < num) {
if (goback) { if (goback) {
if (rQi < goback) { if (this._rQi < goback) {
throw("rQwait cannot backup " + goback + " bytes"); throw new Error("rQwait cannot backup " + goback + " bytes");
} }
rQi -= goback; this._rQi -= goback;
} }
//Util.Debug(" waiting for " + (num-rQlen) +
// " " + msg + " byte(s)");
return true; // true means need more data return true; // true means need more data
} }
return false; return false;
},
// Send Queue
flush: function () {
if (this._websocket.bufferedAmount !== 0) {
Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
} }
// if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
// Private utility routines if (this._sQ.length > 0) {
// this._websocket.send(this._encode_message());
this._sQ = [];
function encode_message() {
if (mode === 'binary') {
// Put in a binary arraybuffer
return (new Uint8Array(sQ)).buffer;
} else {
// base64 encode
return Base64.encode(sQ);
}
} }
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);
}
//
// 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; return true;
} else { } else {
Util.Info("Delaying send, bufferedAmount: " + Util.Info("Delaying send, bufferedAmount: " +
websocket.bufferedAmount); this._websocket.bufferedAmount);
return false; return false;
} }
} },
// overridable for testing send: function (arr) {
function send(arr) { this._sQ = this._sQ.concat(arr);
//Util.Debug(">> send_array: " + arr); return this.flush();
sQ = sQ.concat(arr); },
return flush();
}
function send_string(str) { send_string: function (str) {
//Util.Debug(">> send_string: " + str); this.send(str.split('').map(function (chr) {
api.send(str.split('').map( return chr.charCodeAt(0);
function (chr) { return chr.charCodeAt(0); } ) ); }));
} },
// // Event Handlers
// Other public functions on: function (evt, handler) {
this._eventHandlers[evt] = handler;
},
function recv_message(e) { init: function (protocols, ws_schema) {
//Util.Debug(">> recv_message: " + e.data.length); this._rQ = [];
this._rQi = 0;
try { this._sQ = [];
decode_message(e.data); this._websocket = null;
if (rQlen() > 0) {
eventHandlers.message();
// Compact the receive queue
if (rQ.length > rQmax) {
//Util.Debug("Compacting receive queue");
rQ = rQ.slice(rQi);
rQi = 0;
}
} 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) {
rQ = [];
rQi = 0;
sQ = [];
websocket = null;
var bt = false,
wsbt = false,
try_binary = false;
// Check for full typed array support // Check for full typed array support
var bt = false;
if (('Uint8Array' in window) && if (('Uint8Array' in window) &&
('set' in Uint8Array.prototype)) { ('set' in Uint8Array.prototype)) {
bt = true; bt = true;
} }
// Check for full binary type support in WebSockets // Check for full binary type support in WebSockets
// TODO: this sucks, the property should exist on the prototype // Inspired by:
// but it does not. // https://github.com/Modernizr/Modernizr/issues/370
// https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
var wsbt = false;
try { try {
if (bt && ('binaryType' in (new WebSocket("wss://localhost:17523")))) { if (bt && ('binaryType' in WebSocket.prototype ||
!!(new WebSocket(ws_schema + '://.').binaryType))) {
Util.Info("Detected binaryType support in WebSockets"); Util.Info("Detected binaryType support in WebSockets");
wsbt = true; wsbt = true;
} }
} catch (exc) { } catch (exc) {
// Just ignore failed test localhost connections // Just ignore failed test localhost connection
} }
// Default protocols if not specified // Default protocols if not specified
@ -299,124 +241,144 @@ function init(protocols) {
} }
} }
// If no binary support, make sure it was not requested
if (!wsbt) { if (!wsbt) {
if (protocols === 'binary') { if (protocols === 'binary') {
throw("WebSocket binary sub-protocol requested but not supported"); throw new Error('WebSocket binary sub-protocol requested but not supported');
} }
if (typeof(protocols) === "object") {
if (typeof(protocols) === 'object') {
var new_protocols = []; var new_protocols = [];
for (var i = 0; i < protocols.length; i++) { for (var i = 0; i < protocols.length; i++) {
if (protocols[i] === 'binary') { if (protocols[i] === 'binary') {
Util.Error("Skipping unsupported WebSocket binary sub-protocol"); Util.Error('Skipping unsupported WebSocket binary sub-protocol');
} else { } else {
new_protocols.push(protocols[i]); new_protocols.push(protocols[i]);
} }
} }
if (new_protocols.length > 0) { if (new_protocols.length > 0) {
protocols = new_protocols; protocols = new_protocols;
} else { } else {
throw("Only WebSocket binary sub-protocol was requested and not supported."); throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
} }
} }
} }
return protocols; return protocols;
} },
function open(uri, protocols) { open: function (uri, protocols) {
protocols = init(protocols); var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
protocols = this.init(protocols, ws_schema);
this._websocket = new WebSocket(uri, protocols);
if (test_mode) {
websocket = {};
} else {
websocket = new WebSocket(uri, protocols);
if (protocols.indexOf('binary') >= 0) { if (protocols.indexOf('binary') >= 0) {
websocket.binaryType = 'arraybuffer'; this._websocket.binaryType = 'arraybuffer';
}
} }
websocket.onmessage = recv_message; this._websocket.onmessage = this._recv_message.bind(this);
websocket.onopen = function() { this._websocket.onopen = (function () {
Util.Debug(">> WebSock.onopen"); Util.Debug('>> WebSock.onopen');
if (websocket.protocol) { if (this._websocket.protocol) {
mode = websocket.protocol; this._mode = this._websocket.protocol;
Util.Info("Server chose sub-protocol: " + websocket.protocol); Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
} else { } else {
mode = 'base64'; this._mode = 'base64';
Util.Error("Server select no sub-protocol!: " + websocket.protocol); Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
} }
eventHandlers.open(); this._eventHandlers.open();
Util.Debug("<< WebSock.onopen"); Util.Debug("<< WebSock.onopen");
}; }).bind(this);
websocket.onclose = function(e) { this._websocket.onclose = (function (e) {
Util.Debug(">> WebSock.onclose"); Util.Debug(">> WebSock.onclose");
eventHandlers.close(e); this._eventHandlers.close(e);
Util.Debug("<< WebSock.onclose"); Util.Debug("<< WebSock.onclose");
}; }).bind(this);
websocket.onerror = function(e) { this._websocket.onerror = (function (e) {
Util.Debug(">> WebSock.onerror: " + e); Util.Debug(">> WebSock.onerror: " + e);
eventHandlers.error(e); this._eventHandlers.error(e);
Util.Debug("<< WebSock.onerror"); Util.Debug("<< WebSock.onerror: " + e);
}; }).bind(this);
} },
function close() { close: function () {
if (websocket) { if (this._websocket) {
if ((websocket.readyState === WebSocket.OPEN) || if ((this._websocket.readyState === WebSocket.OPEN) ||
(websocket.readyState === WebSocket.CONNECTING)) { (this._websocket.readyState === WebSocket.CONNECTING)) {
Util.Info("Closing WebSocket connection"); Util.Info("Closing WebSocket connection");
websocket.close(); this._websocket.close();
}
websocket.onmessage = function (e) { return; };
}
} }
// Override internal functions for testing this._websocket.onmessage = function (e) { return; };
// Takes a send function, returns reference to recv function }
function testMode(override_send, data_mode) { },
test_mode = true;
mode = data_mode; // private methods
api.send = override_send; _encode_message: function () {
api.close = function () {}; if (this._mode === 'binary') {
return recv_message; // Put in a binary arraybuffer
return (new Uint8Array(this._sQ)).buffer;
} else {
// 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";
} }
function constructor() { if (typeof exc.description !== 'undefined') {
// Configuration settings exception_str += " description: " + exc.description + "\n";
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(); if (typeof exc.stack !== 'undefined') {
exception_str += exc.stack;
} }
if (exception_str.length > 0) {
Util.Error("recv_message, caught exception: " + exception_str);
} else {
Util.Error("recv_message, caught exception: " + exc);
}
if (typeof exc.name !== 'undefined') {
this._eventHandlers.error(exc.name + ": " + exc.message);
} else {
this._eventHandlers.error(exc);
}
}
}
};
})();

View file

@ -7,8 +7,7 @@
* See README.md for usage and integration instructions. * See README.md for usage and integration instructions.
*/ */
"use strict"; /*jslint bitwise: false, white: false, browser: true, devel: true */
/*jslint bitwise: false, white: false */
/*global Util, window, document */ /*global Util, window, document */
// Globals defined here // Globals defined here
@ -39,29 +38,31 @@ if (!window.$D) {
// init log level reading the logging HTTP param // init log level reading the logging HTTP param
WebUtil.init_logging = function (level) { WebUtil.init_logging = function (level) {
"use strict";
if (typeof level !== "undefined") { if (typeof level !== "undefined") {
Util._log_level = level; Util._log_level = level;
} else { } else {
Util._log_level = (document.location.href.match( var param = document.location.href.match(/logging=([A-Za-z0-9\._\-]*)/);
/logging=([A-Za-z0-9\._\-]*)/) || Util._log_level = (param || ['', Util._log_level])[1];
['', Util._log_level])[1];
} }
Util.init_logging(); Util.init_logging();
}; };
WebUtil.dirObj = function (obj, depth, parent) { WebUtil.dirObj = function (obj, depth, parent) {
var i, msg = "", val = ""; "use strict";
if (! depth) { depth = 2; } if (! depth) { depth = 2; }
if (! parent) { parent = ""; } if (! parent) { parent = ""; }
// Print the properties of the passed-in object // Print the properties of the passed-in object
for (i in obj) { var msg = "";
for (var i in obj) {
if ((depth > 1) && (typeof obj[i] === "object")) { if ((depth > 1) && (typeof obj[i] === "object")) {
// Recurse attributes that are objects // Recurse attributes that are objects
msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i); msg += WebUtil.dirObj(obj[i], depth - 1, parent + "." + i);
} else { } else {
//val = new String(obj[i]).replace("\n", " "); //val = new String(obj[i]).replace("\n", " ");
var val = "";
if (typeof(obj[i]) === "undefined") { if (typeof(obj[i]) === "undefined") {
val = "undefined"; val = "undefined";
} else { } else {
@ -78,7 +79,8 @@ WebUtil.dirObj = function (obj, depth, parent) {
// Read a query string variable // Read a query string variable
WebUtil.getQueryVar = function (name, defVal) { WebUtil.getQueryVar = function (name, defVal) {
var re = new RegExp('[?][^#]*' + name + '=([^&#]*)'), "use strict";
var re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re); match = document.location.href.match(re);
if (typeof defVal === 'undefined') { defVal = null; } if (typeof defVal === 'undefined') { defVal = null; }
if (match) { if (match) {
@ -95,7 +97,8 @@ WebUtil.getQueryVar = function(name, defVal) {
// No days means only for this browser session // No days means only for this browser session
WebUtil.createCookie = function (name, value, days) { WebUtil.createCookie = function (name, value, days) {
var date, expires, secure; "use strict";
var date, expires;
if (days) { if (days) {
date = new Date(); date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
@ -103,6 +106,8 @@ WebUtil.createCookie = function(name,value,days) {
} else { } else {
expires = ""; expires = "";
} }
var secure;
if (document.location.protocol === "https:") { if (document.location.protocol === "https:") {
secure = "; secure"; secure = "; secure";
} else { } else {
@ -112,9 +117,12 @@ WebUtil.createCookie = function(name,value,days) {
}; };
WebUtil.readCookie = function (name, defaultValue) { WebUtil.readCookie = function (name, defaultValue) {
var i, c, nameEQ = name + "=", ca = document.cookie.split(';'); "use strict";
for(i=0; i < ca.length; i += 1) { var nameEQ = name + "=",
c = ca[i]; 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); } while (c.charAt(0) === ' ') { c = c.substring(1, c.length); }
if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); } if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length, c.length); }
} }
@ -122,6 +130,7 @@ WebUtil.readCookie = function(name, defaultValue) {
}; };
WebUtil.eraseCookie = function (name) { WebUtil.eraseCookie = function (name) {
"use strict";
WebUtil.createCookie(name, "", -1); WebUtil.createCookie(name, "", -1);
}; };
@ -129,7 +138,8 @@ WebUtil.eraseCookie = function(name) {
* Setting handling. * Setting handling.
*/ */
WebUtil.initSettings = function(callback) { WebUtil.initSettings = function (callback /*, ...callbackArgs */) {
"use strict";
var callbackArgs = Array.prototype.slice.call(arguments, 1); var callbackArgs = Array.prototype.slice.call(arguments, 1);
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.get(function (cfg) { window.chrome.storage.sync.get(function (cfg) {
@ -149,6 +159,7 @@ WebUtil.initSettings = function(callback) {
// No days means only for this browser session // 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) { if (window.chrome && window.chrome.storage) {
//console.log("writeSetting:", name, value); //console.log("writeSetting:", name, value);
if (WebUtil.settings[name] !== value) { if (WebUtil.settings[name] !== value) {
@ -161,6 +172,7 @@ WebUtil.writeSetting = function(name, value) {
}; };
WebUtil.readSetting = function (name, defaultValue) { WebUtil.readSetting = function (name, defaultValue) {
"use strict";
var value; var value;
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
value = WebUtil.settings[name]; value = WebUtil.settings[name];
@ -178,6 +190,7 @@ WebUtil.readSetting = function(name, defaultValue) {
}; };
WebUtil.eraseSetting = function (name) { WebUtil.eraseSetting = function (name) {
"use strict";
if (window.chrome && window.chrome.storage) { if (window.chrome && window.chrome.storage) {
window.chrome.storage.sync.remove(name); window.chrome.storage.sync.remove(name);
delete WebUtil.settings[name]; delete WebUtil.settings[name];
@ -189,9 +202,12 @@ WebUtil.eraseSetting = function(name) {
/* /*
* Alternate stylesheet selection * Alternate stylesheet selection
*/ */
WebUtil.getStylesheets = function() { var i, links, sheets = []; WebUtil.getStylesheets = function () {
links = document.getElementsByTagName("link"); "use strict";
for (i = 0; i < links.length; i += 1) { var links = document.getElementsByTagName("link");
var sheets = [];
for (var i = 0; i < links.length; i += 1) {
if (links[i].title && if (links[i].title &&
links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) { links[i].rel.toUpperCase().indexOf("STYLESHEET") > -1) {
sheets.push(links[i]); sheets.push(links[i]);
@ -203,12 +219,14 @@ WebUtil.getStylesheets = function() { var i, links, sheets = [];
// No sheet means try and use value from cookie, null sheet used to // No sheet means try and use value from cookie, null sheet used to
// clear all alternates. // clear all alternates.
WebUtil.selectStylesheet = function (sheet) { WebUtil.selectStylesheet = function (sheet) {
var i, link, sheets = WebUtil.getStylesheets(); "use strict";
if (typeof sheet === 'undefined') { if (typeof sheet === 'undefined') {
sheet = 'default'; sheet = 'default';
} }
for (i=0; i < sheets.length; i += 1) {
link = sheets[i]; var sheets = WebUtil.getStylesheets();
for (var i = 0; i < sheets.length; i += 1) {
var link = sheets[i];
if (link.title === sheet) { if (link.title === sheet) {
Util.Debug("Using stylesheet " + sheet); Util.Debug("Using stylesheet " + sheet);
link.disabled = false; link.disabled = false;

194
noVNC/karma.conf.js Normal file
View file

@ -0,0 +1,194 @@
// 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',
'tests/assertions.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.captureTimeout = 0; // use SL timeout
my_conf.sauceLabs = {
testName: 'noVNC Tests (all)',
startConnect: true,
tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER
};
}
config.set(my_conf);
};

50
noVNC/package.json Normal file
View file

@ -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.10.0",
"commander": "^2.5.0",
"karma": "^0.12.25",
"karma-chai": "^0.1.0",
"karma-mocha": "^0.1.9",
"karma-mocha-reporter": "^0.3.1",
"karma-phantomjs-launcher": "^0.1.4",
"karma-sauce-launcher": "^0.2.10",
"karma-sinon": "^1.0.3",
"karma-sinon-chai-latest": "^0.1.0",
"mocha": "^2.0.1",
"open": "^0.0.5",
"phantom": "^0.7.0",
"phantomjs": "^1.9.12",
"sinon": "^1.12.1",
"sinon-chai": "^2.6.0",
"spooky": "^0.2.5",
"temp": "^0.8.1"
}
}

24
noVNC/tests/assertions.js Normal file
View file

@ -0,0 +1,24 @@
// some useful assertions for noVNC
chai.use(function (_chai, utils) {
_chai.Assertion.addMethod('displayed', function (target_data) {
var obj = this._obj;
var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data;
// NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
var data = new Uint8Array(data_cl);
this.assert(utils.eql(data, target_data),
"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
"expected #{this} not to have displayed the image #{act}",
target_data,
data);
});
_chai.Assertion.addMethod('sent', function (target_data) {
var obj = this._obj;
var data = obj._websocket._get_sent_data();
this.assert(utils.eql(data, target_data),
"expected #{this} to have sent the data #{exp}, but it actually sent #{act}",
"expected #{this} not to have sent the data #{act}",
target_data,
data);
});
});

View file

@ -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;
}
};
})();

View file

@ -2,7 +2,7 @@ var Spooky = require('spooky');
var path = require('path'); var path = require('path');
var phantom_path = require('phantomjs').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; process.env.PHANTOMJS_EXECUTABLE = phantom_path;
var casper_opts = { var casper_opts = {
child: { child: {
@ -26,7 +26,10 @@ var provide_emitter = function(file_paths) {
file_paths.forEach(function(file_path, path_ind) { file_paths.forEach(function(file_path, path_ind) {
spooky.thenOpen('file://'+file_path); spooky.thenOpen('file://'+file_path);
spooky.then([{ path_ind: path_ind }, function() { spooky.waitFor(function() {
return this.getGlobal('__mocha_done') === true;
},
[{ path_ind: path_ind }, function() {
var res_json = { var res_json = {
file_ind: path_ind file_ind: path_ind
}; };

View file

@ -15,13 +15,16 @@ program
.option('-c, --color', 'Explicitly enable color (default is to use color when not outputting to a pipe)') .option('-c, --color', 'Explicitly enable color (default is to use color when not outputting to a pipe)')
.option('-i, --auto-inject <includefiles>', 'Treat the test list as a set of mocha JS files, and automatically generate HTML files with which to test test. \'includefiles\' should be a comma-separated list of paths to javascript files to include in each of the generated HTML files', make_list, null) .option('-i, --auto-inject <includefiles>', 'Treat the test list as a set of mocha JS files, and automatically generate HTML files with which to test test. \'includefiles\' should be a comma-separated list of paths to javascript files to include in each of the generated HTML files', make_list, null)
.option('-p, --provider <name>', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper') .option('-p, --provider <name>', 'Use the given provider (defaults to "casper"). Currently, may be "casper" or "zombie"', 'casper')
.option('-g, --generate-html', 'Instead of running the tests, just return the path to the generated HTML file, then wait for user interaction to exit (should be used with .js tests)') .option('-g, --generate-html', 'Instead of running the tests, just return the path to the generated HTML file, then wait for user interaction to exit (should be used with .js tests).')
.option('-o, --output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)') .option('-o, --open-in-browser', 'Open the generated HTML files in a web browser using the "open" module (must be used with the "-g"/"--generate-html" option).')
.option('--output-html', 'Instead of running the tests, just output the generated HTML source to STDOUT (should be used with .js tests)')
.option('-d, --debug', 'Show debug output (the "console" event) from the provider') .option('-d, --debug', 'Show debug output (the "console" event) from the provider')
.option('-r, --relative', 'Use relative paths in the generated HTML file')
.parse(process.argv); .parse(process.argv);
if (program.tests.length === 0) { if (program.tests.length === 0) {
program.tests = fs.readdirSync(__dirname).filter(function(f) { return (/^test\.(\w|\.|-)+\.js$/).test(f); }); program.tests = fs.readdirSync(__dirname).filter(function(f) { return (/^test\.(\w|\.|-)+\.js$/).test(f); });
program.tests = program.tests.map(function (f) { return path.resolve(__dirname, f); }); // add full paths in
console.log('using files %s', program.tests); console.log('using files %s', program.tests);
} }
@ -29,6 +32,27 @@ var file_paths = [];
var all_js = program.tests.reduce(function(a,e) { return a && e.slice(-3) == '.js'; }, true); var all_js = program.tests.reduce(function(a,e) { return a && e.slice(-3) == '.js'; }, true);
var get_path = function (/* arguments */) {
if (program.relative) {
return path.join.apply(null, arguments);
} else {
var args = Array.prototype.slice.call(arguments);
args.unshift(__dirname, '..');
return path.resolve.apply(null, args);
}
};
var get_path_cwd = function (/* arguments */) {
if (program.relative) {
var part_path = path.join.apply(null, arguments);
return path.relative(path.join(__dirname, '..'), path.resolve(process.cwd(), part_path));
} else {
var args = Array.prototype.slice.call(arguments);
args.unshift(process.cwd());
return path.resolve.apply(null, args);
}
};
if (all_js && !program.autoInject) { if (all_js && !program.autoInject) {
var all_modules = {}; var all_modules = {};
@ -42,7 +66,17 @@ if (all_js && !program.autoInject) {
var eol = content.indexOf('\n', ind); var eol = content.indexOf('\n', ind);
var modules = content.slice(ind, eol).split(/,\s*/); var modules = content.slice(ind, eol).split(/,\s*/);
modules.forEach(function (mod) { modules.forEach(function (mod) {
all_modules[path.resolve(__dirname, '../include/', mod)+'.js'] = 1; all_modules[get_path('include/', mod) + '.js'] = 1;
});
}
var fakes_ind = content.indexOf('requires test modules: ');
if (fakes_ind > -1) {
fakes_ind += 'requires test modules: '.length;
var fakes_eol = content.indexOf('\n', fakes_ind);
var fakes_modules = content.slice(fakes_ind, fakes_eol).split(/,\s*/);
fakes_modules.forEach(function (mod) {
all_modules[get_path('tests/', mod) + '.js'] = 1;
}); });
} }
}); });
@ -55,26 +89,27 @@ if (program.autoInject) {
temp.track(); temp.track();
var template = { 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='" + get_path('node_modules/mocha/mocha.css') + "'/>\n</head>\n<body><div id='mocha'></div>",
script_tag: function(p) { return "<script src='" + p + "'></script>"; }, script_tag: function(p) { return "<script src='" + p + "'></script>"; },
footer: "<script>\nmocha.checkLeaks();\nmocha.globals(['navigator', 'create', 'ClientUtils', '__utils__']);\nmocha.run();\n</script>\n</body>\n</html>" 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(get_path('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(get_path('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(get_path('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(get_path('node_modules/sinon-chai/lib/sinon-chai.js'));
template.header += "\n" + template.script_tag(get_path('node_modules/sinon-chai/lib/sinon-chai.js'));
template.header += "\n<script>mocha.setup('bdd');</script>"; template.header += "\n<script>mocha.setup('bdd');</script>";
template.header = program.autoInject.reduce(function(acc, sn) { template.header = program.autoInject.reduce(function(acc, sn) {
return acc + "\n" + template.script_tag(path.resolve(process.cwd(), sn)); return acc + "\n" + template.script_tag(get_path_cwd(sn));
}, template.header); }, template.header);
file_paths = program.tests.map(function(jsn, ind) { file_paths = program.tests.map(function(jsn, ind) {
var templ = template.header; var templ = template.header;
templ += "\n"; templ += "\n";
templ += template.script_tag(path.resolve(process.cwd(), jsn)); templ += template.script_tag(get_path_cwd(jsn));
templ += template.footer; templ += template.footer;
var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' }); var tempfile = temp.openSync({ prefix: 'novnc-zombie-inject-', suffix: '-file_num-'+ind+'.html' });
@ -105,20 +140,29 @@ if (program.outputHtml) {
return; return;
} }
if (use_ansi) {
cursor cursor
.bold() .bold()
.write(program.tests[path_ind]) .write(program.tests[path_ind])
.reset() .reset()
.write("\n") .write("\n")
.write(Array(program.tests[path_ind].length+1).join('=')) .write(Array(program.tests[path_ind].length+1).join('='))
.write("\n\n") .write("\n\n");
}
cursor
.write(data) .write(data)
.write("\n"); .write("\n\n");
}); });
}); });
} }
if (program.generateHtml) { if (program.generateHtml) {
var open_browser;
if (program.openInBrowser) {
open_browser = require('open');
}
file_paths.forEach(function(path, path_ind) { file_paths.forEach(function(path, path_ind) {
cursor cursor
.bold() .bold()
@ -127,6 +171,10 @@ if (program.generateHtml) {
.reset() .reset()
.write(path) .write(path)
.write("\n"); .write("\n");
if (program.openInBrowser) {
open_browser(path);
}
}); });
console.log(''); console.log('');
} }
@ -276,10 +324,16 @@ if (!program.outputHtml && !program.generateHtml) {
if (program.debug) { if (program.debug) {
provider.on('console', function(line) { provider.on('console', function(line) {
console.log(line); // log to stderr
console.error(line);
}); });
} }
provider.on('error', function(line) {
// log to stderr
console.error('ERROR: ' + line);
});
/*gprom.finally(function(ph) { /*gprom.finally(function(ph) {
ph.exit(); ph.exit();
// exit with a status code that actually gives information // exit with a status code that actually gives information

View file

@ -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);
});
});
});

353
noVNC/tests/test.display.js Normal file
View file

@ -0,0 +1,353 @@
// requires local modules: util, base64, display
// requires test modules: assertions
/* jshint expr: true */
var expect = chai.expect;
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('checking for cursor uri support', function () {
beforeEach(function () {
this._old_change_cursor = Display.changeCursor;
});
it('should disable cursor URIs if there is no support', function () {
Display.changeCursor = function(target) {
target.style.cursor = undefined;
};
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false });
expect(display._cursor_uri).to.be.false;
});
it('should enable cursor URIs if there is support', function () {
Display.changeCursor = function(target) {
target.style.cursor = 'pointer';
};
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false });
expect(display._cursor_uri).to.be.true;
});
it('respect the cursor_uri option if there is support', function () {
Display.changeCursor = function(target) {
target.style.cursor = 'pointer';
};
var display = new Display({ target: document.createElement('canvas'), prefer_js: true, viewport: false, cursor_uri: false });
expect(display._cursor_uri).to.be.false;
});
afterEach(function () {
Display.changeCursor = this._old_change_cursor;
});
});
describe('viewport handling', function () {
var display;
beforeEach(function () {
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);
});
});
});

View file

@ -1,4 +1,6 @@
var assert = chai.assert; // requires local modules: keysym, keysymdef, keyboard
var assert = chai.assert;
var expect = chai.expect; var expect = chai.expect;
describe('Helpers', function() { describe('Helpers', function() {

View file

@ -1,7 +1,8 @@
// requires local modules: input, keyboard, keysymdef
var assert = chai.assert; var assert = chai.assert;
var expect = chai.expect; var expect = chai.expect;
/* jshint newcap: false, expr: true */
describe('Key Event Pipeline Stages', function() { describe('Key Event Pipeline Stages', function() {
"use strict"; "use strict";
describe('Decode Keyboard Events', function() { describe('Decode Keyboard Events', function() {
@ -50,7 +51,7 @@ describe('Key Event Pipeline Stages', function() {
KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'}); expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
done(); done();
}).keydown({keyCode: 0x41}) }).keydown({keyCode: 0x41});
}); });
it('should forward keyup events with the right type', function(done) { it('should forward keyup events with the right type', function(done) {
KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) { KeyEventDecoder(kbdUtil.ModifierSync(), function(evt) {

1716
noVNC/tests/test.rfb.js Normal file

File diff suppressed because it is too large Load diff

105
noVNC/tests/test.util.js Normal file
View file

@ -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
});

480
noVNC/tests/test.websock.js Normal file
View file

@ -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]);
});
});
});
});

View file

@ -202,7 +202,7 @@
dbgmsg(" " + enc + ": " + VNC_frame_data_multi[enc].length); dbgmsg(" " + enc + ": " + VNC_frame_data_multi[enc].length);
} }
rfb = new RFB({'target': $D('VNC_canvas'), rfb = new RFB({'target': $D('VNC_canvas'),
'updateState': updateState}); 'onUpdateState': updateState});
rfb.testMode(send_array, VNC_frame_encoding); rfb.testMode(send_array, VNC_frame_encoding);
} }
</script> </script>

View file

@ -131,7 +131,7 @@
if (fname) { if (fname) {
message("VNC_frame_data.length: " + VNC_frame_data.length); message("VNC_frame_data.length: " + VNC_frame_data.length);
rfb = new RFB({'target': $D('VNC_canvas'), rfb = new RFB({'target': $D('VNC_canvas'),
'updateState': updateState}); 'onUpdateState': updateState});
} }
} }
</script> </script>

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -11,7 +11,11 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
''' '''
import signal, socket, optparse, time, os, sys, subprocess import signal, socket, optparse, time, os, sys, subprocess, logging
try: from socketserver import ForkingMixIn
except: from SocketServer import ForkingMixIn
try: from http.server import HTTPServer
except: from BaseHTTPServer import HTTPServer
from select import select from select import select
import websocket import websocket
try: try:
@ -20,15 +24,7 @@ except:
from cgi import parse_qs from cgi import parse_qs
from urlparse import urlparse from urlparse import urlparse
class WebSocketProxy(websocket.WebSocketServer): class ProxyRequestHandler(websocket.WebSocketRequestHandler):
"""
Proxy traffic to and from a WebSockets client to a normal TCP
socket server target. All traffic to/from the client is base64
encoded/decoded to allow binary data to be sent/received to/from
the target.
"""
buffer_size = 65536
traffic_legend = """ traffic_legend = """
Traffic Legend: Traffic Legend:
@ -42,148 +38,33 @@ Traffic Legend:
<. - Client send partial <. - Client send partial
""" """
def __init__(self, *args, **kwargs): def new_websocket_client(self):
# Save off proxy specific options
self.target_host = kwargs.pop('target_host', None)
self.target_port = kwargs.pop('target_port', None)
self.wrap_cmd = kwargs.pop('wrap_cmd', None)
self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None)
# Last 3 timestamps command was run
self.wrap_times = [0, 0, 0]
if self.wrap_cmd:
rebinder_path = ['./', os.path.dirname(sys.argv[0])]
self.rebinder = None
for rdir in rebinder_path:
rpath = os.path.join(rdir, "rebind.so")
if os.path.exists(rpath):
self.rebinder = rpath
break
if not self.rebinder:
raise Exception("rebind.so not found, perhaps you need to run make")
self.rebinder = os.path.abspath(self.rebinder)
self.target_host = "127.0.0.1" # Loopback
# Find a free high port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 0))
self.target_port = sock.getsockname()[1]
sock.close()
os.environ.update({
"LD_PRELOAD": self.rebinder,
"REBIND_OLD_PORT": str(kwargs['listen_port']),
"REBIND_NEW_PORT": str(self.target_port)})
if self.target_cfg:
self.target_cfg = os.path.abspath(self.target_cfg)
websocket.WebSocketServer.__init__(self, *args, **kwargs)
def run_wrap_cmd(self):
print("Starting '%s'" % " ".join(self.wrap_cmd))
self.wrap_times.append(time.time())
self.wrap_times.pop(0)
self.cmd = subprocess.Popen(
self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup)
self.spawn_message = True
def started(self):
"""
Called after Websockets server startup (i.e. after daemonize)
"""
# Need to call wrapped command after daemonization so we can
# know when the wrapped command exits
if self.wrap_cmd:
dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
elif self.unix_target:
dst_string = self.unix_target
else:
dst_string = "%s:%s" % (self.target_host, self.target_port)
if self.target_cfg:
msg = " - proxying from %s:%s to targets in %s" % (
self.listen_host, self.listen_port, self.target_cfg)
else:
msg = " - proxying from %s:%s to %s" % (
self.listen_host, self.listen_port, dst_string)
if self.ssl_target:
msg += " (using SSL)"
print(msg + "\n")
if self.wrap_cmd:
self.run_wrap_cmd()
def poll(self):
# If we are wrapping a command, check it's status
if self.wrap_cmd and self.cmd:
ret = self.cmd.poll()
if ret != None:
self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret)
self.cmd = None
if self.wrap_cmd and self.cmd == None:
# Response to wrapped command being gone
if self.wrap_mode == "ignore":
pass
elif self.wrap_mode == "exit":
sys.exit(ret)
elif self.wrap_mode == "respawn":
now = time.time()
avg = sum(self.wrap_times)/len(self.wrap_times)
if (now - avg) < 10:
# 3 times in the last 10 seconds
if self.spawn_message:
print("Command respawning too fast")
self.spawn_message = False
else:
self.run_wrap_cmd()
#
# Routines above this point are run in the master listener
# process.
#
#
# Routines below this point are connection handler routines and
# will be run in a separate forked process for each connection.
#
def new_client(self):
""" """
Called after a new WebSocket connection has been established. Called after a new WebSocket connection has been established.
""" """
# Checks if we receive a token, and look # Checks if we receive a token, and look
# for a valid target for it then # for a valid target for it then
if self.target_cfg: if self.server.target_cfg:
(self.target_host, self.target_port) = self.get_target(self.target_cfg, self.path) (self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path)
# Connect to the target # Connect to the target
if self.wrap_cmd: if self.server.wrap_cmd:
msg = "connecting to command: '%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port)
elif self.unix_target: elif self.server.unix_target:
msg = "connecting to unix socket: %s" % self.unix_target msg = "connecting to unix socket: %s" % self.server.unix_target
else: else:
msg = "connecting to: %s:%s" % ( msg = "connecting to: %s:%s" % (
self.target_host, self.target_port) self.server.target_host, self.server.target_port)
if self.ssl_target: if self.server.ssl_target:
msg += " (using SSL)" msg += " (using SSL)"
self.msg(msg) self.log_message(msg)
tsock = self.socket(self.target_host, self.target_port, tsock = websocket.WebSocketServer.socket(self.server.target_host,
connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target) self.server.target_port,
connect=True, use_ssl=self.server.ssl_target, unix_socket=self.server.unix_target)
if self.verbose and not self.daemon: self.print_traffic(self.traffic_legend)
print(self.traffic_legend)
# Start proxying # Start proxying
try: try:
@ -192,8 +73,9 @@ Traffic Legend:
if tsock: if tsock:
tsock.shutdown(socket.SHUT_RDWR) tsock.shutdown(socket.SHUT_RDWR)
tsock.close() tsock.close()
self.vmsg("%s:%s: Closed target" %( if self.verbose:
self.target_host, self.target_port)) self.log_message("%s:%s: Closed target",
self.server.target_host, self.server.target_port)
raise raise
def get_target(self, target_cfg, path): def get_target(self, target_cfg, path):
@ -242,31 +124,32 @@ Traffic Legend:
cqueue = [] cqueue = []
c_pend = 0 c_pend = 0
tqueue = [] tqueue = []
rlist = [self.client, target] rlist = [self.request, target]
while True: while True:
wlist = [] wlist = []
if tqueue: wlist.append(target) if tqueue: wlist.append(target)
if cqueue or c_pend: wlist.append(self.client) if cqueue or c_pend: wlist.append(self.request)
ins, outs, excepts = select(rlist, wlist, [], 1) ins, outs, excepts = select(rlist, wlist, [], 1)
if excepts: raise Exception("Socket exception") if excepts: raise Exception("Socket exception")
if self.client in outs: if self.request in outs:
# Send queued target data to the client # Send queued target data to the client
c_pend = self.send_frames(cqueue) c_pend = self.send_frames(cqueue)
cqueue = [] cqueue = []
if self.client in ins: if self.request in ins:
# Receive client data, decode it, and queue for target # Receive client data, decode it, and queue for target
bufs, closed = self.recv_frames() bufs, closed = self.recv_frames()
tqueue.extend(bufs) tqueue.extend(bufs)
if closed: if closed:
# TODO: What about blocking on client socket? # TODO: What about blocking on client socket?
self.vmsg("%s:%s: Client closed connection" %( if self.verbose:
self.target_host, self.target_port)) self.log_message("%s:%s: Client closed connection",
self.server.target_host, self.server.target_port)
raise self.CClose(closed['code'], closed['reason']) raise self.CClose(closed['code'], closed['reason'])
@ -275,24 +158,139 @@ Traffic Legend:
dat = tqueue.pop(0) dat = tqueue.pop(0)
sent = target.send(dat) sent = target.send(dat)
if sent == len(dat): if sent == len(dat):
self.traffic(">") self.print_traffic(">")
else: else:
# requeue the remaining data # requeue the remaining data
tqueue.insert(0, dat[sent:]) tqueue.insert(0, dat[sent:])
self.traffic(".>") self.print_traffic(".>")
if target in ins: if target in ins:
# Receive target data, encode it and queue for client # Receive target data, encode it and queue for client
buf = target.recv(self.buffer_size) buf = target.recv(self.buffer_size)
if len(buf) == 0: if len(buf) == 0:
self.vmsg("%s:%s: Target closed connection" %( if self.verbose:
self.target_host, self.target_port)) self.log_message("%s:%s: Target closed connection",
self.server.target_host, self.server.target_port)
raise self.CClose(1000, "Target closed") raise self.CClose(1000, "Target closed")
cqueue.append(buf) cqueue.append(buf)
self.traffic("{") self.print_traffic("{")
class WebSocketProxy(websocket.WebSocketServer):
"""
Proxy traffic to and from a WebSockets client to a normal TCP
socket server target. All traffic to/from the client is base64
encoded/decoded to allow binary data to be sent/received to/from
the target.
"""
buffer_size = 65536
def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs):
# Save off proxy specific options
self.target_host = kwargs.pop('target_host', None)
self.target_port = kwargs.pop('target_port', None)
self.wrap_cmd = kwargs.pop('wrap_cmd', None)
self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None)
# Last 3 timestamps command was run
self.wrap_times = [0, 0, 0]
if self.wrap_cmd:
wsdir = os.path.dirname(sys.argv[0])
rebinder_path = [os.path.join(wsdir, "..", "lib"),
os.path.join(wsdir, "..", "lib", "websockify"),
wsdir]
self.rebinder = None
for rdir in rebinder_path:
rpath = os.path.join(rdir, "rebind.so")
if os.path.exists(rpath):
self.rebinder = rpath
break
if not self.rebinder:
raise Exception("rebind.so not found, perhaps you need to run make")
self.rebinder = os.path.abspath(self.rebinder)
self.target_host = "127.0.0.1" # Loopback
# Find a free high port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 0))
self.target_port = sock.getsockname()[1]
sock.close()
os.environ.update({
"LD_PRELOAD": self.rebinder,
"REBIND_OLD_PORT": str(kwargs['listen_port']),
"REBIND_NEW_PORT": str(self.target_port)})
websocket.WebSocketServer.__init__(self, RequestHandlerClass, *args, **kwargs)
def run_wrap_cmd(self):
self.msg("Starting '%s'", " ".join(self.wrap_cmd))
self.wrap_times.append(time.time())
self.wrap_times.pop(0)
self.cmd = subprocess.Popen(
self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup)
self.spawn_message = True
def started(self):
"""
Called after Websockets server startup (i.e. after daemonize)
"""
# Need to call wrapped command after daemonization so we can
# know when the wrapped command exits
if self.wrap_cmd:
dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port)
elif self.unix_target:
dst_string = self.unix_target
else:
dst_string = "%s:%s" % (self.target_host, self.target_port)
if self.target_cfg:
msg = " - proxying from %s:%s to targets in %s" % (
self.listen_host, self.listen_port, self.target_cfg)
else:
msg = " - proxying from %s:%s to %s" % (
self.listen_host, self.listen_port, dst_string)
if self.ssl_target:
msg += " (using SSL)"
self.msg("%s", msg)
if self.wrap_cmd:
self.run_wrap_cmd()
def poll(self):
# If we are wrapping a command, check it's status
if self.wrap_cmd and self.cmd:
ret = self.cmd.poll()
if ret != None:
self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret)
self.cmd = None
if self.wrap_cmd and self.cmd == None:
# Response to wrapped command being gone
if self.wrap_mode == "ignore":
pass
elif self.wrap_mode == "exit":
sys.exit(ret)
elif self.wrap_mode == "respawn":
now = time.time()
avg = sum(self.wrap_times)/len(self.wrap_times)
if (now - avg) < 10:
# 3 times in the last 10 seconds
if self.spawn_message:
self.warn("Command respawning too fast")
self.spawn_message = False
else:
self.run_wrap_cmd()
def _subprocess_setup(): def _subprocess_setup():
@ -301,14 +299,28 @@ def _subprocess_setup():
signal.signal(signal.SIGPIPE, signal.SIG_DFL) signal.signal(signal.SIGPIPE, signal.SIG_DFL)
def logger_init():
logger = logging.getLogger(WebSocketProxy.log_prefix)
logger.propagate = False
logger.setLevel(logging.INFO)
h = logging.StreamHandler()
h.setLevel(logging.DEBUG)
h.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(h)
def websockify_init(): def websockify_init():
logger_init()
usage = "\n %prog [options]" usage = "\n %prog [options]"
usage += " [source_addr:]source_port [target_addr:target_port]" usage += " [source_addr:]source_port [target_addr:target_port]"
usage += "\n %prog [options]" usage += "\n %prog [options]"
usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE" usage += " [source_addr:]source_port -- WRAP_COMMAND_LINE"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
parser.add_option("--verbose", "-v", action="store_true", parser.add_option("--verbose", "-v", action="store_true",
help="verbose messages and per frame traffic") help="verbose messages")
parser.add_option("--traffic", action="store_true",
help="per frame traffic")
parser.add_option("--record", parser.add_option("--record",
help="record sessions to FILE.[session_number]", metavar="FILE") help="record sessions to FILE.[session_number]", metavar="FILE")
parser.add_option("--daemon", "-D", parser.add_option("--daemon", "-D",
@ -345,8 +357,13 @@ def websockify_init():
help="Configuration file containing valid targets " help="Configuration file containing valid targets "
"in the form 'token: host:port' or, alternatively, a " "in the form 'token: host:port' or, alternatively, a "
"directory containing configuration files of this form") "directory containing configuration files of this form")
parser.add_option("--libserver", action="store_true",
help="use Python library SocketServer engine")
(opts, args) = parser.parse_args() (opts, args) = parser.parse_args()
if opts.verbose:
logging.getLogger(WebSocketProxy.log_prefix).setLevel(logging.DEBUG)
# Sanity checks # Sanity checks
if len(args) < 2 and not (opts.target_cfg or opts.unix_target): if len(args) < 2 and not (opts.target_cfg or opts.unix_target):
parser.error("Too few arguments") parser.error("Too few arguments")
@ -385,9 +402,70 @@ def websockify_init():
try: opts.target_port = int(opts.target_port) try: opts.target_port = int(opts.target_port)
except: parser.error("Error parsing target port") except: parser.error("Error parsing target port")
# Transform to absolute path as daemon may chdir
if opts.target_cfg:
opts.target_cfg = os.path.abspath(opts.target_cfg)
# Create and start the WebSockets proxy # Create and start the WebSockets proxy
libserver = opts.libserver
del opts.libserver
if libserver:
# Use standard Python SocketServer framework
server = LibProxyServer(**opts.__dict__)
server.serve_forever()
else:
# Use internal service framework
server = WebSocketProxy(**opts.__dict__) server = WebSocketProxy(**opts.__dict__)
server.start_server() server.start_server()
class LibProxyServer(ForkingMixIn, HTTPServer):
"""
Just like WebSocketProxy, but uses standard Python SocketServer
framework.
"""
def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs):
# Save off proxy specific options
self.target_host = kwargs.pop('target_host', None)
self.target_port = kwargs.pop('target_port', None)
self.wrap_cmd = kwargs.pop('wrap_cmd', None)
self.wrap_mode = kwargs.pop('wrap_mode', None)
self.unix_target = kwargs.pop('unix_target', None)
self.ssl_target = kwargs.pop('ssl_target', None)
self.target_cfg = kwargs.pop('target_cfg', None)
self.daemon = False
self.target_cfg = None
# Server configuration
listen_host = kwargs.pop('listen_host', '')
listen_port = kwargs.pop('listen_port', None)
web = kwargs.pop('web', '')
# Configuration affecting base request handler
self.only_upgrade = not web
self.verbose = kwargs.pop('verbose', False)
record = kwargs.pop('record', '')
if record:
self.record = os.path.abspath(record)
self.run_once = kwargs.pop('run_once', False)
self.handler_id = 0
for arg in kwargs.keys():
print("warning: option %s ignored when using --libserver" % arg)
if web:
os.chdir(web)
HTTPServer.__init__(self, (listen_host, listen_port),
RequestHandlerClass)
def process_request(self, request, client_address):
"""Override process_request to implement a counter"""
self.handler_id += 1
ForkingMixIn.process_request(self, request, client_address)
if __name__ == '__main__': if __name__ == '__main__':
websockify_init() websockify_init()

View file

@ -47,43 +47,42 @@
<body> <body>
<div id="noVNC-control-bar"> <div id="noVNC-control-bar">
<div id="noVNC-menu-bar" style="display:none;">
</div>
<!--noVNC Mobile Device only Buttons--> <!--noVNC Mobile Device only Buttons-->
<div class="noVNC-buttons-left"> <div class="noVNC-buttons-left">
<input type="image" src="images/drag.png" <input type="image" alt="viewport drag" src="images/drag.png"
id="noVNC_view_drag_button" class="noVNC_status_button" id="noVNC_view_drag_button" class="noVNC_status_button"
title="Move/Drag Viewport"> title="Move/Drag Viewport">
<div id="noVNC_mobile_buttons"> <div id="noVNC_mobile_buttons">
<input type="image" src="images/mouse_none.png" <input type="image" alt="No mousebutton" src="images/mouse_none.png"
id="noVNC_mouse_button0" class="noVNC_status_button"> id="noVNC_mouse_button0" class="noVNC_status_button">
<input type="image" src="images/mouse_left.png" <input type="image" alt="Left mousebutton" src="images/mouse_left.png"
id="noVNC_mouse_button1" class="noVNC_status_button"> id="noVNC_mouse_button1" class="noVNC_status_button">
<input type="image" src="images/mouse_middle.png" <input type="image" alt="Middle mousebutton" src="images/mouse_middle.png"
id="noVNC_mouse_button2" class="noVNC_status_button"> id="noVNC_mouse_button2" class="noVNC_status_button">
<input type="image" src="images/mouse_right.png" <input type="image" alt="Right mousebutton" src="images/mouse_right.png"
id="noVNC_mouse_button4" class="noVNC_status_button"> id="noVNC_mouse_button4" class="noVNC_status_button">
<input type="image" src="images/keyboard.png" <input type="image" alt="Keyboard" src="images/keyboard.png"
id="showKeyboard" class="noVNC_status_button" id="showKeyboard" class="noVNC_status_button"
value="Keyboard" title="Show Keyboard"/> value="Keyboard" title="Show Keyboard"/>
<input type="text" autocapitalize="off" autocorrect="off" <!-- Note that Google Chrome on Android doesn't respect any of these,
id="keyboardinput" class=""/> html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="keyboardinput" autocapitalize="off"
autocorrect="off" autocomplete="off" spellcheck="false"
mozactionhint="Enter" onsubmit="return false;"
style="ime-mode: disabled;"></textarea>
<div id="noVNC_extra_keys"> <div id="noVNC_extra_keys">
<input type="image" src="images/showextrakeys.png" <input type="image" alt="Extra keys" src="images/showextrakeys.png"
id="showExtraKeysButton" id="showExtraKeysButton" class="noVNC_status_button">
class="noVNC_status_button"> <input type="image" alt="Ctrl" src="images/ctrl.png"
<input type="image" src="images/ctrl.png" id="toggleCtrlButton" class="noVNC_status_button">
id="toggleCtrlButton" <input type="image" alt="Alt" src="images/alt.png"
class="noVNC_status_button"> id="toggleAltButton" class="noVNC_status_button">
<input type="image" src="images/alt.png" <input type="image" alt="Tab" src="images/tab.png"
id="toggleAltButton" id="sendTabButton" class="noVNC_status_button">
class="noVNC_status_button"> <input type="image" alt="Esc" src="images/esc.png"
<input type="image" src="images/tab.png" id="sendEscButton" class="noVNC_status_button">
id="sendTabButton"
class="noVNC_status_button">
<input type="image" src="images/esc.png"
id="sendEscButton"
class="noVNC_status_button">
</div> </div>
</div> </div>
</div> </div>
@ -92,29 +91,29 @@
<!--noVNC Buttons--> <!--noVNC Buttons-->
<div class="noVNC-buttons-right"> <div class="noVNC-buttons-right">
<input type="image" src="images/ctrlaltdel.png" <input type="image" alt="Ctrl+Alt+Del" src="images/ctrlaltdel.png"
id="sendCtrlAltDelButton" class="noVNC_status_button" id="sendCtrlAltDelButton" class="noVNC_status_button"
title="Send Ctrl-Alt-Del" /> title="Send Ctrl-Alt-Del" />
<input type="image" src="images/power.png" <input type="image" alt="Shutdown/Reboot" src="images/power.png"
id="xvpButton" class="noVNC_status_button" id="xvpButton" class="noVNC_status_button"
title="Shutdown/Reboot..." /> title="Shutdown/Reboot..." />
<input type="image" src="images/clipboard.png" <input type="image" alt="Clipboard" src="images/clipboard.png"
id="clipboardButton" class="noVNC_status_button" id="clipboardButton" class="noVNC_status_button"
title="Clipboard" /> title="Clipboard" />
<input type="image" src="images/settings.png" <input type="image" alt="Settings" src="images/settings.png"
id="settingsButton" class="noVNC_status_button" id="settingsButton" class="noVNC_status_button"
title="Settings" /> title="Settings" />
<input type="image" src="images/connect.png" <input type="image" alt="Connect" src="images/connect.png"
id="connectButton" class="noVNC_status_button" id="connectButton" class="noVNC_status_button"
title="Connect" /> title="Connect" />
<input type="image" src="images/disconnect.png" <input type="image" alt="Disconnect" src="images/disconnect.png"
id="disconnectButton" class="noVNC_status_button" id="disconnectButton" class="noVNC_status_button"
title="Disconnect" /> title="Disconnect" />
</div> </div>
<!-- Description Panel --> <!-- Description Panel -->
<!-- Shown by default when hosted at for kanaka.github.com --> <!-- Shown by default when hosted at for kanaka.github.com -->
<div id="noVNC_description" style="display:none;" class=""> <div id="noVNC_description" class="">
noVNC is a browser based VNC client implemented using HTML5 Canvas noVNC is a browser based VNC client implemented using HTML5 Canvas
and WebSockets. You will either need a VNC server with WebSockets and WebSockets. You will either need a VNC server with WebSockets
support (such as <a href="http://libvncserver.sourceforge.net/">libvncserver</a>) support (such as <a href="http://libvncserver.sourceforge.net/">libvncserver</a>)

View file

@ -77,7 +77,7 @@
// Load supporting scripts // Load supporting scripts
Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js", Util.load_scripts(["webutil.js", "base64.js", "websock.js", "des.js",
"keysymdef.js", "keyboard.js", "input.js", "display.js", "keysymdef.js", "keyboard.js", "input.js", "display.js",
"jsunzip.js", "rfb.js"]); "jsunzip.js", "rfb.js", "keysym.js"]);
var rfb; var rfb;
@ -198,7 +198,7 @@
'local_cursor': WebUtil.getQueryVar('cursor', true), 'local_cursor': WebUtil.getQueryVar('cursor', true),
'shared': WebUtil.getQueryVar('shared', true), 'shared': WebUtil.getQueryVar('shared', true),
'view_only': WebUtil.getQueryVar('view_only', false), 'view_only': WebUtil.getQueryVar('view_only', false),
'updateState': updateState, 'onUpdateState': updateState,
'onXvpInit': xvpInit, 'onXvpInit': xvpInit,
'onPasswordRequired': passwordRequired}); 'onPasswordRequired': passwordRequired});
rfb.connect(host, port, password, path); rfb.connect(host, port, password, path);