Commit 38781d93 authored by Solly Ross's avatar Solly Ross

Use Typed Arrays for the Websock receive queue

**This commit removes Base64 (and Flash) support**

This commit converts websock.js to used Typed Arrays for the
receive queue (and tweaks rfb.js to ensure that it continues
to function, since only Firefox implements
`%TypedArray%.prototype.slice`).  Base64 support was removed
to simplify code paths, and pave the way for using Typed Arrays
for the send queue as well.

This provides two advantages: first, we allocate a buffer ahead
of time, meaning the browser doesn't have to do any work dynamically
increasing the receive queue size.  Secondly, we are now able to pass
around Typed Array Views (e.g. `Uint8Array`), which are lightweight, and
don't involve copying.

The downside is that we initially allocate more memory -- we currently
start out with 4 MiB, and then automatically double when it looks like
the amount unused is getting to small.

The commit also explicitly adds a check to the compacting logic that
avoids calling the copy functions if `_rQlen === _rQi`.
parent 6c883653
[submodule "include/web-socket-js-project"]
path = include/web-socket-js-project
url = https://github.com/gimite/web-socket-js.git
...@@ -51,9 +51,6 @@ licenses (all MPL 2.0 compatible): ...@@ -51,9 +51,6 @@ licenses (all MPL 2.0 compatible):
include/jsunzip.js : zlib/libpng license include/jsunzip.js : zlib/libpng license
include/web-socket-js/ : New BSD license (3-clause). Source code at
http://github.com/gimite/web-socket-js
include/chrome-app/tcp-stream.js include/chrome-app/tcp-stream.js
: Apache 2.0 license : Apache 2.0 license
......
...@@ -69,11 +69,7 @@ See more screenshots <a href="http://kanaka.github.com/noVNC/screenshots.html">h ...@@ -69,11 +69,7 @@ See more screenshots <a href="http://kanaka.github.com/noVNC/screenshots.html">h
* HTML5 Canvas (with createImageData): Chrome, Firefox 3.6+, iOS * HTML5 Canvas (with createImageData): Chrome, Firefox 3.6+, iOS
Safari, Opera 11+, Internet Explorer 9+, etc. Safari, Opera 11+, Internet Explorer 9+, etc.
* HTML5 WebSockets: For browsers that do not have builtin * HTML5 WebSockets and Typed Arrays
WebSockets support, the project includes
<a href="http://github.com/gimite/web-socket-js">web-socket-js</a>,
a WebSockets emulator using Adobe Flash. iOS 4.2+ has built-in
WebSocket support.
* Fast Javascript Engine: this is not strictly a requirement, but * Fast Javascript Engine: this is not strictly a requirement, but
without a fast Javascript engine, noVNC might be painfully slow. without a fast Javascript engine, noVNC might be painfully slow.
...@@ -130,7 +126,6 @@ use a WebSockets to TCP socket proxy. There is a python proxy included ...@@ -130,7 +126,6 @@ use a WebSockets to TCP socket proxy. There is a python proxy included
* tight encoding : Michael Tinglof (Mercuri.ca) * tight encoding : Michael Tinglof (Mercuri.ca)
* Included libraries: * Included libraries:
* web-socket-js : Hiroshi Ichikawa (github.com/gimite/web-socket-js)
* as3crypto : Henri Torgemane (code.google.com/p/as3crypto) * as3crypto : Henri Torgemane (code.google.com/p/as3crypto)
* base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net) * base64 : Martijn Pieters (Digital Creations 2), Samuel Sieb (sieb.net)
* DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs) * DES : Dave Zimmerman (Widget Workshop), Jef Poskanzer (ACME Labs)
......
Some implementation notes:
There is an included flash object (web-socket-js) that is used to
emulate websocket support on browsers without websocket support
(currently only Chrome has WebSocket support).
Javascript doesn't have a bytearray type, so what you get out of
a WebSocket object is just Javascript strings. Javascript has UTF-16
unicode strings and anything sent through the WebSocket gets converted
to UTF-8 and vice-versa. So, one additional (and necessary) function
of websockify is base64 encoding/decoding what is sent to/from the
browser.
Building web-socket-js emulator:
cd include/web-socket-js/flash-src
mxmlc -static-link-runtime-shared-libraries WebSocketMain.as
Rebuilding inflator.js Rebuilding inflator.js
- Download pako from npm - Download pako from npm
......
...@@ -30,6 +30,7 @@ enable_test_mode = function () { ...@@ -30,6 +30,7 @@ enable_test_mode = function () {
this._rfb_port = port; this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : ""; this._rfb_password = (password !== undefined) ? password : "";
this._rfb_path = (path !== undefined) ? path : ""; this._rfb_path = (path !== undefined) ? path : "";
this._sock.init('binary', 'ws');
this._updateState('ProtocolVersion', "Starting VNC handshake"); this._updateState('ProtocolVersion', "Starting VNC handshake");
}; };
}; };
...@@ -43,7 +44,7 @@ next_iteration = function () { ...@@ -43,7 +44,7 @@ next_iteration = function () {
frame_length = VNC_frame_data.length; frame_length = VNC_frame_data.length;
test_state = 'running'; test_state = 'running';
} }
if (test_state !== 'running') { return; } if (test_state !== 'running') { return; }
iteration += 1; iteration += 1;
......
...@@ -129,7 +129,7 @@ var RFB; ...@@ -129,7 +129,7 @@ var RFB;
'view_only': false, // Disable client mouse/keyboard 'view_only': false, // Disable client mouse/keyboard
'xvp_password_sep': '@', // Separator for XVP password fields 'xvp_password_sep': '@', // Separator for XVP password fields
'disconnectTimeout': 3, // Time (s) to wait for disconnection 'disconnectTimeout': 3, // Time (s) to wait for disconnection
'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection
'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'repeaterID': '', // [UltraVNC] RepeaterID to connect to
'viewportDrag': false, // Move the viewport on mouse drags 'viewportDrag': false, // Move the viewport on mouse drags
...@@ -218,16 +218,8 @@ var RFB; ...@@ -218,16 +218,8 @@ var RFB;
Util.Info("Using native WebSockets"); Util.Info("Using native WebSockets");
this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode); this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
} else { } else {
Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version); this._cleanupSocket('fatal');
if (!Util.Flash || Util.Flash.version < 9) { throw new Error("WebSocket support is required to use noVNC");
this._cleanupSocket('fatal');
throw new Exception("WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> is required");
} else if (document.location.href.substr(0, 7) === 'file://') {
this._cleanupSocket('fatal');
throw new Exception("'file://' URL is incompatible with Adobe Flash");
} else {
this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
}
} }
Util.Debug("<< RFB.constructor"); Util.Debug("<< RFB.constructor");
...@@ -363,8 +355,6 @@ var RFB; ...@@ -363,8 +355,6 @@ var RFB;
_init_vars: function () { _init_vars: function () {
// reset state // reset state
this._sock.init();
this._FBU.rects = 0; this._FBU.rects = 0;
this._FBU.subrects = 0; // RRE and HEXTILE this._FBU.subrects = 0; // RRE and HEXTILE
this._FBU.lines = 0; // RAW this._FBU.lines = 0; // RAW
...@@ -760,7 +750,8 @@ var RFB; ...@@ -760,7 +750,8 @@ var RFB;
if (this._sock.rQwait("auth challenge", 16)) { return false; } if (this._sock.rQwait("auth challenge", 16)) { return false; }
var challenge = this._sock.rQshiftBytes(16); // TODO(directxman12): make genDES not require an Array
var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
var response = RFB.genDES(this._rfb_password, challenge); var response = RFB.genDES(this._rfb_password, challenge);
this._sock.send(response); this._sock.send(response);
this._updateState("SecurityResult"); this._updateState("SecurityResult");
...@@ -1559,11 +1550,21 @@ var RFB; ...@@ -1559,11 +1550,21 @@ var RFB;
rQi += this._FBU.bytes - 1; rQi += this._FBU.bytes - 1;
} else { } else {
if (this._FBU.subencoding & 0x02) { // Background if (this._FBU.subencoding & 0x02) { // Background
this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp); if (this._fb_Bpp == 1) {
this._FBU.background = rQ[rQi];
} else {
// fb_Bpp is 4
this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
}
rQi += this._fb_Bpp; rQi += this._fb_Bpp;
} }
if (this._FBU.subencoding & 0x04) { // Foreground if (this._FBU.subencoding & 0x04) { // Foreground
this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp); if (this._fb_Bpp == 1) {
this._FBU.foreground = rQ[rQi];
} else {
// this._fb_Bpp is 4
this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
}
rQi += this._fb_Bpp; rQi += this._fb_Bpp;
} }
...@@ -1575,7 +1576,12 @@ var RFB; ...@@ -1575,7 +1576,12 @@ var RFB;
for (var s = 0; s < subrects; s++) { for (var s = 0; s < subrects; s++) {
var color; var color;
if (this._FBU.subencoding & 0x10) { // SubrectsColoured if (this._FBU.subencoding & 0x10) { // SubrectsColoured
color = rQ.slice(rQi, rQi + this._fb_Bpp); if (this._fb_Bpp === 1) {
color = rQ[rQi];
} else {
// _fb_Bpp is 4
color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
}
rQi += this._fb_Bpp; rQi += this._fb_Bpp;
} else { } else {
color = this._FBU.foreground; color = this._FBU.foreground;
......
Subproject commit c0855c6caec589c33acc22b6ee5e562287e65f3d
* How to try
Assuming you have Web server (e.g. Apache) running at http://example.com/ .
- Download web_socket.rb from:
http://github.com/gimite/web-socket-ruby/tree/master
- Run sample Web Socket server (echo server) in example.com with: (#1)
$ ruby web-socket-ruby/samples/echo_server.rb example.com 10081
- If your server already provides socket policy file at port 843, modify the file to allow access to port 10081. Otherwise you can skip this step. See below for details.
- Publish the web-socket-js directory with your Web server (e.g. put it in ~/public_html).
- Change ws://localhost:10081 to ws://example.com:10081 in sample.html.
- Open sample.html in your browser.
- After "onopen" is shown, input something, click [Send] and confirm echo back.
#1: First argument of echo_server.rb means that it accepts Web Socket connection from HTML pages in example.com.
* Troubleshooting
If it doesn't work, try these:
1. Try Chrome and Firefox 3.x.
- It doesn't work on Chrome:
-- It's likely an issue of your code or the server. Debug your code as usual e.g. using console.log.
- It works on Chrome but it doesn't work on Firefox:
-- It's likely an issue of web-socket-js specific configuration (e.g. 3 and 4 below).
- It works on both Chrome and Firefox, but it doesn't work on your browser:
-- Check "Supported environment" section below. Your browser may not be supported by web-socket-js.
2. Add this line before your code:
WEB_SOCKET_DEBUG = true;
and use Developer Tools (Chrome/Safari) or Firebug (Firefox) to see if console.log outputs any errors.
3. Make sure you do NOT open your HTML page as local file e.g. file:///.../sample.html. web-socket-js doesn't work on local file. Open it via Web server e.g. http:///.../sample.html.
4. If you are NOT using web-socket-ruby as your WebSocket server, you need to place Flash socket policy file on your server. See "Flash socket policy file" section below for details.
5. Check if sample.html bundled with web-socket-js works.
6. Make sure the port used for WebSocket (10081 in example above) is not blocked by your server/client's firewall.
7. Install debugger version of Flash Player available here to see Flash errors:
http://www.adobe.com/support/flashplayer/downloads.html
* Supported environments
It should work on:
- Google Chrome 4 or later (just uses native implementation)
- Firefox 3.x, Internet Explorer 8 + Flash Player 9 or later
It may or may not work on other browsers such as Safari, Opera or IE 6. Patch for these browsers are appreciated, but I will not work on fixing issues specific to these browsers by myself.
* Flash socket policy file
This implementation uses Flash's socket, which means that your server must provide Flash socket policy file to declare the server accepts connections from Flash.
If you use web-socket-ruby available at
http://github.com/gimite/web-socket-ruby/tree/master
, you don't need anything special, because web-socket-ruby handles Flash socket policy file request. But if you already provide socket policy file at port 843, you need to modify the file to allow access to Web Socket port, because it precedes what web-socket-ruby provides.
If you use other Web Socket server implementation, you need to provide socket policy file yourself. See
http://www.lightsphere.com/dev/articles/flash_socket_policy.html
for details and sample script to run socket policy file server. node.js implementation is available here:
http://github.com/LearnBoost/Socket.IO-node/blob/master/lib/socket.io/transports/flashsocket.js
Actually, it's still better to provide socket policy file at port 843 even if you use web-socket-ruby. Flash always try to connect to port 843 first, so providing the file at port 843 makes startup faster.
* Cookie considerations
Cookie is sent if Web Socket host is the same as the origin of JavaScript. Otherwise it is not sent, because I don't know way to send right Cookie (which is Cookie of the host of Web Socket, I heard).
Note that it's technically possible that client sends arbitrary string as Cookie and any other headers (by modifying this library for example) once you place Flash socket policy file in your server. So don't trust Cookie and other headers if you allow connection from untrusted origin.
* Proxy considerations
The WebSocket spec (http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol) specifies instructions for User Agents to support proxied connections by implementing the HTTP CONNECT method.
The AS3 Socket class doesn't implement this mechanism, which renders it useless for the scenarios where the user trying to open a socket is behind a proxy.
The class RFC2817Socket (by Christian Cantrell) effectively lets us implement this, as long as the proxy settings are known and provided by the interface that instantiates the WebSocket. As such, if you want to support proxied conncetions, you'll have to supply this information to the WebSocket constructor when Flash is being used. One way to go about it would be to ask the user for proxy settings information if the initial connection fails.
* How to host HTML file and SWF file in different domains
By default, HTML file and SWF file must be in the same domain. You can follow steps below to allow hosting them in different domain.
WARNING: If you use the method below, HTML files in ANY domains can send arbitrary TCP data to your WebSocket server, regardless of configuration in Flash socket policy file. Arbitrary TCP data means that they can even fake request headers including Origin and Cookie.
- Unzip WebSocketMainInsecure.zip to extract WebSocketMainInsecure.swf.
- Put WebSocketMainInsecure.swf on your server, instead of WebSocketMain.swf.
- In JavaScript, set WEB_SOCKET_SWF_LOCATION to URL of your WebSocketMainInsecure.swf.
* How to build WebSocketMain.swf
Install Flex 4 SDK:
http://opensource.adobe.com/wiki/display/flexsdk/Download+Flex+4
$ cd flash-src
$ ./build.sh
* License
New BSD License.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -5,7 +5,17 @@ chai.use(function (_chai, utils) { ...@@ -5,7 +5,17 @@ chai.use(function (_chai, utils) {
var data_cl = obj._drawCtx.getImageData(0, 0, obj._viewportLoc.w, obj._viewportLoc.h).data; 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 // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that
var data = new Uint8Array(data_cl); var data = new Uint8Array(data_cl);
this.assert(utils.eql(data, target_data), var same = true;
for (var i = 0; i < obj.length; i++) {
if (data[i] != target_data[i]) {
same = false;
break;
}
}
if (!same) {
console.log("expected data: %o, actual data: %o", target_data, data);
}
this.assert(same,
"expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}",
"expected #{this} not to have displayed the image #{act}", "expected #{this} not to have displayed the image #{act}",
target_data, target_data,
...@@ -15,10 +25,63 @@ chai.use(function (_chai, utils) { ...@@ -15,10 +25,63 @@ chai.use(function (_chai, utils) {
_chai.Assertion.addMethod('sent', function (target_data) { _chai.Assertion.addMethod('sent', function (target_data) {
var obj = this._obj; var obj = this._obj;
var data = obj._websocket._get_sent_data(); var data = obj._websocket._get_sent_data();
this.assert(utils.eql(data, target_data), var same = true;
for (var i = 0; i < obj.length; i++) {
if (data[i] != target_data[i]) {
same = false;
break;
}
}
if (!same) {
console.log("expected data: %o, actual data: %o", target_data, data);
}
this.assert(same,
"expected #{this} to have sent the data #{exp}, but it actually sent #{act}", "expected #{this} to have sent the data #{exp}, but it actually sent #{act}",
"expected #{this} not to have sent the data #{act}", "expected #{this} not to have sent the data #{act}",
target_data, target_data,
data); data);
}); });
_chai.Assertion.addProperty('array', function () {
utils.flag(this, 'array', true);
});
_chai.Assertion.overwriteMethod('equal', function (_super) {
return function assertArrayEqual(target) {
if (utils.flag(this, 'array')) {
var obj = this._obj;
var i;
var same = true;
if (utils.flag(this, 'deep')) {
for (i = 0; i < obj.length; i++) {
if (!utils.eql(obj[i], target[i])) {
same = false;
break;
}
}
this.assert(same,
"expected #{this} to have elements deeply equal to #{exp}",
"expected #{this} not to have elements deeply equal to #{exp}",
Array.prototype.slice.call(target));
} else {
for (i = 0; i < obj.length; i++) {
if (obj[i] != target[i]) {
same = false;
break;
}
}
this.assert(same,
"expected #{this} to have elements equal to #{exp}",
"expected #{this} not to have elements equal to #{exp}",
Array.prototype.slice.call(target));
}
} else {
_super.apply(this, arguments);
}
};
});
}); });
// requires local modules: util, base64, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display // requires local modules: util, websock, rfb, keyboard, keysym, keysymdef, input, inflator, des, display
// requires test modules: fake.websocket, assertions // requires test modules: fake.websocket, assertions
/* jshint expr: true */ /* jshint expr: true */
var assert = chai.assert; var assert = chai.assert;
...@@ -18,6 +18,25 @@ describe('Remote Frame Buffer Protocol Client', function() { ...@@ -18,6 +18,25 @@ describe('Remote Frame Buffer Protocol Client', function() {
before(FakeWebSocket.replace); before(FakeWebSocket.replace);
after(FakeWebSocket.restore); after(FakeWebSocket.restore);
before(function () {
this.clock = sinon.useFakeTimers();
// Use a single set of buffers instead of reallocating to
// speed up tests
var sock = new Websock();
var rQ = new Uint8Array(sock._rQbufferSize);
Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers;
Websock.prototype._allocate_buffers = function () {
this._rQ = rQ;
};
});
after(function () {
Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers;
this.clock.restore();
});
describe('Public API Basic Behavior', function () { describe('Public API Basic Behavior', function () {
var client; var client;
beforeEach(function () { beforeEach(function () {
...@@ -1826,7 +1845,7 @@ describe('Remote Frame Buffer Protocol Client', function() { ...@@ -1826,7 +1845,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
client.connect('host', 8675); client.connect('host', 8675);
client._rfb_state = 'normal'; client._rfb_state = 'normal';
client._normal_msg = sinon.spy(); client._normal_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([])); client._sock._websocket._receive_data(new Uint8Array([]));
expect(client._normal_msg).to.not.have.been.called; expect(client._normal_msg).to.not.have.been.called;
}); });
...@@ -1834,7 +1853,7 @@ describe('Remote Frame Buffer Protocol Client', function() { ...@@ -1834,7 +1853,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
client.connect('host', 8675); client.connect('host', 8675);
client._rfb_state = 'normal'; client._rfb_state = 'normal';
client._normal_msg = sinon.spy(); client._normal_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); client._sock._websocket._receive_data(new Uint8Array([1, 2, 3]));
expect(client._normal_msg).to.have.been.calledOnce; expect(client._normal_msg).to.have.been.calledOnce;
}); });
...@@ -1842,7 +1861,7 @@ describe('Remote Frame Buffer Protocol Client', function() { ...@@ -1842,7 +1861,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
client.connect('host', 8675); client.connect('host', 8675);
client._rfb_state = 'ProtocolVersion'; client._rfb_state = 'ProtocolVersion';
client._init_msg = sinon.spy(); client._init_msg = sinon.spy();
client._sock._websocket._receive_data(Base64.encode([1, 2, 3])); client._sock._websocket._receive_data(new Uint8Array([1, 2, 3]));
expect(client._init_msg).to.have.been.calledOnce; expect(client._init_msg).to.have.been.calledOnce;
}); });
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment