Compare commits

..

17 Commits

Author SHA1 Message Date
James Coglan 9c1e260784 When client handshake fails, close with the right code and reason. 2011-12-19 00:14:06 +00:00
James Coglan 1ba9b8586a Refactor subprotocol documentation. 2011-12-18 16:16:55 +00:00
James Coglan 05bc759af6 Make protocol vaidation a little more strict. 2011-12-18 16:09:36 +00:00
James Coglan e1aac5db02 Switch client to Sec-WebSocket-Version: 13. 2011-12-18 16:05:10 +00:00
James Coglan c751d96b8b Document subprotocol negotiation. 2011-12-18 16:04:23 +00:00
James Coglan fa2aff1387 Implement client-side subprotocol validation, and expose the protocol property. 2011-12-18 15:57:24 +00:00
James Coglan ea635db3f4 Merge branch 'master' into subprotocols 2011-12-18 15:37:05 +00:00
James Coglan b129b447a2 Refactor masking code. 2011-12-18 11:52:47 +00:00
James Coglan 390882f720 Change package description. 2011-12-18 09:20:56 +00:00
James Coglan 5a0c941075 Explicitly return null where parser methods hit an error. 2011-12-17 19:23:56 +00:00
James Coglan f5a78efcfd Extract 255 into a constant. 2011-12-17 19:22:46 +00:00
James Coglan f0f0bfc951 Inline draft-75 parsing function. 2011-12-17 17:23:17 +00:00
James Coglan 48dfb9578c Merge branch 'master' into subprotocols
Conflicts:
	lib/faye/websocket.js
	lib/faye/websocket/protocol8_parser.js
