Compare commits

..

19 Commits

Author SHA1 Message Date
James Coglan 7a1cb15b8c Bump version to 0.6.0. 2015-07-08 20:17:01 +01:00
James Coglan 14937dd365 Emit an error event *after* setting the ready state to 3. 2015-07-08 20:14:14 +01:00
James Coglan 780dd331fe The 'connect' event should emit a ConnectEvent object. 2015-07-07 21:33:15 +01:00
James Coglan 4a3d51cc5b Incorporate StreamReader into Draft75 so that it retains input and recovers safely from exceptions in event listeners. 2015-07-07 21:30:06 +01:00
James Coglan ed17186354 In the Hybi driver, perform parser state changes before validating and emitting events. This protects the parser against errors thrown by event listeners. 2015-07-07 20:36:30 +01:00
James Coglan 1df1293456 Change stage in the Base.shutdown() method to that all parsers stop processing input. 2015-07-04 21:07:34 +01:00
James Coglan d901d3e48d If an event listener emits an error, close the connection with code 1011. 2015-07-04 20:54:22 +01:00
James Coglan a49cd60cb7 Document that on() *adds* a callback, rather than *setting* one, i.e. it does not remove any prior callbacks. 2015-07-04 19:31:36 +01:00
James Coglan 4dd872f0ef Add the pong() command to the documentation. 2015-07-04 18:50:55 +01:00
Victor Gama 90b8c9d23a Add tests to others socket states 2015-07-04 18:34:00 +01:00
Victor Gama c3ac50931e Implement unit tests for pong method 2015-07-04 18:33:59 +01:00
Victor Gama 5f6873ebc0 Ensure message is always defined 2015-07-04 18:33:59 +01:00
Victor Gama 309b5651a7 Implement 'pong' command to hybi.js 2015-07-04 18:33:59 +01:00
James Coglan 1cc0f33e1c Update the changelog for 0.5.4. 2015-03-29 23:13:10 +01:00
James Coglan b273147f0c Replace 'iff' with 'if and only if'. 2015-03-28 19:44:10 +00:00
James Coglan d1796bef12 Use require('..') to import this module in examples. 2015-03-28 09:47:59 +00:00
James Coglan 0034f5fe19 Fail the connection when the server receives an invalid Sec-WebSocket-Extensions header. 2015-03-26 08:51:34 +00:00
James Coglan 09f638893e Bump version to 0.5.4. 2015-03-12 12:59:53 +00:00
James Coglan 8a0235ef51 Don't send a close frame in response to receiving one, if we already sent a close frame. 2015-03-12 12:58:43 +00:00
13 changed files with 250 additions and 52 deletions
+18 -3
View File
@@ -1,3 +1,15 @@
### 0.6.0 / 2015-07-08
* Allow the parser to recover cleanly if event listeners raise an error
* Add a `pong` method for sending unsolicited pong frames
### 0.5.4 / 2015-03-29
* Don't emit extra close frames if we receive a close frame after we already
sent one
* Fail the connection when the driver receives an invalid
`Sec-WebSocket-Extensions` header
### 0.5.3 / 2015-02-22
* Don't treat incoming data as WebSocket frames if a client driver is closed
@@ -6,7 +18,8 @@
### 0.5.2 / 2015-02-19
* Fix compatibility with the HTTP parser on io.js
* Use `websocket-extensions` to make sure messages and close frames are kept in order
* Use `websocket-extensions` to make sure messages and close frames are kept in
order
* Don't emit multiple `error` events
### 0.5.1 / 2014-12-18
@@ -32,7 +45,8 @@
### 0.3.4 / 2014-05-08
* Don't hold memory-leaking references to I/O buffers after they have been parsed
* Don't hold memory-leaking references to I/O buffers after they have been
parsed
### 0.3.3 / 2014-04-24
@@ -40,7 +54,8 @@
### 0.3.2 / 2013-12-29
* Expand `maxLength` to cover sequences of continuation frames and `draft-{75,76}`
* Expand `maxLength` to cover sequences of continuation frames and
`draft-{75,76}`
* Decrease default maximum frame buffer size to 64MB
* Stop parsing when the protocol enters a failure mode, to save CPU cycles
+15 -6
View File
@@ -21,7 +21,7 @@ this module up to some I/O object, it will do all of this for you:
* Deal with proxies that defer delivery of the draft-76 handshake body
* Notify you when the socket is open and closed and when messages arrive
* Recombine fragmented messages
* Dispatch text, binary, ping and close frames
* Dispatch text, binary, ping, pong and close frames
* Manage the socket-closing handshake process
* Automatically reply to ping frames with a matching pong
* Apply masking to messages sent by the client
@@ -259,11 +259,11 @@ the `driver.io` stream.
#### `driver.on('open', function(event) {})`
Sets the callback to execute when the socket becomes open.
Adds a callback to execute when the socket becomes open.
#### `driver.on('message', function(event) {})`
Sets the callback to execute when a message is received. `event` will have a
Adds a callback to execute when a message is received. `event` will have a
`data` attribute containing either a string in the case of a text message or a
`Buffer` in the case of a binary message.
@@ -272,13 +272,13 @@ which emits strings for text messages and buffers for binary messages.
#### `driver.on('error', function(event) {})`
Sets the callback to execute when a protocol error occurs due to the other peer
Adds a callback to execute when a protocol error occurs due to the other peer
sending an invalid byte sequence. `event` will have a `message` attribute
describing the error.
#### `driver.on('close', function(event) {})`
Sets the callback to execute when the socket becomes closed. The `event` object
Adds a callback to execute when the socket becomes closed. The `event` object
has `code` and `reason` attributes.
#### `driver.addExtension(extension)`
@@ -298,7 +298,7 @@ when the headers are serialized and sent.
Initiates the protocol by sending the handshake - either the response for a
server-side driver or the request for a client-side one. This should be the
first method you invoke. Returns `true` iff a handshake was sent.
first method you invoke. Returns `true` if and only if a handshake was sent.
#### `driver.parse(string)`
@@ -330,6 +330,15 @@ callback are both optional. If a callback is given, it will be invoked when the
socket receives a pong frame whose content matches `string`. Returns `false` if
frames can no longer be sent, or if the driver does not support ping/pong.
#### `driver.pong(string = '')`
Sends a pong frame over the socket, queueing it if necessary. `string` is
optional. Returns `false` if frames can no longer be sent, or if the driver does
not support ping/pong.
You don't need to call this when a ping frame is received; pings are replied to
automatically by the driver. This method is for sending unsolicited pongs.
#### `driver.close()`
Initiates the closing handshake if the socket is still open. For drivers with no
+1 -1
View File
@@ -1,5 +1,5 @@
var net = require('net'),
websocket = require('../lib/websocket/driver'),
websocket = require('..'),
deflate = require('permessage-deflate');
var server = net.createServer(function(connection) {
+10 -2
View File
@@ -3,13 +3,15 @@
var Emitter = require('events').EventEmitter,
util = require('util'),
streams = require('../streams'),
Headers = require('./headers');
Headers = require('./headers'),
Reader = require('./stream_reader');
var Base = function(request, url, options) {
Emitter.call(this);
Base.validateOptions(options || {}, ['maxLength', 'masking', 'requireMasking', 'protocols']);
this._request = request;
this._reader = new Reader();
this._options = options || {};
this._maxLength = this._options.maxLength || this.MAX_LENGTH;
this._headers = new Headers();
@@ -77,7 +79,9 @@ var instance = {
start: function() {
if (this.readyState !== 0) return false;
this._write(this._handshakeResponse());
var response = this._handshakeResponse();
if (!response) return false;
this._write(response);
if (this._stage !== -1) this._open();
return true;
},
@@ -94,6 +98,10 @@ var instance = {
return false;
},
pong: function() {
return false;
},
close: function(reason, code) {
if (this.readyState !== 1) return false;
this.readyState = 3;
+1 -1
View File
@@ -79,8 +79,8 @@ var instance = {
_failHandshake: function(message) {
message = 'Error during WebSocket handshake: ' + message;
this.emit('error', new Error(message));
this.readyState = 3;
this.emit('error', new Error(message));
this.emit('close', new Base.CloseEvent(this.ERRORS.protocol_error, message));
},
+6 -5
View File
@@ -26,9 +26,10 @@ var instance = {
parse: function(buffer) {
if (this.readyState > 1) return;
var data, message, value;
for (var i = 0, n = buffer.length; i < n; i++) {
data = buffer[i];
this._reader.put(buffer);
this._reader.eachByte(function(data) {
var message, value;
switch (this._stage) {
case -1:
@@ -60,9 +61,9 @@ var instance = {
case 2:
if (data === 0xFF) {
this._stage = 0;
message = new Buffer(this._buffer).toString('utf8', 0, this._buffer.length);
this.emit('message', new Base.MessageEvent(message));
this._stage = 0;
}
else {
if (this._length) {
@@ -76,7 +77,7 @@ var instance = {
}
break;
}
}
}, this);
},
frame: function(data) {
+31 -21
View File
@@ -5,14 +5,12 @@ var crypto = require('crypto'),
Extensions = require('websocket-extensions'),
Base = require('./base'),
Frame = require('./hybi/frame'),
Message = require('./hybi/message'),
Reader = require('./hybi/stream_reader');
Message = require('./hybi/message');
var Hybi = function(request, url, options) {
Base.apply(this, arguments);
this._extensions = new Extensions();
this._reader = new Reader();
this._stage = 0;
this._masking = this._options.masking;
this._protocols = this._options.protocols || [];
@@ -133,16 +131,16 @@ var instance = {
case 3:
buffer = this._reader.read(4);
if (buffer) {
this._frame.maskingKey = buffer;
this._stage = 4;
this._frame.maskingKey = buffer;
}
break;
case 4:
buffer = this._reader.read(this._frame.length);
if (buffer) {
this._emitFrame(buffer);
this._stage = 0;
this._emitFrame(buffer);
}
break;
@@ -169,6 +167,12 @@ var instance = {
return this.frame(message, 'ping');
},
pong: function(message) {
if (this.readyState > 1) return false;
message = message ||'';
return this.frame(message, 'pong');
},
close: function(reason, code) {
reason = reason || '';
code = code || this.ERRORS.normal_closure;
@@ -281,7 +285,12 @@ var instance = {
},
_handshakeResponse: function() {
var extensions = this._extensions.generateResponse(this._request.headers['sec-websocket-extensions']);
try {
var extensions = this._extensions.generateResponse(this._request.headers['sec-websocket-extensions']);
} catch (e) {
return this._fail('protocol_error', e.message);
}
if (extensions) this._headers.set('Sec-WebSocket-Extensions', extensions);
var start = 'HTTP/1.1 101 Switching Protocols',
@@ -290,23 +299,25 @@ var instance = {
return new Buffer(headers.join('\r\n'), 'utf8');
},
_shutdown: function(code, reason) {
_shutdown: function(code, reason, error) {
delete this._frame;
delete this._message;
this.readyState = 2;
this._stage = 5;
var sendCloseFrame = (this.readyState === 1);
this.readyState = 2;
this._extensions.close(function() {
this.frame(reason, 'close', code);
if (sendCloseFrame) this.frame(reason, 'close', code);
this.readyState = 3;
if (error) this.emit('error', new Error(reason));
this.emit('close', new Base.CloseEvent(code, reason));
}, this);
},
_fail: function(type, message) {
if (this.readyState > 1) return;
this.emit('error', new Error(message));
this._shutdown(this.ERRORS[type], message);
this._shutdown(this.ERRORS[type], message, true);
},
_parseOpcode: function(data) {
@@ -322,6 +333,8 @@ var instance = {
frame.rsv3 = rsvs[2];
frame.opcode = (data & this.OPCODE);
this._stage = 1;
if (!this._extensions.validFrameRsv(frame))
return this._fail('protocol_error',
'One or more reserved bits are on: reserved1 = ' + (frame.rsv1 ? 1 : 0) +
@@ -336,38 +349,35 @@ var instance = {
if (this._message && this.OPENING_OPCODES.indexOf(frame.opcode) >= 0)
return this._fail('protocol_error', 'Received new data frame but previous continuous frame is unfinished');
this._stage = 1;
},
_parseLength: function(data) {
var frame = this._frame;
frame.masked = (data & this.MASK) === this.MASK;
if (this._requireMasking && !frame.masked)
return this._fail('unacceptable', 'Received unmasked frame but masking is required');
frame.length = (data & this.LENGTH);
if (frame.length >= 0 && frame.length <= 125) {
if (!this._checkFrameLength()) return;
this._stage = frame.masked ? 3 : 4;
if (!this._checkFrameLength()) return;
} else {
frame.lengthBytes = (frame.length === 126 ? 2 : 8);
this._stage = 2;
frame.lengthBytes = (frame.length === 126 ? 2 : 8);
}
if (this._requireMasking && !frame.masked)
return this._fail('unacceptable', 'Received unmasked frame but masking is required');
},
_parseExtendedLength: function(buffer) {
var frame = this._frame;
frame.length = this._getInteger(buffer);
this._stage = frame.masked ? 3 : 4;
if (this.MESSAGE_OPCODES.indexOf(frame.opcode) < 0 && frame.length > 125)
return this._fail('protocol_error', 'Received control frame having too long payload: ' + frame.length);
if (!this._checkFrameLength()) return;
this._stage = frame.masked ? 3 : 4;
},
_checkFrameLength: function() {
+2 -1
View File
@@ -3,6 +3,7 @@
var Stream = require('stream').Stream,
url = require('url'),
util = require('util'),
Base = require('./base'),
Headers = require('./headers'),
HttpParser = require('../http_parser');
@@ -69,7 +70,7 @@ var instance = {
this.headers = this._http.headers;
if (this.statusCode === 200) {
this.emit('connect');
this.emit('connect', new Base.ConnectEvent());
} else {
var message = "Can't establish a connection to the server at " + this._origin.href;
this.emit('error', new Error(message));
@@ -3,6 +3,7 @@
var StreamReader = function() {
this._queue = [];
this._queueSize = 0;
this._offset = 0;
};
StreamReader.prototype.put = function(buffer) {
@@ -15,13 +16,15 @@ StreamReader.prototype.put = function(buffer) {
StreamReader.prototype.read = function(length) {
if (length > this._queueSize) return null;
if (length === 0) return new Buffer(0);
var queue = this._queue,
first = queue[0],
buffer;
this._queueSize -= length;
var queue = this._queue,
remain = length,
first = queue[0],
buffers, buffer;
if (first.length >= length) {
this._queueSize -= length;
if (first.length === length) {
return queue.shift();
} else {
@@ -30,10 +33,8 @@ StreamReader.prototype.read = function(length) {
return buffer;
}
}
var remain = length, buffers;
for (var i=0, n = queue.length; i < n; i++) {
for (var i = 0, n = queue.length; i < n; i++) {
if (remain < queue[i].length) break;
remain -= queue[i].length;
}
@@ -43,10 +44,26 @@ StreamReader.prototype.read = function(length) {
buffers.push(queue[0].slice(0, remain));
queue[0] = queue[0].slice(remain);
}
this._queueSize -= length;
return this._concat(buffers, length);
};
StreamReader.prototype.eachByte = function(callback, context) {
var buffer, n, index;
while (this._queue.length > 0) {
buffer = this._queue[0];
n = buffer.length;
while (this._offset < n) {
index = this._offset;
this._offset += 1;
callback.call(context, buffer[index]);
}
this._offset = 0;
this._queue.shift();
}
};
StreamReader.prototype._concat = function(buffers, length) {
if (Buffer.concat) return Buffer.concat(buffers, length);
+1 -1
View File
@@ -5,7 +5,7 @@
, "keywords" : ["websocket"]
, "license" : "MIT"
, "version" : "0.5.3"
, "version" : "0.6.0"
, "engines" : {"node": ">=0.6.0"}
, "main" : "./lib/websocket/driver"
, "dependencies" : {"websocket-extensions": ">=0.1.1"}
+1 -1
View File
@@ -184,7 +184,7 @@ test.describe("Client", function() { with(this) {
}})
it("emits a 'connect' event when the proxy connects", function() { with(this) {
expect(proxy, "emit").given("connect")
expect(proxy, "emit").given("connect", anything())
expect(proxy, "emit").given("close")
expect(proxy, "emit").given("end")
proxy.write(new Buffer("HTTP/1.1 200 OK\r\n\r\n"))
+22
View File
@@ -42,6 +42,28 @@ test.describe("draft-75", function() { with(this) {
driver().parse([0x6c, 0x6f, 0xff])
assertEqual( "Hello", message )
}})
describe("when a message listener throws an error", function() { with(this) {
before(function() { with(this) {
this.messages = []
driver().on("message", function(msg) {
messages.push(msg.data)
throw new Error("an error")
})
}})
it("is not trapped by the parser", function() { with(this) {
var buffer = [0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff]
assertThrows(Error, function() { driver().parse(buffer) })
}})
it("parses text frames without dropping input", function() { with(this) {
try { driver().parse([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff, 0x00, 0x57]) } catch (e) {}
try { driver().parse([0x6f, 0x72, 0x6c, 0x64, 0xff]) } catch (e) {}
assertEqual( ["Hello", "World"], messages )
}})
}})
}})
describe("frame", function() { with(this) {
+115
View File
@@ -79,6 +79,37 @@ test.describe("Hybi", function() { with(this) {
}})
}})
describe("with invalid extensions", function() { with(this) {
before(function() { with(this) {
request().headers["sec-websocket-extensions"] = "x-webkit- -frame"
}})
it("does not write a handshake", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().start()
}})
it("does not trigger the onopen event", function() { with(this) {
driver().start()
assertEqual( false, open )
}})
it("triggers the onerror event", function() { with(this) {
driver().start()
assertEqual( "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame", error.message )
}})
it("triggers the onclose event", function() { with(this) {
driver().start()
assertEqual( [1002, "Invalid Sec-WebSocket-Extensions header: x-webkit- -frame"], close )
}})
it("changes the state to closed", function() { with(this) {
driver().start()
assertEqual( "closed", driver().getState() )
}})
}})
describe("with custom headers", function() { with(this) {
before(function() { with(this) {
driver().setHeader("Authorization", "Bearer WAT")
@@ -160,6 +191,30 @@ test.describe("Hybi", function() { with(this) {
}})
}})
describe("pong", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().pong()
}})
it("returns true", function() { with(this) {
assertEqual( true, driver().pong() )
}})
it("queues the pong until the handshake has been sent", function() { with(this) {
expect(driver().io, "emit").given("data", buffer(
"HTTP/1.1 101 Switching Protocols\r\n" +
"Upgrade: websocket\r\n" +
"Connection: Upgrade\r\n" +
"Sec-WebSocket-Accept: JdiiuafpBKRqD7eol0y4vJDTsTs=\r\n" +
"\r\n"))
expect(driver().io, "emit").given("data", buffer([0x8a, 0]))
driver().pong()
driver().start()
}})
}})
describe("close", function() { with(this) {
it("does not write anything to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
@@ -330,6 +385,28 @@ test.describe("Hybi", function() { with(this) {
driver().parse([0x89, 0x04, 0x4f, 0x48, 0x41, 0x49])
assertEqual( [0x8a, 0x04, 0x4f, 0x48, 0x41, 0x49], collector().bytes )
}})
describe("when a message listener throws an error", function() { with(this) {
before(function() { with(this) {
this.messages = []
driver().on("message", function(msg) {
messages.push(msg.data)
throw new Error("an error")
})
}})
it("is not trapped by the parser", function() { with(this) {
var buffer = [0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]
assertThrows(Error, function() { driver().parse(buffer) })
}})
it("parses unmasked text frames without dropping input", function() { with(this) {
try { driver().parse([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x81, 0x05]) } catch (e) {}
try { driver().parse([0x57, 0x6f, 0x72, 0x6c, 0x64]) } catch (e) {}
assertEqual( ["Hello", "World"], messages )
}})
}})
}})
describe("frame", function() { with(this) {
@@ -394,6 +471,17 @@ test.describe("Hybi", function() { with(this) {
}})
}})
describe("pong", function() { with(this) {
it("writes a pong frame to the socket", function() { with(this) {
driver().pong("mic check")
assertEqual([0x8a, 0x09, 0x6d, 0x69, 0x63, 0x20, 0x63, 0x68, 0x65, 0x63, 0x6b], collector().bytes)
}})
it("returns true", function() { with(this) {
assertEqual(true, driver().pong())
}})
}})
describe("close", function() { with(this) {
it("writes a close frame to the socket", function() { with(this) {
driver().close("<%= reasons %>", 1003)
@@ -467,6 +555,17 @@ test.describe("Hybi", function() { with(this) {
}})
}})
describe("pong", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().pong()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().pong() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
@@ -490,6 +589,11 @@ test.describe("Hybi", function() { with(this) {
it("changes the state to closed", function() { with(this) {
assertEqual( "closed", driver().getState() )
}})
it("does not write another close frame", function() { with(this) {
expect(driver().io, "emit").exactly(0)
this.driver().parse([0x88, 0x04, 0x03, 0xe9, 0x4f, 0x4b])
}})
}})
}})
@@ -522,6 +626,17 @@ test.describe("Hybi", function() { with(this) {
}})
}})
describe("pong", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)
driver().pong()
}})
it("returns false", function() { with(this) {
assertEqual( false, driver().pong() )
}})
}})
describe("close", function() { with(this) {
it("does not write to the socket", function() { with(this) {
expect(driver().io, "emit").exactly(0)