Compare commits

...

47 Commits

Author SHA1 Message Date
James Coglan 4c3eaf8141 Bump version to 0.2.0, add changelog. 2011-12-21 01:28:20 +00:00
James Coglan 0b53267717 protocol property should be '' if not set. 2011-12-21 01:19:39 +00:00
James Coglan bdb0de2cfa Draft-75/76 frames with length headers should be ignored. 2011-12-20 19:44:07 +00:00
James Coglan 6704d33859 Implement the full framing interpreter for draft-75/76, including closing frames for 76. 2011-12-19 22:59:08 +00:00
James Coglan 4240b6bc27 Test that parsers are not sensitive to how data is split across packets. 2011-12-19 21:36:49 +00:00
James Coglan afdf6c223c Change version strings. 2011-12-19 21:25:53 +00:00
James Coglan 2782990fe3 Rename Protocol8Parser to HybiParser. 2011-12-19 21:25:19 +00:00
James Coglan 421f4660e5 Don't use map() to generate masks. 2011-12-19 10:12:26 +00:00
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 5635baf3a7 Don't skip any Autobahn tests since they run fast enough now. 2011-12-17 15:13:28 +00:00
James Coglan 2fa34ef097 Prevent stack overflows while parsing. 2011-12-17 15:13:09 +00:00
James Coglan 06f4c51a5b Refactor Protocol8 parser to read in chunks rather than one byte at a time. 2011-12-17 14:34:22 +00:00
James Coglan 3fa6e43ca9 Move Protocol8Parser.Handshake into its own file. 2011-12-17 13:30:05 +00:00
James Coglan a138004ec3 Don't pass TCP socket in when constructing a parser. 2011-12-17 03:06:40 +00:00
James Coglan d295047393 Decouple TCP sockets from parsers. 2011-12-17 01:41:34 +00:00
James Coglan 6f1bc69cc0 Remove an unneeded local var. 2011-12-16 23:04:23 +00:00
James Coglan c2ad20e226 Make Protocol8's frame() method much faster. 2011-12-16 23:03:23 +00:00
James Coglan 880398ff29 Merge remote branch 'majek/issue7' 2011-12-16 21:12:51 +00:00
James Coglan 0ea03585f9 Add benchmarking page. 2011-12-16 21:11:23 +00:00
James Coglan 918f86f58d Inline some parsing methods to improve performance. 2011-12-16 16:45:27 +00:00
Marek Majkowski 2f4f1cdc8f Fix #7 - random values should be 0...255 inclusive. 2011-12-16 15:07:20 +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
James Coglan 19da8de414 Bump version to 0.1.2. 2011-12-05 09:39:40 +00:00
James Coglan 85ddb3f3dc Remove some comments. 2011-12-05 09:38:10 +00:00
James Coglan 7e2461e153 Fix draft-76 sockets when running behind HAProxy. 2011-12-04 21:15:24 +00:00
James Coglan ca3571f103 Don't do so many writes. 2011-12-03 21:37:38 +00:00
James Coglan 27f8f9a0f2 Detect closed sockets on the server side. 2011-12-02 10:00:25 +00:00
James Coglan b1ead4947e Bump version to 0.1.1. 2011-11-30 00:57:00 +00:00
James Coglan 8f1739e41c Change example back to using onmessage. 2011-11-29 21:08:37 +00:00
James Coglan b5a3de9112 Use addEventListener in the tests. 2011-11-29 21:01:27 +00:00
James Coglan 642532f928 Use addEventListener to listen for messages in examples. 2011-11-29 13:08:22 +00:00
James Coglan 85a11d58d9 Fix broken EventTarget interface. 2011-11-29 12:47:28 +00:00
20 changed files with 661 additions and 364 deletions
+23
View File
@@ -0,0 +1,23 @@
=== 0.2.0 / 2011-12-21
* Add support for Sec-WebSocket-Protocol negotiation
* Support hixie-76 close frames and 75/76 ignored segments
* Improve performance of HyBi parsing/framing functions
* Decouple parsers from TCP and reduce write volume
=== 0.1.2 / 2011-12-05
* Detect closed sockets on the server side when TCP connection breaks
* Make hixie-76 sockets work through HAProxy
=== 0.1.1 / 2011-11-30
* Fix addEventListener() interface methods
=== 0.1.0 / 2011-11-27
* Initial release, based on WebSocket components from Faye
+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
+1 -2
View File
@@ -3,8 +3,7 @@ var WebSocket = require('../lib/faye/websocket');
var host = 'ws://localhost:9001',
agent = 'Faye (Node ' + process.version + ')',
cases = 0,
skip = [247,248,249,250,251,252,253,254,255,
256,257,258,259,260,261,262,263,264];
skip = [];
var socket = new WebSocket.Client(host + '/getCaseCount');
+21
View File
@@ -0,0 +1,21 @@
defaults
mode http
timeout client 5s
timeout connect 5s
timeout server 5s
frontend all 0.0.0.0:3000
mode http
timeout client 120s
option forwardfor
option http-server-close
option http-pretend-keepalive
default_backend sockets
backend sockets
balance uri depth 2
timeout server 120s
server socket1 127.0.0.1:7000
+9 -4
View File
@@ -12,18 +12,23 @@
<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.onmessage = function(event) {
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);
};
});
socket.onclose = function(event) {
logger.innerHTML += '<li>CLOSE: ' + event.code + ', ' + event.reason + '</li>';
+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);
+16 -12
View File
@@ -7,15 +7,15 @@
// * http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
// * http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
var Draft75Parser = require('./websocket/draft75_parser'),
Draft76Parser = require('./websocket/draft76_parser'),
Protocol8Parser = require('./websocket/protocol8_parser'),
API = require('./websocket/api');
var Draft75Parser = require('./websocket/draft75_parser'),
Draft76Parser = require('./websocket/draft76_parser'),
HybiParser = require('./websocket/hybi_parser'),
API = require('./websocket/api');
var getParser = function(request) {
var headers = request.headers;
return headers['sec-websocket-version']
? Protocol8Parser
? HybiParser
: (headers['sec-websocket-key1'] && headers['sec-websocket-key2'])
? Draft76Parser
: Draft75Parser;
@@ -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,9 +40,12 @@ var WebSocket = function(request, socket, head) {
this.bufferedAmount = 0;
var Parser = getParser(request);
this._parser = new Parser(this, this._stream);
this._parser.handshakeResponse(head);
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();
@@ -53,12 +56,13 @@ var WebSocket = function(request, socket, head) {
var self = this;
this._stream.addListener('data', function(data) {
self._parser.parse(data);
var response = self._parser.parse(data);
if (!response) return;
try { self._stream.write(response, 'binary') } catch (e) {}
});
this._stream.addListener('close', function() {
self.close(1006, '', false);
['close', 'end', 'error'].forEach(function(event) {
self._stream.addListener(event, function() { self.close(1006, '', false) });
});
this._stream.addListener('error', function() {});
};
var API = require('./websocket/api');
+13 -9
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;
@@ -19,7 +20,13 @@ var API = {
send: function(data, type, errorType) {
if (this.readyState === API.CLOSED) return false;
return this._parser.frame(data, type, errorType);
var frame = this._parser.frame(data, type, errorType);
try {
this._stream.write(frame, 'binary');
return true;
} catch (e) {
return false;
}
},
close: function(code, reason, ack) {
@@ -45,13 +52,13 @@ var API = {
}
},
addEventListener: function(type, listener, useCapture) {
addEventListener: function(eventType, listener, useCapture) {
this._listeners = this._listeners || {};
var list = this._listeners[eventType] = this._listeners[eventType] || [];
list.push(listener);
},
removeEventListener: function(type, listener, useCapture) {
removeEventListener: function(eventType, listener, useCapture) {
if (!this._listeners || !this._listeners[eventType]) return;
if (!listener) {
@@ -74,13 +81,10 @@ var API = {
if (this['on' + event.type])
this['on' + event.type](event);
var args = Array.prototype.slice.call(arguments),
eventType = args.shift();
if (!this._listeners || !this._listeners[event.type]) return;
if (!this._listeners || !this._listeners[eventType]) return;
this._listeners[eventType].forEach(function(listener) {
listener.apply(this, args);
this._listeners[event.type].forEach(function(listener) {
listener(event);
}, this);
}
};
+15 -11
View File
@@ -2,24 +2,25 @@ var API = require('./api'),
net = require('net'),
tls = require('tls');
var Protocol8Parser = require('./protocol8_parser');
var HybiParser = require('./hybi_parser');
var Client = function(url) {
this.url = url;
this.uri = require('url').parse(url);
var Client = function(url, protocols) {
this.url = url;
this._uri = require('url').parse(url);
this.protocol = '';
this.readyState = API.CONNECTING;
this.bufferedAmount = 0;
var secure = (this.uri.protocol === 'wss:'),
var secure = (this._uri.protocol === 'wss:'),
self = this,
onConnect = function() { self._onConnect() },
connection = secure
? tls.connect(this.uri.port || 443, this.uri.hostname, onConnect)
: net.createConnection(this.uri.port || 80, this.uri.hostname);
? tls.connect(this._uri.port || 443, this._uri.hostname, onConnect)
: net.createConnection(this._uri.port || 80, this._uri.hostname);
this._parser = new Protocol8Parser(this, connection, {masking: true});
this._parser = new HybiParser(this, {masking: true, protocols: protocols});
this._stream = connection;
if (!secure) connection.addListener('connect', onConnect);
@@ -34,9 +35,11 @@ var Client = function(url) {
};
Client.prototype._onConnect = function() {
this._handshake = this._parser.createHandshake(this.uri);
this._handshake = this._parser.createHandshake(this._uri, this._stream);
this._message = [];
this._handshake.requestData();
try {
this._stream.write(this._handshake.requestData(), 'binary');
} catch (e) {}
};
Client.prototype._onData = function(data) {
@@ -49,6 +52,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);
@@ -58,7 +62,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);
}
+73 -49
View File
@@ -1,65 +1,89 @@
var Draft75Parser = function(webSocket, stream) {
this._socket = webSocket;
this._stream = stream;
this._buffer = [];
this._buffering = false;
var Draft75Parser = function(webSocket) {
this._socket = webSocket;
this._stage = 0;
};
var instance = {
FRAME_START : new Buffer([0x00]),
FRAME_END : new Buffer([0xFF]),
getVersion: function() {
return 'draft-75';
return 'hixie-75';
},
handshakeResponse: function() {
var stream = this._stream;
try {
stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n');
stream.write('Upgrade: WebSocket\r\n');
stream.write('Connection: Upgrade\r\n');
stream.write('WebSocket-Origin: ' + this._socket.request.headers.origin + '\r\n');
stream.write('WebSocket-Location: ' + this._socket.url + '\r\n\r\n');
} catch (e) {
// socket closed while writing
// no handshake sent; client will stop using WebSocket
return new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'WebSocket-Origin: ' + this._socket.request.headers.origin + '\r\n' +
'WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'utf8');
},
parse: function(buffer) {
var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i];
switch (this._stage) {
case 0:
this._parseLeadingByte(data);
break;
case 1:
value = (data & 0x7F);
this._length = value + 128 * this._length;
if (this._closing && this._length === 0) {
this._socket.close(null, null, false);
}
else if ((0x80 & data) !== 0x80) {
if (this._length === 0) {
this._socket.receive('');
this._stage = 0;
}
else {
this._buffer = [];
this._stage = 2;
}
}
break;
case 2:
if (data === 0xFF) {
message = new Buffer(this._buffer);
this._socket.receive(message.toString('utf8', 0, this._buffer.length));
this._stage = 0;
}
else {
this._buffer.push(data);
if (this._length && this._buffer.length === this._length)
this._stage = 0;
}
break;
}
}
},
parse: function(data) {
for (var i = 0, n = data.length; i < n; i++)
this._handleChar(data[i]);
_parseLeadingByte: function(data) {
if ((0x80 & data) === 0x80) {
this._length = 0;
this._stage = 1;
} else {
delete this._length;
this._buffer = [];
this._stage = 2;
}
},
frame: function(data) {
var stream = this._stream;
try {
stream.write(this.FRAME_START, 'binary');
stream.write(new Buffer(data), 'utf8');
stream.write(this.FRAME_END, 'binary');
return true;
} catch (e) {
return false;
}
},
_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);
}
if (Buffer.isBuffer(data)) return data;
var buffer = new Buffer(data, 'utf8'),
frame = new Buffer(buffer.length + 2);
frame[0] = 0x00;
frame[buffer.length + 1] = 0xFF;
buffer.copy(frame, 1);
return frame;
}
};
+49 -13
View File
@@ -23,12 +23,35 @@ var bigEndian = function(number) {
};
Draft76Parser.prototype.getVersion = function() {
return 'draft-76';
return 'hixie-76';
};
Draft76Parser.prototype.handshakeResponse = function(head) {
var request = this._socket.request, tmp;
var response = new Buffer('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'Sec-WebSocket-Origin: ' + request.headers.origin + '\r\n' +
'Sec-WebSocket-Location: ' + this._socket.url + '\r\n\r\n',
'binary');
var signature = this.handshakeSignature(head);
if (signature) {
tmp = new Buffer(response.length + signature.length);
response.copy(tmp, 0);
signature.copy(tmp, response.length);
response = tmp;
}
return response;
};
Draft76Parser.prototype.handshakeSignature = function(head) {
if (head.length === 0) return null;
this._handshakeComplete = true;
var request = this._socket.request,
stream = this._stream,
key1 = request.headers['sec-websocket-key1'],
value1 = numberFromKey(key1) / spacesInKey(key1),
@@ -42,17 +65,30 @@ Draft76Parser.prototype.handshakeResponse = function(head) {
MD5.update(bigEndian(value2));
MD5.update(head.toString('binary'));
try {
stream.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n', 'binary');
stream.write('Upgrade: WebSocket\r\n', 'binary');
stream.write('Connection: Upgrade\r\n', 'binary');
stream.write('Sec-WebSocket-Origin: ' + request.headers.origin + '\r\n', 'binary');
stream.write('Sec-WebSocket-Location: ' + this._socket.url + '\r\n\r\n', 'binary');
stream.write(MD5.digest('binary'), 'binary');
} catch (e) {
// socket closed while writing
// no handshake sent; client will stop using WebSocket
}
return new Buffer(MD5.digest('binary'), 'binary');
};
Draft76Parser.prototype.parse = function(data) {
if (this._handshakeComplete)
return Draft75Parser.prototype.parse.call(this, data);
return this.handshakeSignature(data);
};
Draft76Parser.prototype._parseLeadingByte = function(data) {
if (data !== 0xFF)
return Draft75Parser.prototype._parseLeadingByte.call(this, data);
this._closing = true;
this._length = 0;
this._stage = 1;
};
Draft76Parser.prototype.close = function(code, reason, callback, context) {
if (this._closed) return;
if (this._closing) this._socket.send(new Buffer([0xFF, 0x00]));
this._closed = true;
if (callback) callback.call(context);
};
module.exports = Draft76Parser;
@@ -1,89 +1,31 @@
var crypto = require('crypto');
var crypto = require('crypto'),
Handshake = require('./hybi_parser/handshake'),
Reader = require('./hybi_parser/stream_reader');
var Handshake = function(uri, stream) {
this._uri = uri;
this._stream = stream;
var buffer = new Buffer(16), i = 16;
while (i--) buffer[i] = Math.floor(Math.random() * 254);
this._key = buffer.toString('base64');
var SHA1 = crypto.createHash('sha1');
SHA1.update(this._key + Protocol8Parser.prototype.GUID);
this._accept = SHA1.digest('base64');
var HTTPParser = process.binding('http_parser').HTTPParser,
parser = new HTTPParser(HTTPParser.RESPONSE || 'response'),
current = null,
self = this;
this._nodeVersion = HTTPParser.RESPONSE ? 6 : 4;
this._complete = false;
this._headers = {};
this._parser = parser;
parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length);
};
parser.onHeaderValue = function(b, start, length) {
self._headers[current] = b.toString('utf8', start, start + length);
};
parser.onHeadersComplete = function(info) {
self._status = info.statusCode;
var headers = info.headers;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self._headers[headers[i]] = headers[i+1];
};
parser.onMessageComplete = function() {
self._complete = true;
};
};
Handshake.prototype.requestData = function() {
var stream = this._stream, u = this._uri;
try {
stream.write('GET ' + u.pathname + (u.search || '') + ' HTTP/1.1\r\n');
stream.write('Host: ' + u.hostname + (u.port ? ':' + u.port : '') + '\r\n');
stream.write('Upgrade: websocket\r\n');
stream.write('Connection: Upgrade\r\n');
stream.write('Sec-WebSocket-Key: ' + this._key + '\r\n');
stream.write('Sec-WebSocket-Version: 8\r\n');
stream.write('\r\n');
} catch (e) {}
};
Handshake.prototype.parse = function(data) {
var consumed = this._parser.execute(data, 0, data.length),
offset = (this._nodeVersion < 6) ? 1 : 0;
return (consumed === data.length) ? [] : data.slice(consumed + offset);
};
Handshake.prototype.isComplete = function() {
return this._complete;
};
Handshake.prototype.isValid = function() {
if (this._status !== 101) return false;
var upgrade = this._headers.Upgrade,
connection = this._headers.Connection;
return upgrade && /^websocket$/i.test(upgrade) &&
connection && connection.split(/\s*,\s*/).indexOf('Upgrade') >= 0 &&
this._headers['Sec-WebSocket-Accept'] === this._accept;
};
var Protocol8Parser = function(webSocket, stream, options) {
var HybiParser = function(webSocket, options) {
this._reset();
this._socket = webSocket;
this._stream = stream;
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*/);
};
HybiParser.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,
@@ -92,8 +34,6 @@ var instance = {
OPCODE: 15,
LENGTH: 127,
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
OPCODES: {
continuation: 0,
text: 1,
@@ -123,110 +63,82 @@ var instance = {
getVersion: function() {
var version = this._socket.request.headers['sec-websocket-version'];
return 'protocol-' + version;
return 'hybi-' + version;
},
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 + this.GUID);
var accept = SHA1.digest('base64');
SHA1.update(secKey + Handshake.GUID);
var stream = this._stream;
try {
stream.write('HTTP/1.1 101 Switching Protocols\r\n');
stream.write('Upgrade: websocket\r\n');
stream.write('Connection: Upgrade\r\n');
stream.write('Sec-WebSocket-Accept: ' + accept + '\r\n\r\n');
} catch (e) {
// socket closed while writing
// no handshake sent; client will stop using WebSocket
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() {
return new Handshake(this._socket.uri, this._stream);
createHandshake: function(uri) {
return new Handshake(uri, this._protocols);
},
parse: function(data) {
for (var i = 0, n = data.length; i < n; i++) {
this._reader.put(data);
var buffer = true;
while (buffer) {
switch (this._stage) {
case 0: this._parseOpcode(data[i]); break;
case 1: this._parseLength(data[i]); break;
case 2: this._parseExtendedLength(data[i]); break;
case 3: this._parseMask(data[i]); break;
case 4: this._parsePayload(data[i]); break;
}
if (this._stage === 4 && this._length === 0)
this._emitFrame();
}
},
frame: function(data, type, code) {
if (this._closed) return;
var opcode = this.OPCODES[type || (typeof data === 'string' ? 'text' : 'binary')],
buffer = new Buffer(data),
insert = code ? 2 : 0,
length = buffer.length + insert,
masked = this._masking ? this.MASK : 0,
stream = this._stream,
frame, factor, mask, i, n;
data = new Buffer(length);
if (code) {
data[0] = Math.floor(code / 256);
data[1] = code & 255;
}
for (i = 0, n = buffer.length; i < n; i++)
data[insert + i] = buffer[i];
if (length <= 125) {
frame = new Buffer(2);
frame[1] = masked | length;
} else if (length >= 126 && length <= 65535) {
frame = new Buffer(4);
frame[1] = masked | 126;
frame[2] = Math.floor(length / 256);
frame[3] = length & 255;
} else {
frame = new Buffer(10);
frame[1] = masked | 127;
for (var i = 0; i < 8; i++) {
factor = Math.pow(2, 8 * (8 - 1 - i));
frame[2+i] = Math.floor(length / factor) & 255;
case 0:
buffer = this._reader.read(1);
if (buffer) this._parseOpcode(buffer[0]);
break;
case 1:
buffer = this._reader.read(1);
if (buffer) this._parseLength(buffer[0]);
break;
case 2:
buffer = this._reader.read(this._lengthSize);
if (buffer) this._parseExtendedLength(buffer);
break;
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._mask = buffer;
this._stage = 4;
}
break;
case 4:
buffer = this._reader.read(this._length);
if (buffer) {
this._payload = buffer;
this._emitFrame();
this._stage = 0;
}
break;
}
}
frame[0] = this.FIN | opcode;
if (this._masking) {
mask = [1,2,3,4].map(function() { return Math.floor(Math.random() * 255) });
for (i = 0, n = data.length; i < n; i++)
data[i] = data[i] ^ mask[i % 4];
}
try {
stream.write(frame, 'binary');
if (mask) stream.write(new Buffer(mask), 'binary');
if (data.length > 0) stream.write(data, 'utf8');
return true;
} catch (e) {
return false;
}
},
close: function(code, reason, callback, context) {
if (this._closed) return;
if (callback) this._closingCallback = [callback, context];
this.frame(reason || '', 'close', code || this.ERRORS.normal_closure);
this._closed = true;
},
buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this._buffer.push(fragment[i]);
},
_parseOpcode: function(data) {
@@ -271,27 +183,76 @@ var instance = {
}
},
_parseExtendedLength: function(data) {
this._lengthBuffer.push(data);
if (this._lengthBuffer.length < this._lengthSize) return;
this._length = this._getInteger(this._lengthBuffer);
_parseExtendedLength: function(buffer) {
this._length = this._getInteger(buffer);
this._stage = this._masked ? 3 : 4;
},
_parseMask: function(data) {
this._mask.push(data);
if (this._mask.length < 4) return;
this._stage = 4;
frame: function(data, type, code) {
if (this._closed) return null;
var isText = (typeof data === 'string'),
opcode = this.OPCODES[type || (isText ? 'text' : 'binary')],
buffer = isText ? new Buffer(data, 'utf8') : data,
insert = code ? 2 : 0,
length = buffer.length + insert,
header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10),
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;
if (length <= 125) {
frame[1] = masked | length;
} else if (length <= 65535) {
frame[1] = masked | 126;
frame[2] = Math.floor(length / 256);
frame[3] = length & BYTE;
} else {
frame[1] = masked | 127;
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) & BYTE;
frame[offset+1] = code & BYTE;
}
buffer.copy(frame, offset + insert);
if (this._masking) {
mask = [Math.floor(Math.random() * 256), Math.floor(Math.random() * 256),
Math.floor(Math.random() * 256), Math.floor(Math.random() * 256)];
new Buffer(mask).copy(frame, header);
HybiParser.mask(frame, mask, offset);
}
return frame;
},
_parsePayload: function(data) {
this._payload.push(data);
if (this._payload.length < this._length) return;
this._emitFrame();
close: function(code, reason, callback, context) {
if (this._closed) return;
if (callback) this._closingCallback = [callback, context];
this._socket.send(reason || '', 'close', code || this.ERRORS.normal_closure);
this._closed = true;
},
buffer: function(fragment) {
for (var i = 0, n = fragment.length; i < n; i++)
this._buffer.push(fragment[i]);
},
_emitFrame: function() {
var payload = this._unmask(this._payload, this._mask),
var payload = HybiParser.mask(this._payload, this._mask),
opcode = this._opcode;
if (opcode === this.OPCODES.continuation) {
@@ -343,7 +304,6 @@ var instance = {
if (payload.length > 125) return this._socket.close(this.ERRORS.protocol_error, null, false);
this._socket.send(payload, 'pong');
}
this._stage = 0;
},
_reset: function() {
@@ -364,21 +324,11 @@ 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;
}
};
for (var key in instance)
Protocol8Parser.prototype[key] = instance[key];
HybiParser.prototype[key] = instance[key];
module.exports = Protocol8Parser;
module.exports = HybiParser;
@@ -0,0 +1,91 @@
var crypto = require('crypto');
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);
this._key = buffer.toString('base64');
var SHA1 = crypto.createHash('sha1');
SHA1.update(this._key + Handshake.GUID);
this._accept = SHA1.digest('base64');
var HTTPParser = process.binding('http_parser').HTTPParser,
parser = new HTTPParser(HTTPParser.RESPONSE || 'response'),
current = null,
self = this;
this._nodeVersion = HTTPParser.RESPONSE ? 6 : 4;
this._complete = false;
this._headers = {};
this._parser = parser;
parser.onHeaderField = function(b, start, length) {
current = b.toString('utf8', start, start + length);
};
parser.onHeaderValue = function(b, start, length) {
self._headers[current] = b.toString('utf8', start, start + length);
};
parser.onHeadersComplete = function(info) {
self._status = info.statusCode;
var headers = info.headers;
if (!headers) return;
for (var i = 0, n = headers.length; i < n; i += 2)
self._headers[headers[i]] = headers[i+1];
};
parser.onMessageComplete = function() {
self._complete = true;
};
};
Handshake.GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
Handshake.prototype.requestData = function() {
var u = this._uri;
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) {
var consumed = this._parser.execute(data, 0, data.length),
offset = (this._nodeVersion < 6) ? 1 : 0;
return (consumed === data.length) ? [] : data.slice(consumed + offset);
};
Handshake.prototype.isComplete = function() {
return this._complete;
};
Handshake.prototype.isValid = function() {
if (this._status !== 101) return false;
var upgrade = this._headers.Upgrade,
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;
};
module.exports = Handshake;
@@ -0,0 +1,43 @@
var StreamReader = function() {
this._queue = [];
this._cursor = 0;
};
StreamReader.prototype.read = function(bytes) {
return this._readBuffer(bytes);
};
StreamReader.prototype.put = function(buffer) {
if (!buffer || buffer.length === 0) return;
if (!buffer.copy) buffer = new Buffer(buffer);
this._queue.push(buffer);
};
StreamReader.prototype._readBuffer = function(length) {
var buffer = new Buffer(length),
queue = this._queue,
remain = length,
n = queue.length,
i = 0,
chunk, offset, size;
if (remain === 0) return buffer;
while (remain > 0 && i < n) {
chunk = queue[i];
offset = (i === 0) ? this._cursor : 0;
size = Math.min(remain, chunk.length - offset);
chunk.copy(buffer, length - remain, offset, offset + size);
remain -= size;
i += 1;
}
if (remain > 0) return null;
queue.splice(0, i-1);
this._cursor = (i === 1 ? this._cursor : 0) + size;
return buffer;
};
module.exports = StreamReader;
+3 -3
View File
@@ -1,13 +1,13 @@
{ "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"]
, "version" : "0.1.0"
, "version" : "0.2.0"
, "engines" : {"node": ">=0.4.0"}
, "main" : "./lib/faye/websocket"
, "devDependencies" : {"jsclass": ">=3.0.4"}
, "devDependencies" : {"jsclass": ""}
, "bugs" : "http://github.com/jcoglan/faye-websocket-node/issues"
+20 -8
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,9 +49,14 @@ 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.onmessage = function(message) { self._message = message.data }
this._ws.addEventListener('message', function(message) { self._message = message.data })
callback()
},
@@ -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()
}})
+50 -17
View File
@@ -3,37 +3,70 @@ var Draft75Parser = require('../../../lib/faye/websocket/draft75_parser')
JS.ENV.Draft75ParserSpec = JS.Test.describe("Draft75Parser", function() { with(this) {
before(function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.socket = new FakeSocket
this.parser = new Draft75Parser(webSocket, socket)
this.parser = new Draft75Parser(webSocket)
}})
describe("parse", function() { with(this) {
it("parses text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
sharedBehavior("draft-75 parser", function() { with(this) {
it("parses text frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses multiple frames from the same packet", function() { with(this) {
expect(webSocket, "receive").given("Hello").exactly(2)
parser.parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses text frames beginning 0x00-0x7F", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x66, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("ignores frames with a length header", function() { with(this) {
expect(webSocket, "receive").exactly(0)
parser.parse([0x80, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses text following an ignored block", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x80, 0x02, 0x48, 0x65, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff])
}})
it("parses multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
it("parses frames received in several packets", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65])
parser.parse([0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
it("parses fragmented frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c])
parser.parse([0x6c, 0x6f, 0xff])
}})
}})
it("parses multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parser.parse([0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff])
}})
behavesLike("draft-75 parser")
it("parses fragmented frames", function() { with(this) {
expect(webSocket, "receive").given("Hello")
parser.parse([0x00, 0x48, 0x65, 0x6c])
parser.parse([0x6c, 0x6f, 0xff])
it("does not close the socket if a 76 close frame is received", function() { with(this) {
expect(webSocket, "close").exactly(0)
expect(webSocket, "receive").given("")
parser.parse([0xFF, 0x00])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
parser.frame("Hello")
assertEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], socket.read() )
assertBufferEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
parser.frame("Apple = ")
assertEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], socket.read() )
assertBufferEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], parser.frame("Apple = ") )
}})
}})
}})
+28
View File
@@ -0,0 +1,28 @@
var Draft76Parser = require('../../../lib/faye/websocket/draft76_parser')
JS.ENV.Draft76ParserSpec = JS.Test.describe("Draft76Parser", function() { with(this) {
before(function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.parser = new Draft76Parser(webSocket)
parser._handshakeComplete = true
}})
describe("parse", function() { with(this) {
behavesLike("draft-75 parser")
it("closes the socket if a close frame is received", function() { with(this) {
expect(webSocket, "close")
parser.parse([0xFF, 0x00])
}})
}})
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
assertBufferEqual( [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
assertBufferEqual( [0x00, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf, 0xff], parser.frame("Apple = ") )
}})
}})
}})
@@ -1,10 +1,9 @@
var Protocol8Parser = require('../../../lib/faye/websocket/protocol8_parser')
var HybiParser = require('../../../lib/faye/websocket/hybi_parser')
JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { with(this) {
JS.ENV.HybiParserSpec = JS.Test.describe("HybiParser", function() { with(this) {
before(function() { with(this) {
this.webSocket = {dispatchEvent: function() {}}
this.socket = new FakeSocket
this.parser = new Protocol8Parser(webSocket, socket)
this.parser = new HybiParser(webSocket)
}})
define("parse", function() {
@@ -39,6 +38,11 @@ JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { wi
parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses multiple frames from the same packet", function() { with(this) {
expect(webSocket, "receive").given("Hello").exactly(2)
parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])
}})
it("parses empty text frames", function() { with(this) {
expect(webSocket, "receive").given("")
parse([0x81, 0x00])
@@ -81,6 +85,12 @@ JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { wi
parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
}})
it("parses frames received in several packets", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c])
parse([0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf])
}})
it("parses fragmented multibyte text frames", function() { with(this) {
expect(webSocket, "receive").given("Apple = ")
parse([0x01, 0x0a, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3])
@@ -116,18 +126,15 @@ JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { wi
describe("frame", function() { with(this) {
it("returns the given string formatted as a WebSocket frame", function() { with(this) {
parser.frame("Hello")
assertEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], socket.read() )
assertBufferEqual( [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello") )
}})
it("encodes multibyte characters correctly", function() { with(this) {
parser.frame("Apple = ")
assertEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], socket.read() )
assertBufferEqual( [0x81, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x65, 0x20, 0x3d, 0x20, 0xef, 0xa3, 0xbf], parser.frame("Apple = ") )
}})
it("encodes medium-length strings using extra length bytes", function() { with(this) {
parser.frame("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello")
assertEqual( [129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111], socket.read() )
assertBufferEqual( [129, 126, 0, 200, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111, 72, 101, 108, 108, 111], parser.frame("HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello") )
}})
it("encodes long strings using extra length bytes", function() { with(this) {
@@ -136,18 +143,15 @@ JS.ENV.Protocol8ParserSpec = JS.Test.describe("Protocol8Parser", function() { wi
message += "Hello"
output = output.concat([0x48, 0x65, 0x6c, 0x6c, 0x6f])
}
parser.frame(message)
assertEqual( output, socket.read() )
assertBufferEqual( output, parser.frame(message) )
}})
it("encodes close frames with an error code", function() { with(this) {
parser.frame("Hello", "close", 1002)
assertEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], socket.read() )
assertBufferEqual( [0x88, 0x07, 0x03, 0xea, 0x48, 0x65, 0x6c, 0x6c, 0x6f], parser.frame("Hello", "close", 1002) )
}})
it("encodes pong frames", function() { with(this) {
parser.frame("", "pong")
assertEqual( [0x8a, 0x00], socket.read() )
assertBufferEqual( [0x8a, 0x00], parser.frame("", "pong") )
}})
}})
}})
+10 -19
View File
@@ -6,23 +6,6 @@ var WebSocket = require('../lib/faye/websocket'),
https = require('https')
JS.ENV.FakeSocket = function() {
this._fragments = []
}
FakeSocket.prototype.write = function(buffer, encoding) {
this._fragments.push([buffer, encoding])
}
FakeSocket.prototype.read = function() {
var output = []
this._fragments.forEach(function(buffer, i) {
for (var j = 0, n = buffer[0].length; j < n; j++)
output.push(buffer[0][j])
})
return output
}
FakeSocket.prototype.addListener = function() {}
JS.ENV.EchoServer = function() {}
EchoServer.prototype.listen = function(port, ssl) {
var server = ssl
@@ -33,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)
}
@@ -55,9 +38,17 @@ JS.Packages(function() { with(this) {
JS.require('JS.Test', function() {
JS.Test.Unit.Assertions.define("assertBufferEqual", function(array, buffer) {
this.assertEqual(array.length, buffer.length);
var ary = [], n = buffer.length;
while (n--) ary[n] = buffer[n];
this.assertEqual(array, ary);
})
JS.require( 'ClientSpec',
'Draft75ParserSpec',
'Protocol8ParserSpec',
'Draft76ParserSpec',
'HybiParserSpec',
JS.Test.method('autorun'))
})