2011-12-17 15:37:54 +00:00
James Coglan 15f1b4df99 Remove benchmark page. 2011-12-17 15:20:30 +00:00
James Coglan 2dbac35b15 Only send Sec-WebSocket-Protocol response if there is a matching protocol available. 2011-12-15 20:05:03 +00:00
James Coglan 430b8ab7df Formatting tweaks. 2011-12-15 19:59:28 +00:00
James Coglan 283b13b617 Initial support for user-defined subprotocol negotiation on the server side. 2011-12-15 00:33:23 +00:00
13 changed files with 163 additions and 127 deletions
+27 -2
View File
@@ -49,8 +49,8 @@ server.listen(8000);
The client supports both the plain-text `ws` protocol and the encrypted `wss`
protocol, and has exactly the same interface as a socket you would use in a web
browser. On the wire it identifies itself as hybi-08, though it's compatible
with servers speaking later versions of the protocol, at least up to version 17.
browser. On the wire it identifies itself as hybi-13, though it's compatible
with servers speaking later versions of the protocol.
```js
var WebSocket = require('faye-websocket'),
@@ -72,6 +72,29 @@ ws.onclose = function(event) {
```
## Subprotocol negotiation
The WebSocket protocol allows peers to select and identify the application
protocol to use over the connection. On the client side, you can set which
protocols the client accepts by passing a list of protocol names when you
construct the socket:
```js
var ws = new WebSocket.Client('ws://www.example.com/', ['irc', 'amqp']);
```
On the server side, you can likewise pass in the list of protocols the server
supports after the other constructor arguments:
```js
var ws = new WebSocket(request, socket, head, ['irc', 'amqp']);
```
If the client and server agree on a protocol, both the client- and server-side
socket objects expose the selected protocol through the `ws.protocol` property.
If they cannot agree on a protocol to use, the client closes the connection.
## WebSocket API
The WebSocket API consists of several event handlers and a method for sending
@@ -92,6 +115,8 @@ messages.
sends a text or binary message over the connection to the other peer.
* <b><tt>close(code, reason)</tt></b> closes the connection, sending the given
status code and reason text, both of which are optional.
* <b><tt>protocol</tt></b> is a string or `null` identifying the subprotocol the
socket is using.
## License
-41
View File
@@ -1,41 +0,0 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>WebSocket benchmarks</title>
</head>
<body>
<div id="out"></div>
<script type="text/javascript">
var Socket = window.MozWebSocket || window.WebSocket,
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/'),
out = document.getElementById('out');
var msg = '', n = 64000;
while (n--) msg += 'X';
var start = new Date().getTime();
function ping(event) {
var data = (event && event.data) || '0',
id = parseInt(data.split(':')[0]) + 1;
out.innerHTML = id;
if (id === 1000) {
var time = new Date().getTime() - start;
out.innerHTML = 'Time: ' + time;
} else {
socket.send(id + ':' + msg);
}
};
socket.onopen = ping;
socket.onmessage = ping;
</script>
</body>
</html>
+7 -2
View File
@@ -12,14 +12,19 @@
<script type="text/javascript">
var logger = document.getElementsByTagName('ul')[0],
Socket = window.MozWebSocket || window.WebSocket,
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/'),
protos = ['foo', 'bar', 'xmpp'],
socket = new Socket('ws://' + location.hostname + ':' + location.port + '/', protos),
index = 0;
socket.onopen = function() {
logger.innerHTML += '<li>OPEN</li>';
logger.innerHTML += '<li>OPEN: ' + socket.protocol + '</li>';
socket.send('Hello, world');
};
socket.onerror = function(event) {
logger.innerHTML += '<li>ERROR: ' + error.message + '</li>';
};
socket.addEventListener('message', function(event) {
logger.innerHTML += '<li>MESSAGE: ' + event.data + '</li>';
setTimeout(function() { socket.send(++index + ' ' + event.data) }, 2000);
+2 -2
View File
@@ -25,8 +25,8 @@ var server = secure
: http.createServer(staticHandler);
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head);
console.log('open', ws.url, ws.version);
var ws = new WebSocket(request, socket, head, ['irc', 'xmpp']);
console.log('open', ws.url, ws.version, ws.protocol);
ws.onmessage = function(event) {
ws.send(event.data);
+3 -2
View File
@@ -30,7 +30,7 @@ var isSecureConnection = function(request) {
}
};
var WebSocket = function(request, socket, head) {
var WebSocket = function(request, socket, head, supportedProtos) {
this.request = request;
this._stream = request.socket;
@@ -40,11 +40,12 @@ var WebSocket = function(request, socket, head) {
this.bufferedAmount = 0;
var Parser = getParser(request);
this._parser = new Parser(this);
this._parser = new Parser(this, {protocols: supportedProtos});
var handshake = this._parser.handshakeResponse(head);
try { this._stream.write(handshake, 'binary') } catch (e) {}
this.protocol = this._parser.protocol;
this.readyState = API.OPEN;
this.version = this._parser.getVersion();
+1
View File
@@ -8,6 +8,7 @@ var API = {
onmessage: null,
onerror: null,
onclose: null,
protocol: null,
receive: function(data) {
if (this.readyState !== API.OPEN) return false;
+4 -3
View File
@@ -4,7 +4,7 @@ var API = require('./api'),
var Protocol8Parser = require('./protocol8_parser');
var Client = function(url) {
var Client = function(url, protocols) {
this.url = url;
this._uri = require('url').parse(url);
@@ -19,7 +19,7 @@ var Client = function(url) {
? tls.connect(this._uri.port || 443, this._uri.hostname, onConnect)
: net.createConnection(this._uri.port || 80, this._uri.hostname);
this._parser = new Protocol8Parser(this, {masking: true});
this._parser = new Protocol8Parser(this, {masking: true, protocols: protocols});
this._stream = connection;
if (!secure) connection.addListener('connect', onConnect);
@@ -51,6 +51,7 @@ Client.prototype._onData = function(data) {
if (!this._handshake.isComplete()) return;
if (this._handshake.isValid()) {
this.protocol = this._handshake.protocol;
this.readyState = API.OPEN;
var event = new API.Event('open');
event.initEvent('open', false, false);
@@ -60,7 +61,7 @@ Client.prototype._onData = function(data) {
} else {
this.readyState = API.CLOSED;
var event = new API.Event('close');
var event = new API.Event('close', {code: 1006, reason: ''});
event.initEvent('close', false, false);
this.dispatchEvent(event);
}
+17 -20
View File
@@ -22,8 +22,23 @@ var instance = {
},
parse: function(data) {
for (var i = 0, n = data.length; i < n; i++)
this._handleChar(data[i]);
for (var i = 0, n = data.length; i < n; i++) {
switch (data[i]) {
case 0x00:
this._buffering = true;
break;
case 0xFF:
this._buffer = new Buffer(this._buffer);
this._socket.receive(this._buffer.toString('utf8', 0, this._buffer.length));
this._buffer = [];
this._buffering = false;
break;
default:
if (this._buffering) this._buffer.push(data[i]);
}
}
},
frame: function(data) {
@@ -35,24 +50,6 @@ var instance = {
this.FRAME_END.copy(frame, buffer.length + 1);
return frame;
},
_handleChar: function(data) {
switch (data) {
case 0x00:
this._buffering = true;
break;
case 0xFF:
this._buffer = new Buffer(this._buffer);
this._socket.receive(this._buffer.toString('utf8', 0, this._buffer.length));
this._buffer = [];
this._buffering = false;
break;
default:
if (this._buffering) this._buffer.push(data);
}
}
};
+58 -37
View File
@@ -4,13 +4,28 @@ var crypto = require('crypto'),
var Protocol8Parser = function(webSocket, options) {
this._reset();
this._socket = webSocket;
this._reader = new Reader();
this._stage = 0;
this._masking = options && options.masking;
this._socket = webSocket;
this._reader = new Reader();
this._stage = 0;
this._masking = options && options.masking;
this._protocols = options && options.protocols;
if (typeof this._protocols === 'string')
this._protocols = this._protocols.split(/\s*,\s*/);
};
Protocol8Parser.mask = function(payload, mask, offset) {
if (mask.length === 0) return payload;
offset = offset || 0;
for (var i = 0, n = payload.length - offset; i < n; i++) {
payload[offset + i] = payload[offset + i] ^ mask[i % 4];
}
return payload;
};
var instance = {
BYTE: 255,
FIN: 128,
MASK: 128,
RSV1: 64,
@@ -53,21 +68,37 @@ var instance = {
handshakeResponse: function() {
var secKey = this._socket.request.headers['sec-websocket-key'];
if (!secKey) return;
if (!secKey) return null;
var SHA1 = crypto.createHash('sha1');
SHA1.update(secKey + Handshake.GUID);
var accept = SHA1.digest('base64');
return new Buffer('HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Accept: ' + accept + '\r\n\r\n',
'utf8');
var accept = SHA1.digest('base64'),
protos = this._socket.request.headers['sec-websocket-protocol'],
supported = this._protocols,
proto,
headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + accept
];
if (protos !== undefined && supported !== undefined) {
if (typeof protos === 'string') protos = protos.split(/\s*,\s*/);
proto = protos.filter(function(p) { return supported.indexOf(p) >= 0 })[0];
if (proto) {
this.protocol = proto;
headers.push('Sec-WebSocket-Protocol: ' + proto);
}
}
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
},
createHandshake: function(uri) {
return new Handshake(uri);
return new Handshake(uri, this._protocols);
},
parse: function(data) {
@@ -158,7 +189,7 @@ var instance = {
},
frame: function(data, type, code) {
if (this._closed) return;
if (this._closed) return null;
var isText = (typeof data === 'string'),
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
@@ -169,6 +200,7 @@ var instance = {
offset = header + (this._masking ? 4 : 0),
masked = this._masking ? this.MASK : 0,
frame = new Buffer(length + offset),
BYTE = this.BYTE,
mask, i;
frame[0] = this.FIN | opcode;
@@ -178,30 +210,29 @@ var instance = {
} else if (length <= 65535) {
frame[1] = masked | 126;
frame[2] = Math.floor(length / 256);
frame[3] = length & 255;
frame[3] = length & BYTE;
} else {
frame[1] = masked | 127;
frame[2] = Math.floor(length / Math.pow(2,56)) & 255;
frame[3] = Math.floor(length / Math.pow(2,48)) & 255;
frame[4] = Math.floor(length / Math.pow(2,40)) & 255;
frame[5] = Math.floor(length / Math.pow(2,32)) & 255;
frame[6] = Math.floor(length / Math.pow(2,24)) & 255;
frame[7] = Math.floor(length / Math.pow(2,16)) & 255;
frame[8] = Math.floor(length / Math.pow(2,8)) & 255;
frame[9] = length & 255;
frame[2] = Math.floor(length / Math.pow(2,56)) & BYTE;
frame[3] = Math.floor(length / Math.pow(2,48)) & BYTE;
frame[4] = Math.floor(length / Math.pow(2,40)) & BYTE;
frame[5] = Math.floor(length / Math.pow(2,32)) & BYTE;
frame[6] = Math.floor(length / Math.pow(2,24)) & BYTE;
frame[7] = Math.floor(length / Math.pow(2,16)) & BYTE;
frame[8] = Math.floor(length / Math.pow(2,8)) & BYTE;
frame[9] = length & BYTE;
}
if (code) {
frame[offset] = Math.floor(code / 256);
frame[offset+1] = code & 255;
frame[offset] = Math.floor(code / 256) & BYTE;
frame[offset+1] = code & BYTE;
}
buffer.copy(frame, offset + insert);
if (this._masking) {
mask = new Buffer([1,2,3,4].map(function() { return Math.floor(Math.random() * 256) }));
mask.copy(frame, header);
for (i = 0; i < length; i++)
frame[offset + i] = frame[offset + i] ^ mask[i % 4];
Protocol8Parser.mask(frame, mask, offset);
}
return frame;
@@ -220,7 +251,7 @@ var instance = {
},
_emitFrame: function() {
var payload = this._unmask(this._payload, this._mask),
var payload = Protocol8Parser.mask(this._payload, this._mask),
opcode = this._opcode;
if (opcode === this.OPCODES.continuation) {
@@ -292,16 +323,6 @@ var instance = {
for (var i = 0, n = bytes.length; i < n; i++)
number += bytes[i] << (8 * (n - 1 - i));
return number;
},
_unmask: function(payload, mask) {
var unmasked = new Buffer(payload.length), b;
for (var i = 0, n = payload.length; i < n; i++) {
b = payload[i];
if (mask.length > 0) b = b ^ mask[i % 4];
unmasked[i] = b;
}
return unmasked;
}
};
@@ -1,7 +1,8 @@
var crypto = require('crypto');
var Handshake = function(uri) {
var Handshake = function(uri, protocols) {
this._uri = uri;
this._protocols = protocols;
var buffer = new Buffer(16), i = 16;
while (i--) buffer[i] = Math.floor(Math.random() * 256);
@@ -43,13 +44,20 @@ Handshake.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
Handshake.prototype.requestData = function() {
var u = this._uri;
return new Buffer('GET ' + u.pathname + (u.search || '') + ' HTTP/1.1\r\n' +
'Host: ' + u.hostname + (u.port ? ':' + u.port : '') + '\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Key: ' + this._key + '\r\n' +
'Sec-WebSocket-Version: 8\r\n\r\n',
'utf8');
var headers = [
'GET ' + u.pathname + (u.search || '') + ' HTTP/1.1',
'Host: ' + u.hostname + (u.port ? ':' + u.port : ''),
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Key: ' + this._key,
'Sec-WebSocket-Version: 13'
];
if (this._protocols)
headers.push('Sec-WebSocket-Protocol: ' + this._protocols.join(', '));
return new Buffer(headers.concat('','').join('\r\n'), 'utf8');
};
Handshake.prototype.parse = function(data) {
@@ -67,10 +75,16 @@ Handshake.prototype.isValid = function() {
if (this._status !== 101) return false;
var upgrade = this._headers.Upgrade,
connection = this._headers.Connection;
connection = this._headers.Connection,
protocol = this._headers['Sec-WebSocket-Protocol'];
this.protocol = this._protocols && this._protocols.indexOf(protocol) >= 0
? protocol
: null;
return upgrade && /^websocket$/i.test(upgrade) &&
connection && connection.split(/\s*,\s*/).indexOf('Upgrade') >= 0 &&
((!this._protocols && !protocol) || this.protocol) &&
this._headers['Sec-WebSocket-Accept'] === this._accept;
};
+1 -1
View File
@@ -1,5 +1,5 @@
{ "name" : "faye-websocket"
, "description" : "Robust general-purpose WebSocket server and client"
, "description" : "Standards-compliant WebSocket server and client"
, "homepage" : "http://github.com/jcoglan/faye-websocket-node"
, "author" : "James Coglan <jcoglan@gmail.com> (http://jcoglan.com/)"
, "keywords" : ["websocket"]
+19 -7
View File
@@ -13,7 +13,7 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
setTimeout(callback, 100)
},
open_socket: function(url, callback) {
open_socket: function(url, protocols, callback) {
var done = false,
self = this,
@@ -24,7 +24,7 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
callback()
}
this._ws = new Client(url)
this._ws = new Client(url, protocols)
this._ws.onopen = function() { resume(true) }
this._ws.onclose = function() { resume(false) }
@@ -49,6 +49,11 @@ JS.ENV.WebSocketSteps = JS.Test.asyncSteps({
callback()
},
check_protocol: function(protocol, callback) {
this.assertEqual( protocol, this._ws.protocol )
callback()
},
listen_for_message: function(callback) {
var self = this
this._ws.addEventListener('message', function(message) { self._message = message.data })
@@ -76,30 +81,37 @@ JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
include(WebSocketSteps)
before(function() {
this.protocols = ["foo", "echo"]
this.plain_text_url = "ws://localhost:8000/bayeux"
this.secure_url = "wss://localhost:8000/bayeux"
})
sharedBehavior("socket client", function() { with(this) {
it("can open a connection", function() { with(this) {
open_socket(socket_url)
open_socket(socket_url, protocols)
check_open()
check_protocol("echo")
}})
it("cannot open a connection to the wrong host", function() { with(this) {
open_socket(blocked_url)
open_socket(blocked_url, protocols)
check_closed()
}})
it("cannot open a connection with unacceptable protocols", function() { with(this) {
open_socket(socket_url, ["foo"])
check_closed()
}})
it("can close the connection", function() { with(this) {
open_socket(socket_url)
open_socket(socket_url, protocols)
close_socket()
check_closed()
}})
describe("in the OPEN state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url)
open_socket(socket_url, protocols)
}})
it("can send and receive messages", function() { with(this) {
@@ -111,7 +123,7 @@ JS.ENV.ClientSpec = JS.Test.describe("Client", function() { with(this) {
describe("in the CLOSED state", function() { with(this) {
before(function() { with(this) {
open_socket(socket_url)
open_socket(socket_url, protocols)
close_socket()
}})
+1 -1
View File
@@ -16,7 +16,7 @@ EchoServer.prototype.listen = function(port, ssl) {
: http.createServer()
server.addListener('upgrade', function(request, socket, head) {
var ws = new WebSocket(request, socket, head)
var ws = new WebSocket(request, socket, head, ["echo"])
ws.onmessage = function(event) {
ws.send(event.data)
}